Compare commits

..
Author SHA1 Message Date
LogicandDuansg b9f1d65cb5 [refactor] Apache Fory (#3873)
Signed-off-by: Logic <zqr10159@dromara.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-27 17:28:44 +08:00
Logic 1dea5467fc [refactor]: springboot4.0.0 (#3872) 2025-11-27 17:22:07 +08:00
1840 changed files with 12449 additions and 117487 deletions
+1 -47
View File
@@ -2545,8 +2545,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
"profile": "https://github.com/Saramanda9988",
"contributions": [
"doc",
"code"
"doc"
]
},
{
@@ -2668,51 +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"
]
}
],
"contributorsPerLine": 7,
-55
View File
@@ -1,55 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
# The line max length is 120, match checkstyle rules.
max_line_length = 120
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
tab_width = 2
indent_size = 2
[*.{yml,yaml}]
tab_width = 2
indent_size = 2
[*.xml]
ij_xml_attribute_wrap = off
ij_xml_text_wrap = off
ij_xml_keep_blank_lines = 1
[pom.xml]
indent_size = 2
[*.java]
indent_size = 4
tab_width = 4
[*.js]
indent_size = 2
[*.ts]
indent_size = 2
-82
View File
@@ -1,82 +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:
# Set permissions for deployment
permissions:
contents: write
jobs:
deploy:
name: Deploy Documentation to asf-site Branch
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup Node.js environment
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Install dependencies in home directory
- name: Install Dependencies
working-directory: home
run: npm ci
# Build the documentation site
- name: Build Documentation
working-directory: home
run: npm 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 }}"
-1
View File
@@ -39,7 +39,6 @@ header:
- '**/*.js'
- '**/*.less'
- '**/*.txt'
- '**/*.st'
- '**/target/**'
- '.gitattributes'
- '**/.gitignore'
+1 -1
View File
@@ -31,7 +31,7 @@
"MD025": true,
"MD029": true,
"MD033": {
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font", "Tabs", "TabItem"]
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font"]
},
"MD036": false,
"MD040": true,
-22
View File
@@ -1,22 +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.
# Testcontainers configuration
# Disable Ryuk container reuse to avoid issues in CI
testcontainers.reuse.enable=false
# Configure Docker host detection
testcontainers.docker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy
+1 -8
View File
@@ -525,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>
@@ -543,13 +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>
</tr>
</tbody>
</table>
+1 -8
View File
@@ -522,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>
@@ -540,13 +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>
</tr>
</tbody>
</table>
+1 -8
View File
@@ -525,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>
@@ -543,13 +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>
</tr>
</tbody>
</table>
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ftp",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorFTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "api",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorHTTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ping",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPing}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPort}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "fullsite",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSiteMap}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ssl_cert",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSSL}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "udp_port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorUDP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "website",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorWebsite}}"
},
"params": [
+1 -1
View File
@@ -4,7 +4,7 @@
"intervals": 10,
"tags": [],
"app": "kubernetes",
"instance": "172.29.0.11",
"host": "172.29.0.11",
"name": "Brave_Stingray_55yR"
},
"collector": "",
+2 -2
View File
@@ -26,8 +26,8 @@
<artifactId>hertzbeat-ai</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.1.1</spring-ai.version>
<java.version>17</java.version>
<spring-ai.version>1.0.3</spring-ai.version>
<java.version>21</java.version>
</properties>
<dependencies>
@@ -0,0 +1,246 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.subject.SubjectSum;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Custom Server-Sent Events transport provider for Model Context Protocol.
*/
@Slf4j
public class CustomSseServerTransport implements McpServerTransportProvider {
private final ObjectMapper objectMapper;
private final String messageEndpoint;
private final String sseEndpoint;
private final String baseUrl;
@Getter
private final RouterFunction<ServerResponse> routerFunction;
@Setter
private McpServerSession.Factory sessionFactory;
private final Map<String, Object> sessionRequest = new HashMap<>();
private final ConcurrentHashMap<String, McpServerSession> sessions;
private volatile boolean isClosing;
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, "/sse");
}
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
this(objectMapper, "", messageEndpoint, sseEndpoint);
}
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
this.sessions = new ConcurrentHashMap();
this.isClosing = false;
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(baseUrl, "Message base URL must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
}
public Mono<Void> notifyClients(String method, Object params) {
if (this.sessions.isEmpty()) {
log.debug("No active sessions to broadcast message to");
return Mono.empty();
} else {
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
return Flux.fromIterable(this.sessions.values())
.flatMap((session) -> session.sendNotification(method, params)
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
}
public Mono<Void> closeGracefully() {
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
this.isClosing = true;
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
}
private ServerResponse handleSseConnection(ServerRequest request) {
log.debug("Handling SSE connection for request: {}", request);
HttpServletRequest servletRequest = request.servletRequest();
try {
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
} catch (Exception e) {
log.error("Authentication failed for SSE connection: {}", e.getMessage());
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
}
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else {
String sessionId = UUID.randomUUID().toString();
log.debug("Generated session ID for SSE connection: {}", sessionId);
log.debug("Creating new SSE connection for session: {}", sessionId);
return ServerResponse.sse((sseBuilder) -> {
sseBuilder.onComplete(() -> {
log.debug("SSE connection completed for session: {}", sessionId);
this.sessions.remove(sessionId);
});
sseBuilder.onTimeout(() -> {
log.debug("SSE connection timed out for session: {}", sessionId);
this.sessions.remove(sessionId);
});
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
McpServerSession session = this.sessionFactory.create(sessionTransport);
this.sessionRequest.put(sessionId, request.servletRequest());
this.sessions.put(sessionId, session);
try {
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
} catch (Exception e) {
log.error("Failed to send initial endpoint event: {}", e.getMessage());
sseBuilder.error(e);
}
}, Duration.ZERO);
}
}
private ServerResponse handleMessage(ServerRequest request) {
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else if (request.param("sessionId").isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
} else {
String sessionId = (String) request.param("sessionId").get();
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
McpContextHolder.setSubject(subject);
if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
} else {
try {
String body = request.body(String.class);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
session.handle(message).block();
return ServerResponse.ok().build();
} catch (IOException | IllegalArgumentException e) {
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
} catch (Exception e) {
log.error("Error handling message: {}", e.getMessage());
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
}
}
}
}
private class WebMvcMcpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ServerResponse.SseBuilder sseBuilder;
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
this.sessionId = sessionId;
this.sseBuilder = sseBuilder;
log.debug("Session transport {} initialized with SSE builder", sessionId);
}
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
log.debug("Message sent to session {}", this.sessionId);
} catch (Exception e) {
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
this.sseBuilder.error(e);
}
});
}
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
}
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
log.debug("Closing session transport: {}", this.sessionId);
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
});
}
public void close() {
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
}
}
}
@@ -33,8 +33,8 @@ public final class McpContextHolder {
* Attaches the user's context to the current thread.
*/
public static void setSubject(SubjectSum subject) {
subjectHolder.set(subject);
}
/**
@@ -0,0 +1,182 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
import org.springframework.stereotype.Component;
/**
* Provider for system prompts used in the AI agent
*/
@Component
public class PromptProvider {
/**
* Static version of the HertzBeat monitoring prompt
*/
public static final String HERTZBEAT_SYSTEM_PROMPT = """
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
*******
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
********
## Available HertzBeat Tools:
### Monitor Management Tools:
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
### Alert Rule Management Tools:
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
- **toggle_alert_rule**: Enable or disable alert rules
- **get_alert_rule_details**: Get detailed information about specific alert rules
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
### Alert & Alarm Analysis Tools:
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
- **get_alerts_summary**: Get alert statistics and status distribution
### Metrics Data Analysis Tools:
- **query_realtime_metrics**: Get current real-time metrics data for monitors
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
- **get_warehouse_status**: Check metrics storage system status
## Natural Language Examples:
### Monitor Management:
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
- "Monitor website https://example.com with SSL checking every 60 seconds"
- "Show me all Linux servers that are currently offline"
- "List all Redis monitors with their connection status"
### Alert Configuration:
- ALERT RULE means when to alert a user
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
- "Alert when OpenAI credit grants exceed 1000"
- "Set up HBase Master alert when heap memory usage is over 80%"
### Metrics Analysis:
- "Show me current CPU usage for server 192.168.1.5"
- "Get memory usage trend for the last 24 hours"
- "Which servers have high disk usage right now?"
- "Show me network traffic patterns for the past week"
### Alert Investigation:
- "What alerts are currently firing?"
- "Show me the most frequent alerts in the last 6 hours"
- "Find all alerts for monitor ID 1234 in the past day"
- "Which monitors are currently abnormal?"
## Workflow Guidelines:
1. **Adding Monitors**:
- ALWAYS use get_monitor_additional_params first to check required parameters
- Use list_monitor_types to show available types
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
2. **Creating Alert Rules or Alerts**:
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
Read the create_alert_rule tool description for even more details
*******
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions
6. You will construct the proper expression with field conditions
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
*********
- Field Condition Expression format: [field_conditions]
- Give all the available fieldConditions to the user, so they can choose the one they want to use
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
- Use parentheses for complex conditions to ensure correct evaluation order
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
→ equals(VmName, "my-vm")
- Complex OpenAI: app="openai", metrics="credit_grants",
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Priority levels: 0=critical, 1=warning, 2=info
3. **Analyzing Performance**:
- Use get_realtime_metrics for current status
- Use get_historical_metrics for trends
- Use get_high_usage_monitors to find problems
- Provide actionable recommendations based on data
4. **Troubleshooting Alerts**:
- Use query_alerts to find current issues
- Use get_monitor_alerts for specific monitor problems
- Use get_frequent_alerts to identify recurring issues
- Suggest root cause analysis steps
## Parameter Guidelines:
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
- **Alert Priorities**: critical, warning, info
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
- **Metric Types**: cpu, memory, disk, network, custom
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
## Best Practices:
- Never create alert rules without exact user input on app, metrics, and field conditions
- Always validate monitor types and parameters before adding monitors
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
- Construct field conditions based on metric's children
- Use exact app and metric names from hierarchy (case-sensitive)
- Set appropriate alert thresholds based on baseline performance
- Use time-series data to identify trends and predict issues
- Correlate alerts with metrics data for root cause analysis
- Recommend monitoring intervals based on service criticality
- Provide clear explanations of monitoring data and actionable insights
## Avoid these common errors:
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
@@ -21,25 +21,10 @@ import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository interface for Message entities
*/
@Repository
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
/**
* Find all messages by conversation id, ordered by create time
* @param conversationId conversation id
* @return list of messages
*/
List<ChatMessage> findByConversationIdOrderByGmtCreateAsc(Long conversationId);
/**
* Find all messages by conversation ids, ordered by create time
* @param conversationIds list of conversation ids
* @return list of messages
*/
List<ChatMessage> findByConversationIdInOrderByGmtCreateAsc(List<Long> conversationIds);
}
@@ -18,7 +18,12 @@
package org.apache.hertzbeat.ai.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Service interface for MCP server operations.
@@ -30,4 +35,22 @@ public interface McpServerService {
* @return ToolCallbackProvider with all HertzBeat monitoring tools
*/
ToolCallbackProvider hertzbeatTools();
/**
* Provides a custom SSE server transport for the MCP server
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
);
/**
* Provides the MCP server router function for web MVC
* @param transport Custom SSE server transport
* @return RouterFunction for handling MCP server requests
*/
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
}
@@ -19,15 +19,13 @@
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
@@ -36,6 +34,7 @@ import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
@@ -54,15 +53,13 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
private final ApplicationContext applicationContext;
private final GeneralConfigDao generalConfigDao;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
private boolean isConfigured = false;
@Value("classpath:/prompt/system-message.st")
private Resource systemResource;
@Autowired
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
this.applicationContext = applicationContext;
@@ -94,7 +91,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
return chatClient.prompt()
.messages(messages)
.system(SystemPromptTemplate.builder().resource(systemResource).build().getTemplate())
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
@@ -30,15 +30,11 @@ 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;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
@@ -46,13 +42,13 @@ import java.util.stream.Collectors;
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@@ -71,13 +67,12 @@ public class ConversationServiceImpl implements ConversationService {
}
log.info("Starting streaming conversation: {}", conversationId);
ChatConversation conversation = conversationDao.findById(conversationId)
.orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId));
// Manually load messages for conversation history
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
conversation.setMessages(messages);
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
@@ -85,18 +80,10 @@ public class ConversationServiceImpl implements ConversationService {
conversationDao.save(conversation);
}
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder()
.conversationId(conversationId)
.content(message)
.role("user")
.build();
chatMessage = messageDao.save(chatMessage);
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
@@ -118,12 +105,10 @@ public class ConversationServiceImpl implements ConversationService {
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
ChatMessage assistantMessage = ChatMessage.builder()
.conversationId(conversationId)
.content(fullResponse.toString())
.role("assistant")
.build();
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
@@ -160,40 +145,16 @@ public class ConversationServiceImpl implements ConversationService {
if (conversationId == null) {
return null;
}
ChatConversation conversation = conversationDao.findById(conversationId).orElse(null);
if (conversation != null) {
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
conversation.setMessages(messages);
}
return conversation;
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ChatConversation> getAllConversations() {
List<ChatConversation> conversations = conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
if (conversations.isEmpty()) {
return conversations;
}
List<Long> conversationIds = conversations.stream()
.map(ChatConversation::getId)
.toList();
List<ChatMessage> allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds);
Map<Long, List<ChatMessage>> messagesByConversationId = allMessages.stream()
.collect(Collectors.groupingBy(ChatMessage::getConversationId));
for (ChatConversation conversation : conversations) {
List<ChatMessage> messages = messagesByConversationId.getOrDefault(conversation.getId(), Collections.emptyList());
conversation.setMessages(messages);
}
return conversations;
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteConversation(Long conversationId) {
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
if (!messages.isEmpty()) {
messageDao.deleteAll(messages);
}
conversationDao.deleteById(conversationId);
}
}
@@ -18,17 +18,23 @@
package org.apache.hertzbeat.ai.service.impl;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.service.McpServerService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Implementation of the McpServerService interface.
@@ -50,4 +56,36 @@ public class McpServerServiceImpl implements McpServerService {
public ToolCallbackProvider hertzbeatTools() {
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
}
/**
* Provides a custom SSE server transport for the MCP server.
*
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
@Bean
public CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
) {
return new CustomSseServerTransport(
objectMapper,
serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(),
serverProperties.getSseEndpoint()
);
}
/**
* Provides the MCP server transport bean.
*
* @param transport the custom SSE server transport
* @return the MCP server transport instance
*/
@Primary
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
return transport.getRouterFunction();
}
}
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools;
/**
@@ -41,4 +42,5 @@ public interface AlertTools {
*/
String getAlertsSummary();
}
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools;
/**
@@ -32,17 +33,16 @@ public interface MetricsTools {
/**
* Get historical metrics data for a monitor
*
* @param instance Instance identifier (e.g., "ip:port", "ip", or "domain")
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param monitorId Monitor ID
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param interval Whether to aggregate data with intervals
* @return Historical metrics data formatted for display
*/
String getHistoricalMetrics(String instance, String app, String metrics, String metric, String label, String history, Boolean interval);
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
/**
* Check warehouse storage server status
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools;
import java.util.List;
@@ -26,25 +27,35 @@ public interface MonitorTools {
/**
* Add a new monitor with comprehensive configuration
*
*
* @param name Monitor name
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
* @param host Target host (IP address or domain name)
* @param port Target port (optional, depends on monitor type)
* @param intervals Collection interval in seconds (default: 600)
* @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.)
* @param username Username for authentication (optional)
* @param password Password for authentication (optional)
* @param database Database name (for database monitors)
* @param additionalParams Additional app-specific parameters as JSON string (optional)
* @param description Monitor description (optional)
* @return Result message with monitor ID if successful
*/
String addMonitor(
String name,
String app,
String name,
String app,
String host,
Integer port,
Integer intervals,
String params,
String username,
String password,
String database,
String additionalParams,
String description
);
/**
* List all available monitor types that can be added
*
*
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
* @return Formatted string list of available monitor types with descriptions
*/
@@ -78,9 +89,9 @@ public interface MonitorTools {
/**
* Get parameter definitions required for a specific monitor type
*
*
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorParams(String app);
String getMonitorAdditionalParams(String app);
}
@@ -51,9 +51,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AppService appService;
@Override
@Tool(name = "create_alert_rule", description = """
HertzBeat: ALERT RULE means when to alert a user
ALERT RULE means when to alert a user
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
@@ -280,7 +281,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
@Override
@Tool(name = "list_alert_rules", description = """
HertzBeat: List existing alert rules with filtering options.
List existing alert rules with filtering options.
Shows configured thresholds and alert definitions.
""")
public String listAlertRules(
@@ -342,7 +343,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
@Override
@Tool(name = "toggle_alert_rule", description = """
HertzBeat: Enable or disable an alert rule.
Enable or disable an alert rule.
Allows activating or deactivating threshold monitoring.
""")
public String toggleAlertRule(
@@ -378,9 +379,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
}
@Override
@Tool(name = "get_alert_rule_details", description = """
HertzBeat: Get detailed information about a specific alert rule.
Get detailed information about a specific alert rule.
Shows complete threshold configuration and rule settings.
""")
public String getAlertRuleDetails(
@@ -439,7 +441,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
@Override
@Tool(name = "get_apps_metrics_hierarchy", description = """
HertzBeat: Get the hierarchical structure of all available apps and their metrics for alert rule creation.
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
This tool provides the exact app name, metric name and corresponding param names according to each metric.
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
@@ -492,7 +494,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
@Override
@Tool(name = "bind_monitors_to_alert_rule", description = """
HertzBeat: Bind monitors to an alert rule.
Bind monitors to an alert rule.
Call this tool if users want to bind specific monitors to their alert rule.
Get the right monitor ids for a particular app using the query_monitors tool.
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
@@ -645,6 +647,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
}
/**
* Validates that the app, metric, and field conditions are valid according to hierarchy
* @param app App name to validate
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
@@ -32,6 +33,7 @@ import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
/**
* Implementation of Alert Tools functionality for alarm data queries and management
*/
@@ -43,7 +45,7 @@ public class AlertToolsImpl implements AlertTools {
@Override
@Tool(name = "query_alerts", description = """
HertzBeat: Query alerts with comprehensive filtering and pagination options.
Query alerts with comprehensive filtering and pagination options.
ALERT TYPES:
- Pass alertType='single' for individual alert instances
@@ -191,9 +193,10 @@ public class AlertToolsImpl implements AlertTools {
}
}
@Override
@Tool(name = "get_alerts_summary", description = """
HertzBeat: Get alerts summary statistics including total counts, status distribution, and recent trends.
Get alerts summary statistics including total counts, status distribution, and recent trends.
Returns comprehensive overview of the current alerting status across all monitors.
""")
public String getAlertsSummary() {
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
@@ -46,7 +47,7 @@ public class MetricsToolsImpl implements MetricsTools {
@Override
@Tool(name = "query_realtime_metrics", description = """
HertzBeat: Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
Get real-time metrics data for a specific monitor.
Returns current metrics values including CPU, memory, disk usage, etc.
@@ -143,7 +144,7 @@ public class MetricsToolsImpl implements MetricsTools {
@Override
@Tool(name = "get_historical_metrics", description = """
HertzBeat: Get historical metrics data for analysis and trending.
Get historical metrics data for analysis and trending.
Returns time-series data for specified metrics over a time range.
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
@@ -151,7 +152,7 @@ public class MetricsToolsImpl implements MetricsTools {
Ask user to provide the filters for labels, history and interval aggregation
""")
public String getHistoricalMetrics(
@ToolParam(description = "Instance identifier (e.g., 'ip:port', 'ip', or 'domain')") String instance,
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
@@ -160,7 +161,7 @@ public class MetricsToolsImpl implements MetricsTools {
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
try {
log.info("Getting historical metrics for monitor instance {} and metrics {}", instance, metrics);
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
if (history == null || history.trim().isEmpty()) {
history = "24h";
@@ -169,15 +170,15 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(instance,
app, metrics, fieldParameter, history, interval);
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
return String.format("No historical metrics data found for monitor %s and metrics '%s'", instance, metrics);
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(instance).append(")\n");
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
response.append("=".repeat(60)).append("\n\n");
@@ -222,9 +223,10 @@ public class MetricsToolsImpl implements MetricsTools {
}
}
@Override
@Tool(name = "get_warehouse_status", description = """
HertzBeat: Check the status of the metrics storage warehouse system.
Check the status of the metrics storage warehouse system.
Returns whether the metrics storage is operational and accessible.
""")
public String getWarehouseStatus() {
@@ -20,7 +20,6 @@ package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
@@ -49,6 +48,7 @@ public class MonitorToolsImpl implements MonitorTools {
@Autowired
private AppService appService;
/**
* Tool to query monitor information with flexible filtering and pagination.
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
@@ -56,48 +56,48 @@ public class MonitorToolsImpl implements MonitorTools {
* Returns detailed monitor information including ID, name, type, host, and status.
*/
@Override
@Tool(name = "query_monitors", description = """
HertzBeat: Query Existing/configured monitors in HertzBeat.
@Tool(name = "query_monitors", description = """
Query Existing/configured monitors in HertzBeat.
This tool retrieves monitors based on various filters and parameters.
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
MONITOR STATUSES:
- status=1: Online/Active monitors (healthy, responding normally)
- status=2: Offline monitors (not responding, connection failed)
- status=3: Unreachable monitors (network/connectivity issues)
- status=0: Paused monitors (manually disabled/suspended)
- status=9 or null: All monitors regardless of status (default)
COMMON USE CASES & PARAMETER COMBINATIONS:
1. BASIC MONITOR LISTING:
- Default: No parameters (shows all monitors, 8 per page)
- By type: app='linux' (show only Linux monitors)
- Search: search='web' (find monitors with 'web' in name/host)
2. STATUS-BASED QUERIES:
- Healthy monitors: status=1, pageSize=50
- Problem monitors: status=2 or status=3, pageSize=50
- Offline monitors only: status=2
- Unreachable monitors only: status=3
- Paused monitors: status=0
3. MONITORING HEALTH OVERVIEW:
- All statuses with statistics: status=9, includeStats=true, pageSize=100
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
4. ADVANCED FILTERING:
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
- Label-based: labels='env:prod,critical:true'
- Host search: search='192.168' (find by IP pattern)
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
5. SORTING & PAGINATION:
- Recently updated: sort='gmtUpdate', order='desc'
- Alphabetical: sort='name', order='asc'
- By creation: sort='gmtCreate', order='desc' (newest first)
- Large datasets: pageSize=50-100 for bulk operations
RESPONSE FORMAT:
- includeStats=true: Adds status distribution summary at top
- Default: Simple list with ID, name, type, host, status
@@ -125,18 +125,18 @@ public class MonitorToolsImpl implements MonitorTools {
if (includeStats == null) {
includeStats = false;
}
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
response.append("====================\n\n");
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
@@ -144,13 +144,13 @@ public class MonitorToolsImpl implements MonitorTools {
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");
response.append("- Offline: ").append(offlineCount).append("\n");
response.append("- Offline: ").append(offlineCount).append("\n");
response.append("- Unreachable: ").append(unreachableCount).append("\n");
response.append("- Paused: ").append(pausedCount).append("\n");
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
if (total > 0) {
double healthPercentage = (onlineCount * 100.0) / total;
@@ -158,47 +158,48 @@ public class MonitorToolsImpl implements MonitorTools {
}
response.append("\n");
}
response.append("Query Results: ").append(result.getContent().size())
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
if (result.getTotalPages() > 1) {
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
}
response.append("\n");
for (Monitor monitor : result.getContent()) {
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Instance: ").append(monitor.getInstance())
.append(" | Host: ").append(monitor.getHost())
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
// Add creation date for better context
if (monitor.getGmtCreate() != null) {
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
}
response.append("\n");
}
if (result.getContent().isEmpty()) {
response.append("No monitors found matching the specified criteria.\n");
response.append("Try adjusting your filters or search terms.");
}
return response.toString();
} catch (Exception e) {
return "Error retrieving monitors: " + e.getMessage();
}
}
@Override
@Tool(name = "add_monitor", description = """
HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration.
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.
@@ -206,36 +207,41 @@ public class MonitorToolsImpl implements MonitorTools {
Use the information obtained from this to query user for parameters.
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
**********
Examples of natural language requests this tool handles:
- "Monitor website example.com with HTTPS on port 443"
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
- "Monitor Linux server health on host server.company.com via SSH"
- "Set up Redis monitoring on localhost port 6379 with password"
PARAMETER MAPPING: 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"}
PARAMETER MAPPING: The tool intelligently maps common parameters:
- host: Target server/domain
- port: Service port (auto-detected if not specified)
- username: Authentication username
- password: Authentication password
- database: Database name (for DB monitors)
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
ADDITIONAL PARAMETERS EXAMPLES:
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
""")
public String addMonitor(
@ToolParam(description = "Monitor name (required)", required = true) String name,
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
@ToolParam(description = "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 = "Username for authentication (optional)", required = false) String username,
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
try {
log.info("Adding monitor: name={}, app={}", name, app);
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Monitor name is required";
@@ -243,153 +249,128 @@ public class MonitorToolsImpl implements MonitorTools {
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application is required";
}
if (params == null || params.trim().isEmpty()) {
return "Error: Monitor params is required. Use get_monitor_additional_params to see required fields for this monitor type.";
if (host == null || host.trim().isEmpty()) {
return "Error: Host is required";
}
// Set defaults
if (intervals == null || intervals < 10) {
intervals = 600;
}
// Parse params to extract host and port for instance
List<Param> paramList = parseParams(params);
String host = paramList.stream()
.filter(p -> "host".equals(p.getField()))
.map(Param::getParamValue)
.findFirst()
.orElse("");
String port = paramList.stream()
.filter(p -> "port".equals(p.getField()))
.map(Param::getParamValue)
.findFirst()
.orElse(null);
String instance = (port != null && !port.isEmpty()) ? host.trim() + ":" + port : host.trim();
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.instance(instance)
.host(host.trim())
.intervals(intervals)
.status((byte) 1)
.type((byte) 0)
.description(description != null ? description.trim() : "")
.build();
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
// Validate that all required parameters for this monitor type are provided
try {
MonitorDto monitorDto = MonitorDto.builder().monitor(monitor).params(paramList).build();
monitorService.validate(monitorDto, false);
} catch (IllegalArgumentException argumentException) {
if (argumentException.getMessage().contains("required")) {
return String.format("Error: %s. "
+ "Or use get_monitor_additional_params tool to see all required parameters.",
argumentException.getMessage());
} else {
return String.format("Error: %s. ", argumentException.getMessage());
}
}
monitorService.addMonitor(monitor, paramList, null, null);
log.info("Successfully added monitor '{}' with ID: {}", monitor.getName(), monitor.getId());
return String.format("Successfully added %s monitor '%s' with ID: %d (Instance: %s, Interval: %d seconds)",
app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals());
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
for (ParamDefine paramDefine : requiredParams) {
if (paramDefine.isRequired()) {
String fieldName = paramDefine.getField();
boolean hasParam = params.stream()
.anyMatch(param -> fieldName.equals(param.getField()));
if (!hasParam) {
missingParams.add(fieldName);
}
}
}
if (!missingParams.isEmpty()) {
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
+ "Use get_monitor_additional_params tool to see all required parameters.",
app, String.join(", ", missingParams));
}
} catch (Exception e) {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
app.toUpperCase(), name, monitorId, host, intervals);
} catch (Exception e) {
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
return "Error adding monitor '" + name + "': " + e.getMessage();
}
}
/**
* Parse params JSON string to list of Param objects
* Create basic parameter list from user inputs
*/
private List<Param> parseParams(String params) {
List<Param> paramList = new ArrayList<>();
if (params == null || params.trim().isEmpty()) {
return paramList;
private List<Param> createBasicParams(String host, Integer port, String username,
String password, String database, String additionalParams) {
List<Param> params = new ArrayList<>();
// Add host (always required)
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
// Add optional common parameters
if (port != null) {
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
}
try {
String cleaned = params.trim();
// Remove outer braces if present
if (cleaned.startsWith("{") && cleaned.endsWith("}")) {
cleaned = cleaned.substring(1, cleaned.length() - 1);
}
// Split by comma, but handle values that might contain commas within quotes
List<String> pairs = splitJsonPairs(cleaned);
for (String pair : pairs) {
int colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
String key = pair.substring(0, colonIndex).trim().replaceAll("\"", "");
String value = pair.substring(colonIndex + 1).trim().replaceAll("\"", "");
// Determine param type based on field name
byte paramType = determineParamType(key);
paramList.add(Param.builder().field(key).paramValue(value).type(paramType).build());
if (username != null && !username.trim().isEmpty()) {
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
}
if (password != null && !password.trim().isEmpty()) {
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
}
if (database != null && !database.trim().isEmpty()) {
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
}
// Parse additional parameters if provided
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
try {
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
String[] pairs = cleaned.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].trim().replaceAll("\"", "");
String value = keyValue[1].trim().replaceAll("\"", "");
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
}
}
}
} catch (Exception e) {
log.warn("Failed to parse params: {}", e.getMessage());
}
return paramList;
}
/**
* Split JSON key-value pairs, handling quoted values that may contain commas
*/
private List<String> splitJsonPairs(String json) {
List<String> pairs = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
for (char c : json.toCharArray()) {
if (c == '"') {
inQuotes = !inQuotes;
current.append(c);
} else if (c == ',' && !inQuotes) {
if (current.length() > 0) {
pairs.add(current.toString().trim());
current = new StringBuilder();
}
} else {
current.append(c);
} catch (Exception e) {
log.warn("Failed to parse additionalParams: {}", e.getMessage());
}
}
if (current.length() > 0) {
pairs.add(current.toString().trim());
}
return pairs;
return params;
}
/**
* Determine param type based on field name
*/
private byte determineParamType(String fieldName) {
if ("password".equalsIgnoreCase(fieldName) || "privateKey".equalsIgnoreCase(fieldName)) {
return (byte) 2; // Password type
} else if ("port".equalsIgnoreCase(fieldName) || "timeout".equalsIgnoreCase(fieldName)) {
return (byte) 0; // Number type
}
return (byte) 1; // Default string type
}
@Override
@Tool(name = "list_monitor_types", description = """
HertzBeat: List all available monitor types that can be added to HertzBeat.
List all available monitor types that can be added to HertzBeat.
This tool shows all supported monitor types with their display names.
Use this to see what types of monitors you can create with the add_monitor tool.
""")
public String listMonitorTypes(
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
try {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
@@ -402,20 +383,20 @@ public class MonitorToolsImpl implements MonitorTools {
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
}
// Format the response as a nice list
StringBuilder response = new StringBuilder();
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
// Sort monitor types alphabetically by key
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.toList();
for (Map.Entry<String, String> entry : sortedTypes) {
String typeKey = entry.getKey();
String displayName = entry.getValue();
@@ -423,28 +404,28 @@ public class MonitorToolsImpl implements MonitorTools {
.append(" - ").append(displayName)
.append("\n");
}
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
log.info("Successfully listed {} monitor types", monitorTypes);
return response.toString();
} catch (Exception e) {
log.error("Failed to list monitor types: {}", e.getMessage(), e);
return "Error retrieving monitor types: " + e.getMessage();
}
}
@Override
@Tool(name = "get_monitor_params", description = """
HertzBeat: Get the parameter definitions required for a specific monitor type.
@Tool(name = "get_monitor_additional_params", description = """
Get the parameter definitions required for a specific monitor type.
This tool shows what parameters are needed when adding a monitor of the specified type,
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
""")
public String getMonitorParams(
public String getMonitorAdditionalParams(
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
try {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
@@ -457,66 +438,68 @@ public class MonitorToolsImpl implements MonitorTools {
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
+ "This monitor type may not exist or may not require additional parameters.", app);
}
// Format the response
StringBuilder response = new StringBuilder();
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
app, paramDefines.size()));
for (ParamDefine paramDefine : paramDefines) {
response.append("• Field: ").append(paramDefine.getField()).append("\n");
// Add display name if available
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
response.append(" Name: ").append(paramDefine.getName()).append("\n");
}
// Add type
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
response.append(" Type: ").append(paramDefine.getType()).append("\n");
}
// Add required status
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
// Add default value if present
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
}
// Add validation range if present
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
}
// Add limit if present
if (paramDefine.getLimit() != null) {
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
}
// Add placeholder text if present
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
}
response.append("\n");
}
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
return response.toString();
} catch (Exception e) {
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
}
}
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.ai.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -1,148 +0,0 @@
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
*******
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
********
## Available HertzBeat Tools:
### Monitor Management Tools:
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
### Alert Rule Management Tools:
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
- **toggle_alert_rule**: Enable or disable alert rules
- **get_alert_rule_details**: Get detailed information about specific alert rules
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
### Alert & Alarm Analysis Tools:
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
- **get_alerts_summary**: Get alert statistics and status distribution
### Metrics Data Analysis Tools:
- **query_realtime_metrics**: Get current real-time metrics data for monitors
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
- **get_warehouse_status**: Check metrics storage system status
## Natural Language Examples:
### Monitor Management:
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
- "Monitor website https://example.com with SSL checking every 60 seconds"
- "Show me all Linux servers that are currently offline"
- "List all Redis monitors with their connection status"
### Alert Configuration:
- ALERT RULE means when to alert a user
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
- "Alert when OpenAI credit grants exceed 1000"
- "Set up HBase Master alert when heap memory usage is over 80%"
### Metrics Analysis:
- "Show me current CPU usage for server 192.168.1.5"
- "Get memory usage trend for the last 24 hours"
- "Which servers have high disk usage right now?"
- "Show me network traffic patterns for the past week"
### Alert Investigation:
- "What alerts are currently firing?"
- "Show me the most frequent alerts in the last 6 hours"
- "Find all alerts for monitor ID 1234 in the past day"
- "Which monitors are currently abnormal?"
## Workflow Guidelines:
1. **Adding Monitors**:
- ALWAYS use get_monitor_additional_params first to check required parameters
- Use list_monitor_types to show available types
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
2. **Creating Alert Rules or Alerts**:
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
Read the create_alert_rule tool description for even more details
*******
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions
6. You will construct the proper expression with field conditions
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
*********
- Field Condition Expression format: [field_conditions]
- Give all the available fieldConditions to the user, so they can choose the one they want to use
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
- Use parentheses for complex conditions to ensure correct evaluation order
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, "my-vm")"
→ equals(VmName, "my-vm")
- Complex OpenAI: app="openai", metrics="credit_grants",
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Priority levels: 0=critical, 1=warning, 2=info
3. **Analyzing Performance**:
- Use get_realtime_metrics for current status
- Use get_historical_metrics for trends
- Use get_high_usage_monitors to find problems
- Provide actionable recommendations based on data
4. **Troubleshooting Alerts**:
- Use query_alerts to find current issues
- Use get_monitor_alerts for specific monitor problems
- Use get_frequent_alerts to identify recurring issues
- Suggest root cause analysis steps
## Parameter Guidelines:
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
- **Alert Priorities**: critical, warning, info
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
- **Metric Types**: cpu, memory, disk, network, custom
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
## Best Practices:
- Never create alert rules without exact user input on app, metrics, and field conditions
- Always validate monitor types and parameters before adding monitors
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
- Construct field conditions based on metric's children
- Use exact app and metric names from hierarchy (case-sensitive)
- Set appropriate alert thresholds based on baseline performance
- Use time-series data to identify trends and predict issues
- Correlate alerts with metrics data for root cause analysis
- Recommend monitoring intervals based on service criticality
- Provide clear explanations of monitoring data and actionable insights
## Avoid these common errors:
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
@@ -17,25 +17,23 @@
package org.apache.hertzbeat.alert;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* alarm module thread pool
* alarm module thread pool with Virtual Threads
*/
@Component
@Slf4j
public class AlerterWorkerPool {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
private ExecutorService workerExecutor;
private ExecutorService notifyExecutor;
private ExecutorService logWorkerExecutor;
public AlerterWorkerPool() {
initWorkExecutor();
@@ -44,62 +42,31 @@ public class AlerterWorkerPool {
}
private void initWorkExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter workerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(10,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("alerter-worker-", 0)
.factory();
workerExecutor = Executors.newThreadPerTaskExecutor(factory);
}
private void initNotifyExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter notifyExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("notify-worker-%d")
.build();
notifyExecutor = new ThreadPoolExecutor(6,
6,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("notify-worker-", 0)
.factory();
notifyExecutor = Executors.newThreadPerTaskExecutor(factory);
}
private void initLogWorkerExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("log-worker-", 0)
.factory();
logWorkerExecutor = Executors.newThreadPerTaskExecutor(factory);
}
/**
* Run the alerter task
* @param runnable task
* @throws RejectedExecutionException when The thread pool is full of
*/
public void executeJob(Runnable runnable) throws RejectedExecutionException {
public void executeJob(Runnable runnable){
workerExecutor.execute(runnable);
}
@@ -107,9 +74,8 @@ public class AlerterWorkerPool {
* Executes the given runnable task using the notifyExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeNotify(Runnable runnable) throws RejectedExecutionException {
public void executeNotify(Runnable runnable){
notifyExecutor.execute(runnable);
}
@@ -117,9 +83,8 @@ public class AlerterWorkerPool {
* Executes the given runnable task using the logWorkerExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
public void executeLogJob(Runnable runnable){
logWorkerExecutor.execute(runnable);
}
}
}
@@ -156,7 +156,7 @@ public class MetricsRealTimeAlertCalculator {
long currentTimeMilli = System.currentTimeMillis();
String instance = String.valueOf(metricsData.getId());
String instanceName = metricsData.getInstanceName();
String instanceHost = metricsData.getInstance();
String instanceHost = metricsData.getInstanceHost();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
@@ -205,11 +205,12 @@ public class MetricsRealTimeAlertCalculator {
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instanceHost);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
commonFingerPrints.putAll(define.getLabels());
if (labels != null) {
commonFingerPrints.putAll(labels);
@@ -71,6 +71,7 @@ public class WindowedLogRealTimeAlertCalculator implements Runnable {
}
backoff.reset();
processLogEntry(logEntry);
dataQueue.sendLogEntryToStorage(logEntry);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
@@ -22,7 +22,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
@@ -58,9 +57,6 @@ public class AlertDefineController {
@Autowired
private AlertDefineService alertDefineService;
@Autowired
private DataSourceService dataSourceService;
@PostMapping
@Operation(summary = "New Alarm Definition", description = "Added an alarm definition")
public ResponseEntity<Message<Void>> addNewAlertDefine(@Valid @RequestBody AlertDefine alertDefine) {
@@ -116,12 +112,4 @@ public class AlertDefineController {
}
}
@GetMapping(path = "/datasource/status")
@Operation(summary = "Get available datasource executors status",
description = "Get status of available datasource executors for periodic alerts")
public ResponseEntity<Message<Map<String, Object>>> getDatasourceStatus() {
Map<String, Object> status = dataSourceService.getAvailableExecutors();
return ResponseEntity.ok(Message.successWithData(status));
}
}
@@ -40,10 +40,4 @@ public interface DataSourceService {
* @return result
*/
List<Map<String, Object>> query(String datasource, String expr);
/**
* Get available datasource executors status
* @return map containing available executors by type (promql, sql)
*/
Map<String, Object> getAvailableExecutors();
}
@@ -29,14 +29,11 @@ import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.base.dao.LabelDao;
import org.apache.hertzbeat.base.service.LabelService;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.manager.Label;
import org.apache.hertzbeat.common.util.FileUtil;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.beans.factory.annotation.Autowired;
@@ -51,7 +48,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@@ -62,7 +58,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Alarm definition management interface implementation
@@ -74,31 +69,16 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Autowired
private AlertDefineDao alertDefineDao;
@Autowired
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
@Resource
private LabelService labelService;
@Resource
private LabelDao labelDao;
private final DataSourceService dataSourceService;
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
private static final Set<String> SYSTEM_BUILT_IN_LABELS = Set.of(
CommonConstants.LABEL_INSTANCE,
CommonConstants.LABEL_DEFINE_ID,
CommonConstants.LABEL_ALERT_NAME,
CommonConstants.LABEL_INSTANCE_NAME,
CommonConstants.LABEL_ALERT_SEVERITY,
CommonConstants.ALERT_MODE_LABEL
);
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList, DataSourceService dataSourceService) {
alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it));
this.dataSourceService = dataSourceService;
@@ -113,7 +93,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
JexlExpressionRunner.compile(alertDefine.getExpr());
} catch (Exception e) {
throw new IllegalArgumentException("alert expr error: " + e.getMessage());
}
}
}
}
// the name of the alarm rule is unique
@@ -127,7 +107,6 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException {
saveNewCustomLabel(alertDefine);
alertDefine = alertDefineDao.saveAndFlush(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
@@ -135,31 +114,11 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException {
saveNewCustomLabel(alertDefine);
alertDefineDao.saveAndFlush(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
}
private void saveNewCustomLabel(AlertDefine alertDefine) {
Map<String, String> labels = alertDefine.getLabels();
if (labels == null) {
labels = new HashMap<>(8);
alertDefine.setLabels(labels);
}
Map<String, String> customLabels = labels.entrySet().stream()
.filter(entry -> !isSystemBuiltInLabel(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
List<Label> addLabels = labelService.determineNewLabels(customLabels.entrySet());
if (!addLabels.isEmpty()) {
labelDao.saveAll(addLabels);
}
}
private boolean isSystemBuiltInLabel(String labelKey) {
return SYSTEM_BUILT_IN_LABELS.contains(labelKey);
}
@Override
public void deleteAlertDefine(long alertId) throws RuntimeException {
alertDefineDao.deleteById(alertId);
@@ -297,7 +256,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
if (!StringUtils.hasText(type)) {
throw new IllegalArgumentException("Alert definition type cannot be null or empty");
}
switch (type) {
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME:
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
@@ -308,7 +267,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
default:
throw new IllegalArgumentException("Unsupported alert definition type: " + type);
}
// Query enabled alert definitions by type
return alertDefineDao.findAlertDefinesByTypeAndEnableTrue(type);
}
@@ -31,16 +31,12 @@ import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.apache.hertzbeat.common.support.valid.SqlSecurityException;
import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
@@ -53,18 +49,12 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class DataSourceServiceImpl implements DataSourceService {
/**
* Default allowed tables for SQL queries
*/
private static final List<String> DEFAULT_ALLOWED_TABLES = List.of(WarehouseConstants.LOG_TABLE_NAME);
protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Setter
@Autowired(required = false)
private List<QueryExecutor> executors;
private final SqlSecurityValidator sqlSecurityValidator;
@Getter
private final Cache<String, ParseTree> expressionCache = Caffeine.newBuilder()
.maximumSize(256)
@@ -72,10 +62,12 @@ public class DataSourceServiceImpl implements DataSourceService {
.recordStats()
.build();
public DataSourceServiceImpl(@Autowired(required = false) List<QueryExecutor> executors) {
this.executors = executors != null ? executors : Collections.emptyList();
this.sqlSecurityValidator = new SqlSecurityValidator(DEFAULT_ALLOWED_TABLES);
}
@Getter
private final Cache<String, CommonTokenStream> tokenStreamCache = Caffeine.newBuilder()
.maximumSize(512)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats()
.build();
@Override
public List<Map<String, Object>> calculate(String datasource, String expr) {
@@ -118,46 +110,19 @@ public class DataSourceServiceImpl implements DataSourceService {
}
// replace all white space
expr = expr.replaceAll("\\s+", " ");
// SQL security validation for SQL-based datasources
if (isSqlDatasource(datasource)) {
validateSqlSecurity(expr);
}
try {
return executor.execute(expr);
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new AlertExpressionException(e.getMessage());
}
}
/**
* Check if the datasource is SQL-based
*/
private boolean isSqlDatasource(String datasource) {
return datasource != null && datasource.equalsIgnoreCase(WarehouseConstants.SQL);
}
/**
* Validate SQL statement for security
*/
private void validateSqlSecurity(String sql) {
try {
sqlSecurityValidator.validate(sql);
} catch (SqlSecurityException e) {
log.warn("SQL security validation failed: {}", e.getMessage());
throw new AlertExpressionException("SQL security validation failed: " + e.getMessage());
throw new RuntimeException("Query execution failed", e);
}
}
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
CommonTokenStream tokens = createTokenStream(expr);
CommonTokenStream tokens = tokenStreamCache.get(expr, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
ParseTree tree = expressionCache.get(expr, e -> parser.expr());
// Validate EOF only during the first parsing (when tokens.index > 0)
// Skip EOF check when cache hit, as it has already been validated on the first pass.
if (tokens.index() > 0 && tokens.LA(1) != Token.EOF) {
if (null != tokens && tokens.LA(1) != Token.EOF) {
throw new AlertExpressionException(bundle.getString("alerter.calculate.parse.error"));
}
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor, tokens);
@@ -168,34 +133,4 @@ public class DataSourceServiceImpl implements DataSourceService {
AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expr));
return new CommonTokenStream(lexer);
}
@Override
public Map<String, Object> getAvailableExecutors() {
boolean hasPromqlExecutor = false;
boolean hasSqlExecutor = false;
java.util.Set<String> availableExecutors = new java.util.HashSet<>();
if (executors != null) {
for (QueryExecutor executor : executors) {
String datasource = executor.getDatasource();
availableExecutors.add(datasource);
// Check if executor supports promql
if (executor.support(WarehouseConstants.PROMQL)) {
hasPromqlExecutor = true;
}
// Check if executor supports sql
if (executor.support(WarehouseConstants.SQL)) {
hasSqlExecutor = true;
}
}
}
Map<String, Object> result = new java.util.HashMap<>(8);
result.put("hasPromqlExecutor", hasPromqlExecutor);
result.put("hasSqlExecutor", hasSqlExecutor);
result.put("availableExecutors", availableExecutors);
return result;
}
}
}
@@ -18,7 +18,6 @@
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
@@ -44,7 +43,6 @@ import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@@ -72,8 +70,6 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
private static final String SUBSCRIBE_URL_PREFIX = "https://console.huaweicloud.com/smn/subscription/confirm";
private final AlarmCommonReduce alarmCommonReduce;
public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
@@ -201,9 +197,6 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
if (StringUtils.isBlank(subscribeUrl)) {
return;
}
if (!subscribeUrl.startsWith(SUBSCRIBE_URL_PREFIX)) {
throw new SecurityException("Untrusted domain: " + subscribeUrl);
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(subscribeUrl);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
@@ -310,13 +303,6 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
*/
private X509Certificate getCertificate(String signCertUrl) throws Exception {
URL url = new URL(signCertUrl);
if (!"https".equalsIgnoreCase(url.getProtocol())) {
throw new SecurityException("Only HTTPS is allowed");
}
boolean trusted = Arrays.stream(Region.values()).anyMatch(ep -> ep.getEndpoint().equals(url.getHost()));
if (!trusted) {
throw new SecurityException("Untrusted domain: " + url.getHost());
}
try (InputStream in = url.openStream()) {
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
return (X509Certificate) cf.generateCertificate(in);
@@ -354,51 +340,4 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
return "huaweicloud-ces";
}
/**
* doc: <a href="https://console.huaweicloud.com/apiexplorer/#/endpoint/SMN">SMN API</a>
*/
@Getter
private enum Region {
AE_AD_1("ae-ad-1", "smn.ae-ad-1.myhuaweicloud.com"),
AF_SOUTH_1("af-south-1", "smn.af-south-1.myhuaweicloud.com"),
AP_SOUTHEAST_1("ap-southeast-1", "smn.ap-southeast-1.myhuaweicloud.com"),
AP_SOUTHEAST_2("ap-southeast-2", "smn.ap-southeast-2.myhuaweicloud.com"),
AP_SOUTHEAST_3("ap-southeast-3", "smn.ap-southeast-3.myhuaweicloud.com"),
AP_SOUTHEAST_4("ap-southeast-4", "smn.ap-southeast-4.myhuaweicloud.com"),
CN_EAST_2("cn-east-2", "smn.cn-east-2.myhuaweicloud.com"),
CN_EAST_3("cn-east-3", "smn.cn-east-3.myhuaweicloud.com"),
CN_EAST_4("cn-east-4", "smn.cn-east-4.myhuaweicloud.com"),
CN_EAST_5("cn-east-5", "smn.cn-east-5.myhuaweicloud.com"),
CN_NORTH_1("cn-north-1", "smn.cn-north-1.myhuaweicloud.com"),
CN_NORTH_11("cn-north-11", "smn.cn-north-11.myhuaweicloud.com"),
CN_NORTH_12("cn-north-12", "smn.cn-north-12.myhuaweicloud.com"),
CN_NORTH_2("cn-north-2", "smn.cn-north-2.myhuaweicloud.cn"),
CN_NORTH_4("cn-north-4", "smn.cn-north-4.myhuaweicloud.com"),
CN_NORTH_9("cn-north-9", "smn.cn-north-9.myhuaweicloud.com"),
CN_SOUTH_1("cn-south-1", "smn.cn-south-1.myhuaweicloud.com"),
CN_SOUTH_2("cn-south-2", "smn.cn-south-2.myhuaweicloud.com"),
CN_SOUTH_4("cn-south-4", "smn.cn-south-4.myhuaweicloud.com"),
CN_SOUTHWEST_2("cn-southwest-2", "smn.cn-southwest-2.myhuaweicloud.com"),
CN_SOUTHWEST_3("cn-southwest-3", "smn.cn-southwest-3.myhuaweicloud.com"),
EU_WEST_0("eu-west-0", "smn.eu-west-0.myhuaweicloud.com"),
LA_NORTH_2("la-north-2", "smn.la-north-2.myhuaweicloud.com"),
LA_SOUTH_2("la-south-2", "smn.la-south-2.myhuaweicloud.com"),
MY_KUALALUMPUR_1("my-kualalumpur-1", "smn.my-kualalumpur-1.myhuaweicloud.com"),
NA_MEXICO_1("na-mexico-1", "smn.na-mexico-1.myhuaweicloud.com"),
RU_MOSCOW_1("ru-moscow-1", "smn.ru-moscow-1.myhuaweicloud.com"),
SA_BRAZIL_1("sa-brazil-1", "smn.sa-brazil-1.myhuaweicloud.com"),
TR_WEST_1("tr-west-1", "smn.tr-west-1.myhuaweicloud.com"),
EU_WEST_101("eu-west-101", "smn.eu-west-101.myhuaweicloud.eu");
private final String id;
private final String endpoint;
Region(String id, String endpoint) {
this.id = id;
this.endpoint = endpoint;
}
}
}
@@ -303,8 +303,9 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
@Override
public boolean sendTestMsg(NoticeReceiver noticeReceiver) {
Map<String, String> labels = new HashMap<>(8);
labels.put(CommonConstants.LABEL_INSTANCE, "127.0.0.1");
labels.put(CommonConstants.LABEL_INSTANCE, "1000000");
labels.put(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert");
labels.put(CommonConstants.LABEL_INSTANCE_HOST, "127.0.0.1");
Map<String, String> annotations = new HashMap<>(8);
annotations.put("suggest", "Please check the CPU usage of the server");
SingleAlert singleAlert1 = SingleAlert.builder()
@@ -125,7 +125,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj_copy");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
@@ -175,7 +175,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
@@ -225,7 +225,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Vibrant_Gazelle_83vJ");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(responseTime));
@@ -108,6 +108,7 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService).isLateData(anyLong());
verify(timeService).updateMaxTimestamp(anyLong());
verify(logWorker).reduceAndSendLogTask(validLogEntry);
verify(dataQueue).sendLogEntryToStorage(validLogEntry);
}
@Test
@@ -133,6 +134,7 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService, never()).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(invalidTimestampLogEntry);
}
@Test
@@ -160,6 +162,7 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(lateDataLogEntry);
}
@Test
@@ -88,7 +88,7 @@ class AlertDefineControllerTest {
Monitor.builder()
.id(1L)
.app("app")
.instance("localhost")
.host("localhost")
.name("monitor")
.build()
)
@@ -21,8 +21,6 @@ import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
import org.apache.hertzbeat.base.dao.LabelDao;
import org.apache.hertzbeat.base.service.LabelService;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.junit.jupiter.api.BeforeEach;
@@ -71,13 +69,7 @@ class AlertDefineServiceTest {
@Mock
private AlertDefineDao alertDefineDao;
@Mock
private LabelService labelService;
@Mock
private LabelDao labelDao;
@Mock
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
@@ -94,8 +86,6 @@ class AlertDefineServiceTest {
void setUp() {
ReflectionTestUtils.setField(this.alertDefineService, "alertDefineDao", alertDefineDao);
ReflectionTestUtils.setField(this.alertDefineService, "periodicAlertRuleScheduler", periodicAlertRuleScheduler);
ReflectionTestUtils.setField(this.alertDefineService, "labelService", labelService);
ReflectionTestUtils.setField(this.alertDefineService, "labelDao", labelDao);
this.alertDefine = AlertDefine.builder()
.id(1L)
@@ -197,40 +187,40 @@ class AlertDefineServiceTest {
AlertDefine.builder().id(1L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(2L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache hit
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(cachedAlertDefines);
List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines();
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(1L, result.get(0).getId());
assertEquals(2L, result.get(1).getId());
// Verify no database query was called
verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any());
cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(any()), times(0));
}
// Test cache miss scenario
List<AlertDefine> dbAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(3L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache miss
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(null);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(dbAlertDefines);
List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines();
assertNotNull(result);
assertEquals(1, result.size());
assertEquals(3L, result.get(0).getId());
// Verify database query and cache setting were called
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(dbAlertDefines), times(1));
@@ -244,40 +234,40 @@ class AlertDefineServiceTest {
AlertDefine.builder().id(4L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(5L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache hit
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(cachedAlertDefines);
List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines();
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(4L, result.get(0).getId());
assertEquals(5L, result.get(1).getId());
// Verify no database query was called
verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any());
cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(any()), times(0));
}
// Test cache miss scenario
List<AlertDefine> dbAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(6L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache miss
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(null);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(dbAlertDefines);
List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines();
assertNotNull(result);
assertEquals(1, result.size());
assertEquals(6L, result.get(0).getId());
// Verify database query and cache setting were called
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(dbAlertDefines), times(1));
@@ -290,58 +280,58 @@ class AlertDefineServiceTest {
AlertDefine.builder().id(7L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(8L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
// Test valid metric realtime alert type
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(mockAlertDefines);
List<AlertDefine> result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(7L, result.get(0).getId());
assertEquals(8L, result.get(1).getId());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
// Test valid metric periodic alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC))
.thenReturn(Lists.newArrayList());
result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
assertNotNull(result);
assertEquals(0, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
// Test valid log realtime alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(mockAlertDefines);
result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
assertNotNull(result);
assertEquals(2, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
// Test valid log periodic alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC))
.thenReturn(Lists.newArrayList());
result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
assertNotNull(result);
assertEquals(0, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
// Test empty string type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(""));
// Test null type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(null));
// Test invalid type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType("invalid_type"));
}
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.alert.service;
import com.github.benmanes.caffeine.cache.Cache;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
@@ -35,10 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
@@ -50,7 +48,7 @@ class DataSourceServiceTest {
@BeforeEach
void setUp() {
dataSourceService = new DataSourceServiceImpl(null);
dataSourceService = new DataSourceServiceImpl();
}
@Test
@@ -556,11 +554,13 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
String expr = "node_cpu_seconds_total > 50";
Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache();
Cache<String, CommonTokenStream> tokenStreamCache = dataSourceService.getTokenStreamCache();
expressionCache.invalidateAll();
long beforeHits = expressionCache.stats().hitCount();
tokenStreamCache.invalidateAll();
long beforeHits = tokenStreamCache.stats().hitCount();
dataSourceService.calculate("promql", expr);
dataSourceService.calculate("promql", expr);
long actualHits = expressionCache.stats().hitCount() - beforeHits;
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(1, actualHits, "expression cache should hit but miss");
}
@@ -575,7 +575,9 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
dataSourceService.calculate("promql", "node_cpu_seconds_total > 50");
Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache();
Cache<String, CommonTokenStream> tokenStreamCache = dataSourceService.getTokenStreamCache();
expressionCache.invalidateAll();
tokenStreamCache.invalidateAll();
String expr1 = "node_cpu_seconds_total > 30";
String expr2 = "node_cpu_seconds_total > 50";
long beforeHits = expressionCache.stats().hitCount();
@@ -596,13 +598,15 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
dataSourceService.calculate("promql", "node_cpu_seconds_total > 50");
Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache();
Cache<String, CommonTokenStream> tokenStreamCache = dataSourceService.getTokenStreamCache();
expressionCache.invalidateAll();
tokenStreamCache.invalidateAll();
String expr1 = "node_cpu_seconds_total > 30";
String expr2 = "node_cpu_seconds_total > 50";
long beforeHits = expressionCache.stats().hitCount();
long beforeHits = tokenStreamCache.stats().hitCount();
dataSourceService.calculate("promql", expr1);
dataSourceService.calculate("promql", expr2);
long actualHits = expressionCache.stats().hitCount() - beforeHits;
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(0, actualHits, "expression cache should miss but hit");
}
@@ -635,156 +639,4 @@ class DataSourceServiceTest {
assertThrows(AlertExpressionException.class, () -> dataSourceService.calculate("promql", "http_server_requests_seconds_count{!@~!!#$%^&}"));
}
@Test
void query1() {
List<Map<String, Object>> sqlData = List.of(
new HashMap<>(Map.of("count", 10, "severity_text", "ERROR"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
when(mockExecutor.execute(anyString())).thenReturn(sqlData);
dataSourceService.setExecutors(List.of(mockExecutor));
String validSql = "SELECT count(*) FROM hertzbeat_logs WHERE severity_text = 'ERROR'";
List<Map<String, Object>> result = dataSourceService.query("sql", validSql);
assertNotNull(result);
assertEquals(1, result.size());
verify(mockExecutor).execute(anyString());
}
@Test
void query2() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "INSERT INTO hertzbeat_logs (body) VALUES ('test')"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query3() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "DELETE FROM hertzbeat_logs WHERE id = 1"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query4() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "UPDATE hertzbeat_logs SET body = 'hacked' WHERE id = 1"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query5() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "DROP TABLE hertzbeat_logs"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query6() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_logs UNION SELECT * FROM users"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query7() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_logs WHERE id IN (SELECT id FROM other_table)"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query8() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "SELECT * FROM users"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query9() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "WITH cte AS (SELECT * FROM hertzbeat_logs) SELECT * FROM cte"));
verify(mockExecutor, never()).execute(anyString());
}
@Test
void query10() {
List<Map<String, Object>> sqlData = List.of(
new HashMap<>(Map.of("errorCount", 5))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
when(mockExecutor.execute(anyString())).thenReturn(sqlData);
dataSourceService.setExecutors(List.of(mockExecutor));
String complexSql = "SELECT count(*) AS errorCount FROM hertzbeat_logs "
+ "WHERE time_unix_nano >= NOW() AND severity_text = 'ERROR' "
+ "GROUP BY severity_text HAVING count(*) > 2 ORDER BY errorCount LIMIT 10";
List<Map<String, Object>> result = dataSourceService.query("sql", complexSql);
assertNotNull(result);
verify(mockExecutor).execute(anyString());
}
@Test
void query11() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>(Map.of("__value__", 100.0))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("promql")).thenReturn(true);
when(mockExecutor.execute(anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.query("promql", "node_cpu_seconds_total > 50");
assertNotNull(result);
verify(mockExecutor).execute(anyString());
}
@Test
void query12() {
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support("sql")).thenReturn(true);
dataSourceService.setExecutors(List.of(mockExecutor));
assertThrows(AlertExpressionException.class,
() -> dataSourceService.query("sql", "SELEC * FORM hertzbeat_logs"));
verify(mockExecutor, never()).execute(anyString());
}
}
@@ -146,42 +146,7 @@ public class HuaweiCloudExternAlertServiceTest {
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testSubscriptionUrlFail() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938"
+ "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4"
+ ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX&region=cn-north-4");
externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s"
+ "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK"
+ "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w==");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0");
externAlert.setType("SubscriptionConfirmation");
externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:"
+ "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours.");
externAlert.setSigningCertUrl("https://xxxx.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:07:14Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testCertUrlFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb"
+ "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
+ "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
+ "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("DCS Redis “dcs-h4tv” ");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("Notification");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://xxx.myhuaweicloud.com/failedUrl");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -83,10 +83,6 @@
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
</dependency>
<dependency>
<groupId>com.xugudb</groupId>
<artifactId>xugu-jdbc</artifactId>
</dependency>
<!-- postgresql -->
<dependency>
<groupId>org.postgresql</groupId>
@@ -529,8 +529,6 @@ public class JdbcCommonCollect extends AbstractCollect {
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false";
case "xugu" -> "jdbc:xugu://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "postgresql" -> "jdbc:postgresql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "clickhouse" -> "jdbc:clickhouse://" + host + ":" + port
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -15,80 +15,80 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.collect.http;
package org.apache.hertzbeat.collector.collect. http;
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
import static org.apache.hertzbeat. common.constants.SignConstants.RIGHT_DASH;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com. google.gson.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java. io.InterruptedIOException;
import java.io.StringReader;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util. List;
import java.util. Map;
import java.util. Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.net.ssl.SSLException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml. parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax. xml.xpath.XPathConstants;
import javax.xml.xpath. XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.extern.slf4j.Slf4j;
import lombok.extern.slf4j. Slf4j;
import org. apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse;
import org.apache.hertzbeat.collector.collect.http.promethus.PrometheusParseCreator;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache. hertzbeat.collector.collect. prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache. hertzbeat.collector.dispatch. DispatchConstants;
import org. apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.collector.util.JsonPathParser;
import org.apache.hertzbeat.collector.util.TimeExpressionUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache. hertzbeat.common.constants. NetworkConstants;
import org.apache. hertzbeat.common.constants. SignConstants;
import org.apache.hertzbeat.common.entity.job. Metrics;
import org.apache. hertzbeat.common.entity. job.protocol.HttpProtocol;
import org.apache.hertzbeat. common.entity.message.CollectRep;
import org.apache.hertzbeat. common.util.Base64Util;
import org. apache.hertzbeat.common. util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;
import org.apache. hc.core5.http.ClassicHttpResponse;
import org. apache.hc.core5. http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache. hc.core5.http. HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache. hc.client5.http.auth.AuthScope;
import org.apache. hc.client5.http. auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.ClientProtocolException;
import org. apache.hc.client5. http.config.RequestConfig;
import org. apache.hc.client5. http.classic.methods.HttpGet;
import org. apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org. apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache. hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc. core5.http.protocol.HttpContext;
import org. apache.hc.core5. http.io.entity.EntityUtils;
import org.apache. hc.core5.util.Timeout;
import org. springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org. springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;
import org.xml.sax.InputSource;
@@ -97,9 +97,9 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java. util.regex.Pattern;
import java.util.HashSet;
import java.util.ArrayList;
import java.util. ArrayList;
import java.util.Collections;
/**
@@ -109,18 +109,24 @@ import java.util.Collections;
public class HttpCollectImpl extends AbstractCollect {
/**
* Pre-compiled regex patterns for dangerous Xpath detection.
* Compiled once at class load for performance.
* Inner class to wrap HTTP request with its configuration
*/
private static final List<Pattern> DANGEROUS_XPATH_PATTERNS;
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
static {
List<Pattern> patterns = new ArrayList<>();
for (String pattern : CollectorConstants.DANGEROUS_XPATH_PATTERNS) {
// Use CASE_INSENSITIVE to prevent bypass via case variations (e.g., //TEXT() vs //text())
patterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
DANGEROUS_XPATH_PATTERNS = Collections.unmodifiableList(patterns);
}
private final Set<Integer> defaultSuccessStatusCodes = Set.of(
@@ -139,28 +145,37 @@ public class HttpCollectImpl extends AbstractCollect {
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
public void collect(CollectRep. MetricsData.Builder builder, Metrics metrics) {
long startTime = System.currentTimeMillis();
HttpProtocol httpProtocol = metrics.getHttp();
String url = httpProtocol.getUrl();
if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) {
httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH);
if (! StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) {
httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH);
}
if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) {
httpProtocol.setSuccessCodes(List.of(HttpStatus.SC_OK + ""));
httpProtocol. setSuccessCodes(List. of(HttpStatus.SC_OK + ""));
}
HttpContext httpContext = createHttpContext(metrics.getHttp());
HttpUriRequest request = createHttpRequest(metrics.getHttp());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getHttp());
if (requestWithConfig == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to create HTTP request");
return;
}
ClassicHttpRequest request = requestWithConfig. getRequest();
HttpContext httpContext = createHttpContext(metrics. getHttp(), requestWithConfig. getRequestConfig());
// Use HttpClientResponseHandler to handle the response and avoid deprecated execute method
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
boolean isSuccessInvoke = checkSuccessInvoke(metrics, statusCode);
log.debug("http response status: {}", statusCode);
if (!isSuccessInvoke) {
builder.setCode(CollectRep.Code.FAIL);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
return null;
}
long responseTime = System.currentTimeMillis() - startTime;
@@ -170,25 +185,20 @@ public class HttpCollectImpl extends AbstractCollect {
try {
if (DispatchConstants.PARSE_PROMETHEUS.equals(parseType)) {
if (entity != null) {
parseResponseByPrometheusExporter(entity.getContent(), metrics.getAliasFields(), builder);
parseResponseByPrometheusExporter(entity. getContent(), metrics.getAliasFields(), builder);
}
} else if (DispatchConstants.PARSE_HEADER.equals(parseType)) {
parseResponseByHeader(builder, metrics.getAliasFields(), response);
// Consume entity to release connection
EntityUtils.consumeQuietly(entity);
} else {
/*
this could create large objects, potentially impacting JVM memory space significantly.
Option 1: Parse using InputStream, but this requires significant code changes;
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
*/
String resp = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
String resp = entity == null ? "" : EntityUtils. toString(entity, StandardCharsets. UTF_8);
if (!StringUtils.hasText(resp)) {
log.info("http response entity is empty, status: {}.", statusCode);
}
switch (parseType) {
case DispatchConstants.PARSE_JSON_PATH ->
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
parseResponseByJsonPath(resp, metrics. getAliasFields(), metrics. getHttp(), builder, responseTime);
case DispatchConstants.PARSE_PROM_QL ->
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_XML_PATH ->
@@ -196,7 +206,7 @@ public class HttpCollectImpl extends AbstractCollect {
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
parseResponseBySiteMap(resp, metrics. getAliasFields(), builder);
case DispatchConstants.PARSE_CONFIG ->
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
default ->
@@ -208,15 +218,20 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("parse response data error:" + e.getMessage());
}
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error(errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder. setCode(CollectRep. Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (UnknownHostException e2) {
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
log.info(errorMsg);
builder.setCode(CollectRep.Code.UN_REACHABLE);
builder. setCode(CollectRep. Code.UN_REACHABLE);
builder.setMsg("unknown host:" + errorMsg);
} catch (InterruptedIOException | ConnectException | SSLException e3) {
String errorMsg = CommonUtil.getMessageFromThrowable(e3);
@@ -225,7 +240,7 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setMsg(errorMsg);
} catch (IOException e4) {
String errorMsg = CommonUtil.getMessageFromThrowable(e4);
log.info(errorMsg);
log. info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
@@ -233,17 +248,13 @@ public class HttpCollectImpl extends AbstractCollect {
log.error(errorMsg, e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
}
private void parseResponseByHeader(CollectRep.MetricsData.Builder builder, List<String> aliases, CloseableHttpResponse response) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
private void parseResponseByHeader(CollectRep.MetricsData.Builder builder, List<String> aliases, ClassicHttpResponse response) {
CollectRep.ValueRow. Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliases) {
if (!StringUtils.hasText(alias)) {
if (! StringUtils.hasText(alias)) {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
continue;
}
@@ -255,7 +266,7 @@ public class HttpCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(firstHeader.getValue());
}
builder.addValueRow(valueRowBuilder.build());
builder. addValueRow(valueRowBuilder.build());
}
@Override
@@ -280,7 +291,7 @@ public class HttpCollectImpl extends AbstractCollect {
private void addColumnForSummary(Long responseTime, CollectRep.ValueRow.Builder valueRowBuilder, int keywordNum, String alias) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
@@ -299,13 +310,13 @@ public class HttpCollectImpl extends AbstractCollect {
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(new InputSource(new StringReader(resp)));
NodeList urlList = document.getElementsByTagName("url");
for (int i = 0; i < urlList.getLength(); i++) {
for (int i = 0; i < urlList. getLength(); i++) {
Node urlNode = urlList.item(i);
NodeList childNodes = urlNode.getChildNodes();
for (int k = 0; k < childNodes.getLength(); k++) {
Node currentNode = childNodes.item(k);
// distinguish between text nodes and element nodes
if (currentNode.getNodeType() == Node.ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) {
if (currentNode.getNodeType() == Node. ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) {
// retrieves the value of the loc node
siteUrls.add(currentNode.getFirstChild().getNodeValue());
break;
@@ -335,10 +346,13 @@ public class HttpCollectImpl extends AbstractCollect {
long startTime = System.currentTimeMillis();
try {
HttpGet httpGet = new HttpGet(siteUrl);
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) {
statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Integer> statusHandler = response -> {
EntityUtils.consume(response.getEntity());
}
return response.getCode();
};
statusCode = CommonHttpClient.getHttpClient().execute(httpGet, statusHandler);
} catch (ClientProtocolException e1) {
if (e1.getCause() != null) {
errorMsg = e1.getCause().getMessage();
@@ -362,7 +376,7 @@ public class HttpCollectImpl extends AbstractCollect {
} else if (NetworkConstants.STATUS_CODE.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(statusCode == null
? CommonConstants.NULL_VALUE : String.valueOf(statusCode));
} else if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
} else if (NetworkConstants. RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(responseTime));
} else if (NetworkConstants.ERROR_MSG.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(errorMsg);
@@ -374,99 +388,37 @@ public class HttpCollectImpl extends AbstractCollect {
}
}
/**
* Validates the Xpath expression to prevent DoS attacks.
* Checks for dangerous patterns that could traverse the entire XML document.
* Uses pre-compiled patterns for performance and case-insensitive matching for security.
*
* @param xpathExpression the Xpath expression to validate
* @throws IllegalArgumentException if the expression contains dangerous patterns
*/
private void validateXpathExpression(String xpathExpression) throws IllegalArgumentException {
if (!StringUtils.hasText(xpathExpression)) {
return;
}
// Check against dangerous patterns using pre-compiled regex
for (Pattern pattern : DANGEROUS_XPATH_PATTERNS) {
Matcher matcher = pattern.matcher(xpathExpression);
if (matcher.find()) {
throw new IllegalArgumentException(
"Xpath expression contains dangerous pattern that may cause DoS: " + pattern.pattern()
);
}
}
// Check for excessive wildcard usage (more than 3 // or * operators)
long descendantAxisCount = xpathExpression.chars().filter(ch -> ch == '/').count();
long wildcardCount = xpathExpression.chars().filter(ch -> ch == '*').count();
if (descendantAxisCount > 10 || wildcardCount > 5) {
throw new IllegalArgumentException(
"Xpath expression contains too many wildcards or descendant axes, potential DoS risk"
);
}
log.debug("Xpath expression validation passed: {}", xpathExpression);
}
private void parseResponseByXmlPath(String resp, Metrics metrics,
CollectRep.MetricsData.Builder builder, Long responseTime) {
HttpProtocol http = metrics.getHttp();
List<String> aliasFields = metrics.getAliasFields();
String xpathExpression = http.getParseScript();
// Layer 1: Validate Xpath expression is not empty
if (!StringUtils.hasText(xpathExpression)) {
if (! StringUtils.hasText(xpathExpression)) {
log.warn("Http collect parse type is xmlPath, but the xpath expression is empty.");
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Xpath expression is empty");
builder.setMsg("XPath expression is empty");
return;
}
// Layer 2: Check XML response size to prevent memory exhaustion
if (resp != null && resp.length() > CollectorConstants.MAX_XML_RESPONSE_SIZE) {
log.warn("XML response size {} bytes exceeds maximum allowed size {} bytes",
resp.length(), CollectorConstants.MAX_XML_RESPONSE_SIZE);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XML response exceeds maximum allowed size of "
+ (CollectorConstants.MAX_XML_RESPONSE_SIZE / 1024 / 1024) + "MB");
return;
}
// Layer 3: Validate Xpath expression for dangerous patterns
try {
validateXpathExpression(xpathExpression);
} catch (IllegalArgumentException e) {
log.warn("Xpath expression validation failed: {}", e.getMessage());
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(e.getMessage());
return;
}
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Layer 4: Enable XML secure processing and XXE protection
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
DocumentBuilder db = dbf. newDocumentBuilder();
Document document = db.parse(new InputSource(new StringReader(resp)));
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
NodeList nodeList = (NodeList) xpath.evaluate(xpathExpression, document, XPathConstants.NODESET);
NodeList nodeList = (NodeList) xpath. evaluate(xpathExpression, document, XPathConstants.NODESET);
if (nodeList == null || nodeList.getLength() == 0) {
log.debug("Xpath expression '{}' returned no nodes.", xpathExpression);
log.debug("XPath expression '{}' returned no nodes.", xpathExpression);
boolean requestedSummaryFields = aliasFields.stream()
.anyMatch(alias -> NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)
|| CollectorConstants.KEYWORD.equalsIgnoreCase(alias));
@@ -476,10 +428,10 @@ public class HttpCollectImpl extends AbstractCollect {
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants. KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
valueRowBuilder.addColumn(CommonConstants. NULL_VALUE);
}
}
builder.addValueRow(valueRowBuilder.build());
@@ -487,30 +439,21 @@ public class HttpCollectImpl extends AbstractCollect {
return;
}
// Layer 5: Limit the number of results to prevent excessive resource consumption
int resultSize = nodeList.getLength();
int maxResults = CollectorConstants.MAX_XPATH_RESULT_NODES;
if (resultSize > maxResults) {
log.warn("Xpath expression returned {} nodes, exceeding limit of {}. Processing first {} nodes only.",
resultSize, maxResults, maxResults);
resultSize = maxResults;
}
for (int i = 0; i < resultSize; i++) {
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
if (NetworkConstants. RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants. KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
try {
String value = (String) xpath.evaluate(alias, node, XPathConstants.STRING);
valueRowBuilder.addColumn(StringUtils.hasText(value) ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(StringUtils.hasText(value) ? value : CommonConstants.NULL_VALUE);
} catch (XPathExpressionException e) {
log.warn("Failed to evaluate Xpath '{}' for node [{}]: {}", alias, node.getNodeName(), e.getMessage());
log.warn("Failed to evaluate XPath '{}' for node [{}]: {}", alias, node.getNodeName(), e.getMessage());
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
}
}
@@ -519,8 +462,8 @@ public class HttpCollectImpl extends AbstractCollect {
}
} catch (Exception e) {
log.warn("Failed to parse XML response with Xpath '{}': {}", xpathExpression, e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
log.warn("Failed to parse XML response with XPath '{}': {}", xpathExpression, e.getMessage(), e);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg("Failed to parse XML response: " + e.getMessage());
}
}
@@ -529,25 +472,12 @@ public class HttpCollectImpl extends AbstractCollect {
/**
* Parses the response body in Properties/Config format.
* Two modes are supported:
* 1. single-object mode: if http.parseScript is null, aliasFields are treated as indicator names.
* - If there is a locator in the indicator definition, use the locator as the key of the Properties.
* - Otherwise, use aliasField (metric name) as the key for Properties.
* Generate a single row of data.
* 2. array mode: if http.parseScript is not empty (e.g. “users”), treat it as an array base path.
* Treat aliasFields as the attribute name of an array element, and generate a single row of data for each array index. locator is invalid in this mode.
*
* @param resp Response body string
* @param aliasFields List of metrics aliases (i.e., the list of fields in metrics.fields).
* @param http http protocol configuration
* @param builder The metrics data builder.
* @param responseTime response time
*/
private void parseResponseByConfig(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
if (!StringUtils.hasText(resp)) {
if (! StringUtils.hasText(resp)) {
log.warn("Http collect parse type is config, but response body is empty.");
builder.setCode(CollectRep.Code.FAIL);
builder. setCode(CollectRep. Code.FAIL);
builder.setMsg("Response body is empty");
return;
}
@@ -557,14 +487,14 @@ public class HttpCollectImpl extends AbstractCollect {
properties.load(reader);
} catch (IOException e) {
log.warn("Failed to parse config response: {}", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg("Failed to parse config response: " + e.getMessage());
return;
}
String arrayBasePath = http.getParseScript();
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
if (!StringUtils.hasText(arrayBasePath)) {
if (! StringUtils.hasText(arrayBasePath)) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
@@ -573,7 +503,7 @@ public class HttpCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
String value = properties.getProperty(alias);
valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(value != null ? value : CommonConstants. NULL_VALUE);
}
}
CollectRep.ValueRow valueRow = valueRowBuilder.build();
@@ -606,13 +536,13 @@ public class HttpCollectImpl extends AbstractCollect {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
valueRowBuilder.addColumn(responseTime. toString());
} else if (CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) {
valueRowBuilder. addColumn(Integer.toString(keywordNum));
} else {
String currentKey = arrayBasePath + "[" + i + "]." + alias;
String value = properties.getProperty(currentKey);
valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(value != null ? value : CommonConstants.NULL_VALUE);
}
}
CollectRep.ValueRow valueRow = valueRowBuilder.build();
@@ -637,24 +567,24 @@ public class HttpCollectImpl extends AbstractCollect {
for (int i = 0; i < valueRow.getColumnsCount(); i++) {
String columnValue = valueRow.getColumns(i);
String alias = aliasFields.get(i);
if (!CommonConstants.NULL_VALUE.equals(columnValue) && (!NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) && !CollectorConstants.KEYWORD.equalsIgnoreCase(alias))) {
if (! CommonConstants.NULL_VALUE.equals(columnValue) && (! NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) && !CollectorConstants.KEYWORD. equalsIgnoreCase(alias))) {
hasMeaningfulData = true;
break;
}
if ((NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) || CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) && !CommonConstants.NULL_VALUE.equals(columnValue)) {
if ((NetworkConstants.RESPONSE_TIME. equalsIgnoreCase(alias) || CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) && ! CommonConstants.NULL_VALUE.equals(columnValue)) {
hasMeaningfulData = true;
}
}
return hasMeaningfulData;
}
@SuppressWarnings("unchecked")
private void parseResponseByJsonPath(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
List<Object> results = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript());
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
for (int i = 0; i < results.size(); i++) {
Object objectValue = results.get(i);
// if a property is missing or empty due to target version issues, filter it. Refer to the app-elasticsearch.yml configuration under name: nodes
if (objectValue == null) {
continue;
}
@@ -666,8 +596,8 @@ public class HttpCollectImpl extends AbstractCollect {
if (value != null) {
valueRowBuilder.addColumn(String.valueOf(value));
} else {
if (alias.startsWith("$.")) {
List<Object> subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias.substring(1));
if (alias.startsWith("$. ")) {
List<Object> subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias. substring(1));
if (subResults != null && subResults.size() > i) {
Object resultValue = subResults.get(i);
valueRowBuilder.addColumn(resultValue == null ? CommonConstants.NULL_VALUE : String.valueOf(resultValue));
@@ -721,7 +651,7 @@ public class HttpCollectImpl extends AbstractCollect {
return;
}
MetricFamily metricFamily = metricFamilyMap.get(metrics);
if (null == metricFamily || CollectionUtils.isEmpty(metricFamily.getMetricList())) {
if (null == metricFamily || CollectionUtils.isEmpty(metricFamily. getMetricList())) {
return;
}
for (MetricFamily.Metric metric : metricFamily.getMetricList()) {
@@ -733,7 +663,7 @@ public class HttpCollectImpl extends AbstractCollect {
String columnValue = labelMap.get(aliasField);
if (columnValue != null) {
valueRowBuilder.addColumn(columnValue);
} else if (CommonConstants.PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) {
} else if (CommonConstants. PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) {
valueRowBuilder.addColumn(String.valueOf(metric.getValue()));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
@@ -745,9 +675,9 @@ public class HttpCollectImpl extends AbstractCollect {
private void parseResponseByDefault(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
CollectRep.MetricsData. Builder builder, Long responseTime) {
JsonElement element = JsonParser.parseString(resp);
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
if (element.isJsonArray()) {
JsonArray array = element.getAsJsonArray();
for (JsonElement jsonElement : array) {
@@ -779,46 +709,53 @@ public class HttpCollectImpl extends AbstractCollect {
* create httpContext
*
* @param httpProtocol http protocol
* @param requestConfig request config (can be null)
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpProtocol) {
HttpProtocol.Authorization auth = httpProtocol.getAuthorization();
public HttpContext createHttpContext(HttpProtocol httpProtocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
// Set request config if provided
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
HttpProtocol. Authorization auth = httpProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
auth.getDigestAuthPassword(). toCharArray());
AuthScope authScope = new AuthScope(httpProtocol.getHost(), Integer.parseInt(httpProtocol.getPort()));
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
*
* @param httpProtocol http params
* @return http uri request
* @return http request with config
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpProtocol) {
RequestBuilder requestBuilder;
String httpMethod = httpProtocol.getMethod().toUpperCase();
private HttpRequestWithConfig createHttpRequest(HttpProtocol httpProtocol) {
ClassicRequestBuilder requestBuilder;
String httpMethod = httpProtocol.getMethod(). toUpperCase();
if (HttpMethod.GET.matches(httpMethod)) {
requestBuilder = RequestBuilder.get();
requestBuilder = ClassicRequestBuilder.get();
} else if (HttpMethod.POST.matches(httpMethod)) {
requestBuilder = RequestBuilder.post();
requestBuilder = ClassicRequestBuilder.post();
} else if (HttpMethod.PUT.matches(httpMethod)) {
requestBuilder = RequestBuilder.put();
requestBuilder = ClassicRequestBuilder.put();
} else if (HttpMethod.DELETE.matches(httpMethod)) {
requestBuilder = RequestBuilder.delete();
} else if (HttpMethod.PATCH.matches(httpMethod)) {
requestBuilder = RequestBuilder.patch();
requestBuilder = ClassicRequestBuilder.delete();
} else if (HttpMethod. PATCH.matches(httpMethod)) {
requestBuilder = ClassicRequestBuilder.patch();
} else {
// not support the method
log.error("not support the http method: {}.", httpProtocol.getMethod());
@@ -829,17 +766,17 @@ public class HttpCollectImpl extends AbstractCollect {
boolean enableUrlEncoding = Boolean.parseBoolean(httpProtocol.getEnableUrlEncoding());
StringBuilder queryParams = new StringBuilder();
if (params != null && !params.isEmpty()) {
if (params != null && ! params.isEmpty()) {
for (Map.Entry<String, String> param : params.entrySet()) {
String key = param.getKey();
String value = param.getValue();
if (!StringUtils.hasText(key)) {
if (! StringUtils.hasText(key)) {
continue;
}
if (!queryParams.isEmpty()) {
queryParams.append("&");
if (! queryParams.isEmpty()) {
queryParams. append("&");
}
if (enableUrlEncoding) {
@@ -850,9 +787,9 @@ public class HttpCollectImpl extends AbstractCollect {
if (StringUtils.hasText(value)) {
String calculatedValue = TimeExpressionUtil.calculate(value);
if (enableUrlEncoding) {
calculatedValue = UriUtils.encodeQueryParam(calculatedValue, "UTF-8");
calculatedValue = UriUtils. encodeQueryParam(calculatedValue, "UTF-8");
}
queryParams.append("=").append(calculatedValue);
queryParams. append("=").append(calculatedValue);
}
}
}
@@ -863,7 +800,7 @@ public class HttpCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
// headers The custom request header is overwritten here
Map<String, String> headers = httpProtocol.getHeaders();
if (headers != null && !headers.isEmpty()) {
if (headers != null && ! headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(header.getKey(), header.getValue());
@@ -871,7 +808,7 @@ public class HttpCollectImpl extends AbstractCollect {
}
}
// add accept
if (DispatchConstants.PARSE_DEFAULT.equals(httpProtocol.getParseType())
if (DispatchConstants.PARSE_DEFAULT. equals(httpProtocol.getParseType())
|| DispatchConstants.PARSE_JSON_PATH.equals(httpProtocol.getParseType())) {
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
} else if (DispatchConstants.PARSE_XML_PATH.equals(httpProtocol.getParseType())) {
@@ -887,10 +824,10 @@ public class HttpCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization. getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
requestBuilder.addHeader(HttpHeaders. AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
}
}
@@ -905,14 +842,14 @@ public class HttpCollectImpl extends AbstractCollect {
if (enableUrlEncoding) {
// if the url contains parameters directly
if (httpProtocol.getUrl().contains("?")) {
String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?"));
String path = httpProtocol.getUrl(). substring(0, httpProtocol.getUrl().indexOf("?"));
String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1);
uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8");
} else {
uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8");
}
} else {
uri = httpProtocol.getUrl();
uri = httpProtocol. getUrl();
}
// append query params
@@ -922,10 +859,10 @@ public class HttpCollectImpl extends AbstractCollect {
String finalUri;
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
finalUri = httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri;
finalUri = httpProtocol.getHost() + ":" + httpProtocol. getPort() + uri;
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
String ipAddressType = IpDomainUtil. checkIpAddressType(httpProtocol.getHost());
String baseUri = NetworkConstants.IPV6. equals(ipAddressType)
? String.format("[%s]:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri)
: String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri);
boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl());
@@ -943,21 +880,23 @@ public class HttpCollectImpl extends AbstractCollect {
throw e;
}
// custom timeout
// custom timeout - create RequestConfig separately
RequestConfig requestConfig = null;
int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
// build the request and return with config
return new HttpRequestWithConfig(requestBuilder. build(), requestConfig);
}
private boolean checkSuccessInvoke(Metrics metrics, int statusCode) {
List<String> successCodes = metrics.getHttp().getSuccessCodes();
List<String> successCodes = metrics.getHttp(). getSuccessCodes();
Set<Integer> successCodeSet = successCodes != null ? successCodes.stream().map(code -> {
try {
return Integer.valueOf(code);
@@ -970,4 +909,4 @@ public class HttpCollectImpl extends AbstractCollect {
}
return successCodeSet.contains(statusCode);
}
}
}
@@ -117,11 +117,11 @@ public class SslCertificateCollectImpl extends AbstractCollect {
} else if (NAME_EXPIRED.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Boolean.toString(expired));
} else if (NAME_START_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(x509Certificate.getNotBefore().toLocaleString());
valueRowBuilder.addColumn(x509Certificate.getNotBefore().toString());
} else if (NAME_START_TIMESTAMP.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotBefore().getTime()));
} else if (NAME_END_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(x509Certificate.getNotAfter().toLocaleString());
valueRowBuilder.addColumn(x509Certificate.getNotAfter().toString());
} else if (NAME_END_TIMESTAMP.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotAfter().getTime()));
} else {
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -44,16 +44,17 @@ import org.apache.hertzbeat.common.entity.job.protocol.NginxProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.MediaType;
/**
@@ -62,6 +63,27 @@ import org.springframework.http.MediaType;
@Slf4j
public class NginxCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private static final String NGINX_STATUS_NAME = "nginx_status";
private static final String REQ_STATUS_NAME = "req_status";
private static final String AVAILABLE = "available";
@@ -93,34 +115,36 @@ public class NginxCollectImpl extends AbstractCollect {
nginxProtocol.setUrl(url == null ? RIGHT_DASH : RIGHT_DASH + url.trim());
}
HttpContext httpContext = createHttpContext(metrics.getNginx());
HttpUriRequest request = createHttpRequest(metrics.getNginx());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)){
// send an HTTP request and get the response data
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getNginx());
HttpContext httpContext = createHttpContext(metrics.getNginx(), requestWithConfig.getRequestConfig());
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + statusCode);
return;
return null;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
Long responseTime = System.currentTimeMillis() - startTime;
// call different parsing methods based on the metrics name
if (StringUtils.equalsAny(metrics.getName(), NGINX_STATUS_NAME, AVAILABLE)) {
if (NGINX_STATUS_NAME.equals(metrics.getName()) || AVAILABLE.equals(metrics.getName())) {
parseNginxStatusResponse(builder, resp, metrics, responseTime);
} else if (REQ_STATUS_NAME.equals(metrics.getName())) {
parseReqStatusResponse(builder, resp, metrics, responseTime);
}
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
}
@@ -130,18 +154,21 @@ public class NginxCollectImpl extends AbstractCollect {
return DispatchConstants.PROTOCOL_NGINX;
}
private HttpContext createHttpContext(NginxProtocol nginxProtocol) {
private HttpContext createHttpContext(NginxProtocol nginxProtocol, RequestConfig requestConfig) {
HttpHost host = new HttpHost(nginxProtocol.getHost(), Integer.parseInt(nginxProtocol.getPort()));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
HttpClientContext httpClientContext = HttpClientContext.create();
if (requestConfig != null) {
httpClientContext.setRequestConfig(requestConfig);
}
return httpClientContext;
}
private HttpUriRequest createHttpRequest(NginxProtocol nginxProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(NginxProtocol nginxProtocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
String portWithUri = nginxProtocol.getPort() + CollectUtil.replaceUriSpecialChar(nginxProtocol.getUrl());
String host = nginxProtocol.getHost();
if (IpDomainUtil.isHasSchema(host)) {
requestBuilder.setUri(host + ":" + portWithUri);
} else {
@@ -162,16 +189,16 @@ public class NginxCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE);
RequestConfig requestConfig = null;
int timeout = Integer.parseInt(nginxProtocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -335,4 +362,4 @@ public class NginxCollectImpl extends AbstractCollect {
return GET + result;
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -47,24 +47,24 @@ import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.client5.http.impl.auth.DigestScheme;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
@@ -74,11 +74,32 @@ import org.springframework.util.StringUtils;
*/
@Slf4j
public class PrometheusAutoCollectImpl implements PrometheusCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder,
Metrics metrics) {
@@ -89,11 +110,13 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
builder.setMsg(e.getMessage());
return null;
}
HttpContext httpContext = createHttpContext(metrics.getPrometheus());
HttpUriRequest request = createHttpRequest(metrics.getPrometheus());
try (CloseableHttpResponse response =
CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getPrometheus());
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(metrics.getPrometheus(), requestWithConfig.getRequestConfig());
HttpClientResponseHandler<List<CollectRep.MetricsData>> responseHandler = response -> {
int statusCode = response.getCode();
boolean isSuccessInvoke = defaultSuccessStatusCodes.contains(statusCode);
log.debug("http response status: {}", statusCode);
if (!isSuccessInvoke) {
@@ -102,12 +125,18 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
return null;
}
try {
// Parse directly from the entity stream inside the handler loop while connection is open
return parseResponseByPrometheusExporter(response.getEntity().getContent(), builder);
} catch (Exception e) {
log.info("parse error: {}.", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("parse response data error:" + e.getMessage());
return null;
}
};
try {
return CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error(errorMsg);
@@ -133,31 +162,27 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
log.error(errorMsg, e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
return Collections.singletonList(builder.build());
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_PROMETHEUS;
}
private void validateParams(Metrics metrics) throws Exception {
if (metrics == null || metrics.getPrometheus() == null) {
throw new Exception("Prometheus collect must has prometheus params");
}
PrometheusProtocol protocol = metrics.getPrometheus();
if (protocol.getPath() == null
|| !StringUtils.hasText(protocol.getPath())
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
|| !StringUtils.hasText(protocol.getPath())
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
}
}
private List<CollectRep.MetricsData> parseResponseByPrometheusExporter(InputStream inputStream, CollectRep.MetricsData.Builder builder) throws IOException {
long endTime = System.currentTimeMillis();
builder.setTime(endTime);
@@ -201,40 +226,49 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
}
return metricsDataList;
}
/**
* create httpContext
*
* @param protocol prometheus protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(PrometheusProtocol protocol) {
public HttpContext createHttpContext(PrometheusProtocol protocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
provider.setCredentials(AuthScope.ANY, credentials);
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword().toCharArray());
// Fix: Use specific AuthScope instead of AuthScope.ANY
AuthScope authScope = new AuthScope(protocol.getHost(), Integer.parseInt(protocol.getPort()));
provider.setCredentials(authScope, credentials);
AuthCache authCache = new BasicAuthCache();
authCache.put(new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort())), new DigestScheme());
clientContext.setCredentialsProvider(provider);
clientContext.setAuthCache(authCache);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
* @param protocol http params
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PrometheusProtocol protocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// params
Map<String, String> params = protocol.getParams();
if (params != null && !params.isEmpty()) {
@@ -260,7 +294,7 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
}
// add accept
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE);
if (protocol.getAuthorization() != null) {
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
@@ -268,7 +302,7 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
@@ -280,17 +314,17 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
if (StringUtils.hasLength(protocol.getPayload()) && (HttpMethod.POST.matches(protocol.getMethod()) || HttpMethod.PUT.matches(protocol.getMethod()))) {
requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8));
}
// uri
String uri = CollectUtil.replaceUriSpecialChar(protocol.getPath());
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uri);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uri)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uri);
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uri)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uri);
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
@@ -298,18 +332,19 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
}
}
RequestConfig requestConfig = null;
// custom timeout
int timeout = CollectUtil.getTimeout(protocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -319,11 +354,11 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
public static PrometheusAutoCollectImpl getInstance() {
return PrometheusAutoCollectImpl.SingleInstance.INSTANCE;
}
/**
* static instance
*/
private static class SingleInstance {
private static final PrometheusAutoCollectImpl INSTANCE = new PrometheusAutoCollectImpl();
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -30,25 +30,25 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.client5.http.impl.auth.DigestScheme;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
@@ -71,6 +71,27 @@ import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
@Slf4j
public class PrometheusProxyCollectImpl implements PrometheusCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
@@ -80,7 +101,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
PrometheusProtocol prometheusProtocol = metrics.getPrometheus();
HttpUriRequest request;
try {
validateParams(metrics);
} catch (Exception e) {
@@ -89,17 +110,19 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
return Collections.singletonList(builder.build());
}
HttpContext httpContext = createHttpContext(prometheusProtocol);
request = createHttpRequest(prometheusProtocol);
HttpRequestWithConfig requestWithConfig = createHttpRequest(prometheusProtocol);
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(prometheusProtocol, requestWithConfig.getRequestConfig());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
// Use HttpClientResponseHandler to avoid deprecated execute method and manual resource closing
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
log.debug("Prometheus proxy collect, response status: {}", statusCode);
if (!defaultSuccessStatusCodes.contains(statusCode)) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return Collections.singletonList(builder.build());
return null;
}
String rawTextContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
@@ -118,6 +141,11 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
builder.addValueRow(valueRowBuilder.build());
builder.setCode(CollectRep.Code.SUCCESS);
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error("Prometheus proxy collect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e1);
@@ -143,10 +171,6 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
log.error("Prometheus proxy collect unknown error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
return Collections.singletonList(builder.build());
}
@@ -162,7 +186,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
}
PrometheusProtocol protocol = metrics.getPrometheus();
if (!StringUtils.hasText(protocol.getHost())
|| !StringUtils.hasText(protocol.getPort())) {
|| !StringUtils.hasText(protocol.getPort())) {
throw new Exception("Prometheus collect must has host and port params");
}
if (protocol.getPath() == null
@@ -171,42 +195,51 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
}
}
/**
* create httpContext
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol prometheus protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(PrometheusProtocol protocol) {
public HttpContext createHttpContext(PrometheusProtocol protocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
provider.setCredentials(AuthScope.ANY, credentials);
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword().toCharArray());
// Fix: Use specific AuthScope instead of AuthScope.ANY
AuthScope authScope = new AuthScope(protocol.getHost(), Integer.parseInt(protocol.getPort()));
provider.setCredentials(authScope, credentials);
AuthCache authCache = new BasicAuthCache();
HttpHost targetHost = new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort()));
authCache.put(targetHost, new DigestScheme());
clientContext.setCredentialsProvider(provider);
clientContext.setAuthCache(authCache);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol http params
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PrometheusProtocol protocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// params
Map<String, String> params = protocol.getParams();
if (params != null && !params.isEmpty()) {
@@ -231,7 +264,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) {
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE + ";version=0.0.4,*/*;q=0.1");
}
if (protocol.getAuthorization() != null) {
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
@@ -241,7 +274,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
}
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
@@ -255,15 +288,15 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
requestBuilder.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
}
}
String uriPath = CollectUtil.replaceUriSpecialChar(protocol.getPath());
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uriPath);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
@@ -271,24 +304,22 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
}
}
RequestConfig requestConfig;
// custom timeout
int timeout = CollectUtil.getTimeout(protocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
} else {
RequestConfig requestConfig = RequestConfig.custom()
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setRedirectsEnabled(true)
.build();
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -305,4 +336,4 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
private static class SingleInstance {
private static final PrometheusProxyCollectImpl INSTANCE = new PrometheusProxyCollectImpl();
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -24,6 +24,15 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
@@ -38,15 +47,6 @@ import org.apache.hertzbeat.common.entity.push.PushMetricsDto;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
/**
@@ -55,6 +55,27 @@ import org.springframework.http.MediaType;
@Slf4j
public class PushCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private static final Map<Long, Long> timeMap = new ConcurrentHashMap<>();
// ms
@@ -83,20 +104,25 @@ public class PushCollectImpl extends AbstractCollect {
Long time = timeMap.getOrDefault(monitorId, curTime - FIRST_COLLECT_INTERVAL);
timeMap.put(monitorId, curTime);
HttpContext httpContext = createHttpContext(pushProtocol);
HttpUriRequest request = createHttpRequest(pushProtocol, monitorId, time);
HttpRequestWithConfig requestWithConfig = createHttpRequest(pushProtocol, monitorId, time);
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(pushProtocol, requestWithConfig.getRequestConfig());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
return null;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
parseResponse(builder, resp, metrics);
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
String resp = CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
if (resp != null) {
parseResponse(builder, resp, metrics);
}
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error(errorMsg, e);
@@ -111,16 +137,17 @@ public class PushCollectImpl extends AbstractCollect {
return DispatchConstants.PROTOCOL_PUSH;
}
private HttpContext createHttpContext(PushProtocol pushProtocol) {
HttpHost host = new HttpHost(pushProtocol.getHost(), Integer.parseInt(pushProtocol.getPort()));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
private HttpContext createHttpContext(PushProtocol pushProtocol, RequestConfig requestConfig) {
HttpClientContext httpClientContext = HttpClientContext.create();
if (requestConfig != null) {
httpClientContext.setRequestConfig(requestConfig);
}
return httpClientContext;
}
private HttpUriRequest createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// uri
String uri = CollectUtil.replaceUriSpecialChar(pushProtocol.getUri());
@@ -142,19 +169,17 @@ public class PushCollectImpl extends AbstractCollect {
requestBuilder.addParameter("time", String.valueOf(startTime));
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
//requestBuilder.setUri(pushProtocol.getUri());
RequestConfig requestConfig = null;
if (DEFAULT_TIMEOUT > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(DEFAULT_TIMEOUT)
.setSocketTimeout(DEFAULT_TIMEOUT)
// Using deprecated setConnectTimeout for request-level override
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(DEFAULT_TIMEOUT))
.setResponseTimeout(Timeout.ofMilliseconds(DEFAULT_TIMEOUT))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
private void parseResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metric) {
@@ -180,4 +205,4 @@ public class PushCollectImpl extends AbstractCollect {
}
builder.setTime(System.currentTimeMillis());
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,20 +18,20 @@
package org.apache.hertzbeat.collector.collect.redfish;
import java.nio.charset.StandardCharsets;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.springframework.http.MediaType;
/**
@@ -58,11 +58,21 @@ public class RedfishClient {
redfishProtocol.getUsername(), redfishProtocol.getPassword(), Integer.parseInt(redfishProtocol.getTimeout()));
}
@SuppressWarnings("deprecation")
public ConnectSession connect() throws Exception {
HttpHost host = new HttpHost(this.host, this.port);
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
RequestBuilder requestBuilder = RequestBuilder.post();
HttpClientContext httpClientContext = HttpClientContext.create();
// Configure RequestConfig if timeout is set
if (this.timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(this.timeout))
.setResponseTimeout(Timeout.ofMilliseconds(this.timeout))
.setRedirectsEnabled(true)
.build();
httpClientContext.setRequestConfig(requestConfig);
}
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.post();
String uri = REDFISH_SESSION_SERVICE;
if (IpDomainUtil.isHasSchema(this.host)) {
@@ -79,37 +89,31 @@ public class RedfishClient {
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
requestBuilder.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
requestBuilder.addHeader(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8 + "");
// Content-Encoding header is usually for compression, setting charset is done in Content-Type usually,
// but keeping original logic of setting it explicitly if that was the intent, though standard is usually empty or gzip.
// original: requestBuilder.addHeader(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8 + "");
final String json = "{\"UserName\": \"" + this.username + "\", \"Password\": \"" + this.password + "\"}";
StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
requestBuilder.setEntity(entity);
if (this.timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(this.timeout)
.setSocketTimeout(this.timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
ClassicHttpRequest request = requestBuilder.build();
HttpUriRequest request = requestBuilder.build();
Session session;
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpClientContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Session> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_CREATED) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
String location = response.getFirstHeader(NetworkConstants.LOCATION).getValue();
String auth = response.getFirstHeader(NetworkConstants.X_AUTH_TOKEN).getValue();
session = new Session(auth, location, this.host, this.port);
return new Session(auth, location, this.host, this.port);
};
try {
Session session = CommonHttpClient.getHttpClient().execute(request, httpClientContext, responseHandler);
return new RedfishConnectSession(session);
} catch (Exception e) {
throw new Exception("Redfish session create error: " + e.getMessage());
} finally {
request.abort();
throw new Exception("Redfish session create error: " + e.getMessage(), e);
}
return new RedfishConnectSession(session);
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,15 +18,15 @@
package org.apache.hertzbeat.collector.collect.redfish;
import java.nio.charset.StandardCharsets;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
/**
* Redfish connect session
@@ -54,15 +54,20 @@ public class RedfishConnectSession implements ConnectSession {
HttpDelete httpDelete = new HttpDelete(url);
httpDelete.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token());
httpDelete.setHeader(NetworkConstants.LOCATION, session.location());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpDelete)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
EntityUtils.consume(response.getEntity());
return null;
};
try {
CommonHttpClient.getHttpClient().execute(httpDelete, responseHandler);
} catch (Exception e) {
throw new Exception("Redfish session close error:" + e.getMessage());
} finally {
httpDelete.abort();
throw new Exception("Redfish session close error:" + e.getMessage(), e);
}
}
@@ -84,16 +89,19 @@ public class RedfishConnectSession implements ConnectSession {
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token());
httpGet.setHeader(NetworkConstants.LOCATION, session.location());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
return CommonHttpClient.getHttpClient().execute(httpGet, responseHandler);
} catch (Exception e) {
throw new Exception("Redfish session get resource error:" + e.getMessage());
} finally {
httpGet.abort();
throw new Exception("Redfish session get resource error:" + e.getMessage(), e);
}
}
}
}
@@ -67,8 +67,6 @@ public class RedisCommonCollectImpl extends AbstractCollect {
private static final String CLUSTER = "3";
private static final String SINGLE = "1";
private static final String CLUSTER_INFO = "cluster";
private static final String UNIQUE_IDENTITY = "identity";
@@ -142,7 +140,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @return data
*/
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
Map<String, StatefulRedisConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
Map<String, StatefulRedisClusterConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
List<Map<String, String>> list = new ArrayList<>(connectionMap.size());
connectionMap.forEach((identity, connection) ->{
String info = connection.sync().info(metrics.getName());
@@ -216,23 +214,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection map
*/
private Map<String, StatefulRedisConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
// first connection
StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol);
Partitions partitions = connection.getPartitions();
Map<String, StatefulRedisConnection<String, String>> clusterConnectionMap = new HashMap<>(partitions.size());
Map<String, StatefulRedisClusterConnection<String, String>> clusterConnectionMap = new HashMap<>(partitions.size());
for (RedisClusterNode partition : partitions) {
RedisURI uri = partition.getUri();
RedisProtocol singleRedisProtocol = RedisProtocol.builder()
.host(uri.getHost())
.port(String.valueOf(uri.getPort()))
.username(redisProtocol.getUsername())
.password(redisProtocol.getPassword())
.pattern(SINGLE)
.timeout(redisProtocol.getTimeout())
.sshTunnel(redisProtocol.getSshTunnel())
.build();
StatefulRedisConnection<String, String> clusterConnection = getSingleConnection(singleRedisProtocol);
redisProtocol.setHost(uri.getHost());
redisProtocol.setPort(String.valueOf(uri.getPort()));
StatefulRedisClusterConnection<String, String> clusterConnection = getClusterConnection(redisProtocol);
clusterConnectionMap.put(doUri(uri.getHost(), uri.getPort()), clusterConnection);
}
return clusterConnectionMap;
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -22,6 +22,10 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
@@ -32,10 +36,6 @@ import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.hertzbeat.common.entity.sd.EurekaDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.XmlUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;
/**
@@ -57,38 +57,55 @@ public class EurekaSdCollectImpl extends AbstractCollect {
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = RequestBuilder.get().setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH).build();
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
ClassicHttpRequest request = ClassicRequestBuilder.get()
.setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH)
.build();
// Use ResponseHandler to avoid manual resource management and deprecated execute methods
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != 200) {
log.warn("Failed to fetch eureka sd...");
builder.setMsg("StatusCode " + statusCode);
builder.setCode(CollectRep.Code.FAIL);
return;
return null;
}
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
EurekaDiscoveryResponseEntity responseEntity =
XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class);
if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) {
return;
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
String responseBody = CommonHttpClient.getHttpClient().execute(request, responseHandler);
if (responseBody != null) {
EurekaDiscoveryResponseEntity responseEntity =
XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class);
if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) {
return;
}
responseEntity.getApplications()
.stream()
.filter(application -> !CollectionUtils.isEmpty(application.getInstances()))
.forEach(application -> convertTarget(configList, application));
configList.forEach(config -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(config.getHost());
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
}
responseEntity.getApplications()
.stream()
.filter(application -> !CollectionUtils.isEmpty(application.getInstances()))
.forEach(application -> convertTarget(configList, application));
configList.forEach(config -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(config.getHost());
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
} catch (IOException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch eureka sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch eureka sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@@ -108,4 +125,4 @@ public class EurekaSdCollectImpl extends AbstractCollect {
public String supportProtocol() {
return DispatchConstants.PROTOCOL_EUREKA_SD;
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -41,18 +41,18 @@ import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriUtils;
@@ -63,6 +63,28 @@ import org.springframework.web.util.UriUtils;
*/
@Slf4j
public class HttpSdCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
}
@@ -71,22 +93,30 @@ public class HttpSdCollectImpl extends AbstractCollect {
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = createHttpRequest(metrics.getHttp_sd());
HttpContext httpContext = createHttpContext(metrics.getHttp_sd());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getHttp_sd());
if (requestWithConfig == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to create HTTP request");
return;
}
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(metrics.getHttp_sd(), requestWithConfig.getRequestConfig());
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != 200) {
log.warn("Failed to fetch sd...");
builder.setMsg("StatusCode " + statusCode);
builder.setCode(CollectRep.Code.FAIL);
return;
return null;
}
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
TypeReference<List<ServiceDiscoveryResponseEntity>> typeReference = new TypeReference<>() {};
final List<ServiceDiscoveryResponseEntity> responseEntityList = JsonUtil.fromJson(responseBody, typeReference);
if (CollectionUtils.isEmpty(responseEntityList)) {
return;
return null;
}
responseEntityList.stream()
@@ -100,11 +130,21 @@ public class HttpSdCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (IOException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@@ -137,17 +177,23 @@ public class HttpSdCollectImpl extends AbstractCollect {
* create httpContext
*
* @param httpSdProtocol http sd protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpSdProtocol) {
public HttpContext createHttpContext(HttpProtocol httpSdProtocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
HttpProtocol.Authorization auth = httpSdProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (org.springframework.util.StringUtils.hasText(auth.getDigestAuthUsername())
&& org.springframework.util.StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
auth.getDigestAuthPassword().toCharArray());
URL url;
try {
url = new URL(httpSdProtocol.getUrl());
@@ -159,20 +205,20 @@ public class HttpSdCollectImpl extends AbstractCollect {
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
*
* @param httpSdProtocol http request set
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpSdProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(HttpProtocol httpSdProtocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// The default request header can be overridden if customized
// keep-alive
@@ -227,16 +273,17 @@ public class HttpSdCollectImpl extends AbstractCollect {
throw e;
}
RequestConfig requestConfig = null;
// custom timeout
int timeout = CollectUtil.getTimeout(httpSdProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
}
}
@@ -19,13 +19,10 @@ package org.apache.hertzbeat.collector.collect.redis;
import static org.apache.hertzbeat.common.constants.CommonConstants.TYPE_STRING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.resource.ClientResources;
@@ -40,7 +37,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -55,19 +51,13 @@ public class RedisClusterCollectImplTest {
@Mock
private StatefulRedisClusterConnection<String, String> clusterConnection;
private StatefulRedisClusterConnection<String, String> connection;
@Mock
private StatefulRedisConnection<String, String> singleConnection;
private RedisAdvancedClusterCommands<String, String> cmd;
@Mock
private RedisCommands<String, String> cmd;
@Mock
private RedisClusterClient clusterClient;
@Mock
private RedisClient singleClient;
private RedisClusterClient client;
@BeforeEach
void setUp() {
@@ -75,10 +65,8 @@ public class RedisClusterCollectImplTest {
@AfterEach
void setDown() {
clusterConnection.close();
singleConnection.close();
clusterClient.shutdown();
singleClient.shutdown();
connection.close();
client.shutdown();
}
@Test
@@ -122,14 +110,10 @@ public class RedisClusterCollectImplTest {
metrics.setAliasFields(aliasField);
metrics.setFields(fields);
MockedStatic<RedisClusterClient> redisClusterClientMockedStatic = Mockito.mockStatic(RedisClusterClient.class);
redisClusterClientMockedStatic.when(() -> RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(clusterClient);
Mockito.when(clusterClient.connect()).thenReturn(clusterConnection);
MockedStatic<RedisClient> redisClientMockedStatic = Mockito.mockStatic(RedisClient.class);
redisClientMockedStatic.when(() -> RedisClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(singleClient);
Mockito.when(singleClient.connect()).thenReturn(singleConnection);
Mockito.mockStatic(RedisClusterClient.class).when(() -> RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(client);
Mockito.when(client.connect()).thenReturn(connection);
Partitions partitions = new Partitions();
RedisClusterNode node = new RedisClusterNode();
@@ -141,9 +125,9 @@ public class RedisClusterCollectImplTest {
node2.setUri(RedisURI.create("redis://" + uri2));
partitions.add(node2);
Mockito.when(clusterConnection.getPartitions()).thenReturn(partitions);
Mockito.when(connection.getPartitions()).thenReturn(partitions);
Mockito.when(singleConnection.sync()).thenReturn(cmd);
Mockito.when(connection.sync()).thenReturn(cmd);
Mockito.when(cmd.info(metrics.getName())).thenReturn(info);
Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);
@@ -163,8 +147,6 @@ public class RedisClusterCollectImplTest {
assertEquals(row.getColumns(2), uri2);
}
}
redisClusterClientMockedStatic.close();
redisClientMockedStatic.close();
}
}
@@ -29,7 +29,7 @@ import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.remoting.RemotingClient;
import org.junit.jupiter.api.BeforeEach;
@@ -113,7 +113,7 @@ class CollectServerTest {
RemotingClient remotingClient = mock(RemotingClient.class);
ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient);
ClusterMsg.Message message = mock(ClusterMsg.Message.class);
ClusterMessage message = mock(ClusterMessage.class);
collectServer.sendMsg(message);
@@ -135,7 +135,7 @@ class CollectServerTest {
collectNettyEventListener.onChannelActive(channel);
verify(timerDispatch, times(1)).goOnline();
verify(remotingClient, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(remotingClient, times(1)).sendMsg(any(ClusterMessage.class));
ScheduledExecutorService scheduledExecutor =
(ScheduledExecutorService) ReflectionTestUtils.getField(collectServer, "scheduledExecutor");
@@ -34,7 +34,7 @@ import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -116,7 +116,7 @@ class CollectJobServiceTest {
collectJobService.collectSyncOneTimeJobData(job);
verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(collectServer, times(1)).sendMsg(any(ClusterMessage.class));
}
@Test
@@ -137,7 +137,7 @@ class CollectJobServiceTest {
.build();
collectJobService.sendAsyncCollectData(metricsData);
verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(collectServer, times(1)).sendMsg(any(ClusterMessage.class));
}
@Test
@@ -100,6 +100,35 @@
</build>
<profiles>
<profile>
<id>dev-driver</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.nls</groupId>
<artifactId>orai18n</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.ibm.db2</groupId>
<artifactId>jcc</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>inner</id>
<build>
@@ -28,8 +28,6 @@ spring:
jackson:
default-property-inclusion: ALWAYS
# need to disable spring boot mongodb auto config, or default mongodb connection tried and failed...
autoconfigure:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
management:
endpoints:
web:
@@ -1,27 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<configuration scan="true">
<springProperty scope="context" name="application_name" source="spring.application.name" defaultValue="collector"/>
<!-- Output logs to ConsoleAppender -->
<appender name="CONSOLE_RAW" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--<pattern>%d %p (%file:%line\)- %m%n</pattern>-->
<!-- Format output: %d for date, %thread for thread name, %-5level for level with 5 character width, %msg for log message, %n for new line -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
@@ -35,23 +17,14 @@
</appender>
<appender name="SystemOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Rolling policy for loggers, by date and by size -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Archive log file path. %d{yyyy-MM-dd} specifies date format, %i specifies index -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
<!-- Log retention duration -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<!-- Maximum size of log retention -->
<totalSizeCap>5GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<!-- Besides logging by day, log files cannot exceed 200M, if exceeded, log files will start from index 0 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- Append logs -->
<append>true</append>
<!-- Log file format -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
@@ -59,32 +32,23 @@
</appender>
<appender name="ErrOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}-error.%i.log.zip</fileNamePattern>
<!-- Log retention duration -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<!-- Maximum size of log retention -->
<totalSizeCap>5GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- Append logs -->
<append>true</append>
<!-- Log file format -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- This log file records error and above levels -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- Settings for this logger: for example, all output logs under the org.springframework package must be at level info or above to be output! -->
<!-- This can avoid outputting many common debug information of the spring framework! -->
<logger name="org.springframework" level="info" />
<logger name="org.json" level="error"/>
<logger name="io.netty" level="info"/>
@@ -98,7 +62,6 @@
<logger name="org.mongodb" level="warn"/>
<logger name="io.greptime" level="warn"/>
<!-- Production environment configuration -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
@@ -115,7 +78,6 @@
</root>
</springProfile>
<!-- Development environment configuration -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
@@ -124,7 +86,6 @@
</root>
</springProfile>
<!-- Development environment configuration -->
<springProfile name="mysql">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
@@ -133,4 +94,4 @@
</root>
</springProfile>
</configuration>
</configuration>
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,33 +17,30 @@
package org.apache.hertzbeat.collector.collect.common.http;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
/**
* common http client
* common http client for HttpComponents Client 5.x
*/
@Slf4j
public class CommonHttpClient {
@@ -84,11 +81,13 @@ public class CommonHttpClient {
/**
* ssl supported version
* Note: SSLv3 is often disabled in modern JDKs
*/
private static final String[] SUPPORTED_SSL = {"TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3"};
static {
try {
// 1. SSL Context Configuration
SSLContext sslContext = SSLContexts.createDefault();
X509TrustManager x509TrustManager = new X509TrustManager() {
@Override
@@ -102,7 +101,7 @@ public class CommonHttpClient {
for (X509Certificate certificate : x509Certificates) {
Date deadline = certificate.getNotAfter();
if (deadline != null && now.after(deadline)) {
throw new CertificateExpiredException();
throw new CertificateExpiredException("Server certificate expired at " + deadline);
}
}
}
@@ -112,57 +111,63 @@ public class CommonHttpClient {
public X509Certificate[] getAcceptedIssuers() { return null; }
};
sslContext.init(null, new TrustManager[]{x509TrustManager}, null);
SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, SUPPORTED_SSL, null, new NoopHostnameVerifier());
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", sslFactory)
// 2. TlsStrategy Configuration (Replaces deprecated SSLConnectionSocketFactory/Registry)
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext,
SUPPORTED_SSL,
null, // Supported cipher suites (null = default)
SSLBufferMode.STATIC,
NoopHostnameVerifier.INSTANCE
);
// 3. Connection Manager Configuration (Using Builder)
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT))
.setSocketTimeout(Timeout.ofMilliseconds(SOCKET_TIMEOUT))
.setValidateAfterInactivity(TimeValue.ofMilliseconds(INACTIVITY_VALIDATED_TIME))
.build();
connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setMaxConnTotal(MAX_TOTAL_CONNECTIONS)
.setMaxConnPerRoute(MAX_PER_ROUTE_CONNECTIONS)
.setDefaultConnectionConfig(connectionConfig)
.build();
// 4. Request Config (Connection Request Timeout & Redirects)
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(REQUIRE_CONNECT_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
// auto redirect when 301 302 response status
.setConnectionRequestTimeout(Timeout.ofMilliseconds(REQUIRE_CONNECT_TIMEOUT))
.setRedirectsEnabled(true)
.build();
// connection pool
connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE_CONNECTIONS);
connectionManager.setValidateAfterInactivity(INACTIVITY_VALIDATED_TIME);
// 5. Build HttpClient
httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
// clean up unavailable expired connections
.evictExpiredConnections()
// clean up available but idle connections
.evictIdleConnections(100, TimeUnit.SECONDS)
.evictIdleConnections(TimeValue.ofSeconds(100))
.build();
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("http-connection-pool-cleaner-%d")
.setDaemon(true)
.build();
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1, threadFactory);
scheduledExecutor.scheduleWithFixedDelay(() -> {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(40, TimeUnit.SECONDS);
}, 40L, 40L, TimeUnit.SECONDS);
// shutdown hook
// Shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(CommonHttpClient::close));
} catch (Exception ignored) {}
} catch (Exception e) {
log.error("Initialize CommonHttpClient error", e);
}
}
public static CloseableHttpClient getHttpClient() {
return httpClient;
}
public static void close() {
try {
httpClient.close();
if (httpClient != null) {
httpClient.close();
}
} catch (Exception e) {
log.error("close http client error", e);
}
}
}
}
@@ -49,28 +49,4 @@ public interface CollectorConstants extends NetworkConstants {
String STATUS_CODE = "statusCode";
/**
* Maximum XML response size in bytes (10MB) to prevent DoS attacks
*/
int MAX_XML_RESPONSE_SIZE = 10 * 1024 * 1024;
/**
* Maximum number of nodes returned by Xpath query to prevent excessive resource consumption
*/
int MAX_XPATH_RESULT_NODES = 1000;
/**
* Dangerous Xpath expression patterns that could cause DoS attacks
* These patterns match expressions that traverse the entire XML document
*/
String[] DANGEROUS_XPATH_PATTERNS = {
"//\\*\\s*\\|\\s*//@\\*\\s*\\|\\s*//text\\(\\)", // //* | //@* | //text()
"//\\*\\s*\\|", // //* | ...
"//@\\*\\s*\\|", // //@* | ...
"//node\\(\\)\\s*\\|", // //node() | ...
"descendant-or-self::node\\(\\)\\s*\\|", // descendant-or-self::node() | ...
"/descendant-or-self::node\\(\\)", // /descendant-or-self::node()
"//\\*[\\s\\S]*//\\*" // //** with multiple wildcards
};
}
@@ -17,65 +17,48 @@
package org.apache.hertzbeat.collector.dispatch;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Collection task worker thread pool
* Collection task worker thread pool with Virtual Threads
*/
@Component
@Slf4j
public class WorkerPool implements DisposableBean {
private ThreadPoolExecutor workerExecutor;
private ExecutorService workerExecutor;
public WorkerPool() {
initWorkExecutor();
}
private void initWorkExecutor() {
// thread factory
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("[Important] WorkerPool workerExecutor has uncaughtException.", throwable);
log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("collect-worker-%d")
.build();
int coreSize = Math.max(2, Runtime.getRuntime().availableProcessors());
int maxSize = Runtime.getRuntime().availableProcessors() * 16;
workerExecutor = new ThreadPoolExecutor(coreSize,
maxSize,
10,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
.name("collect-vt-", 0)
.factory();
workerExecutor = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
log.info("WorkerPool initialized with JDK 25 Virtual Threads successfully.");
}
/**
* Run the collection task thread
*
* @param runnable Task
* @throws RejectedExecutionException when thread pool full
* Run the collection task
* @param runnable Task
*/
public void executeJob(Runnable runnable) throws RejectedExecutionException {
public void executeJob(Runnable runnable) {
workerExecutor.execute(runnable);
}
@Override
public void destroy() throws Exception {
public void destroy() {
if (workerExecutor != null) {
workerExecutor.shutdownNow();
workerExecutor.close();
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,7 +18,6 @@
package org.apache.hertzbeat.collector.dispatch.entrance;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.protobuf.ByteString;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
@@ -33,7 +32,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProces
import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.RemotingClient;
@@ -65,7 +64,7 @@ public class CollectServer implements CommandLineRunner {
private final TimerDispatch timerDispatch;
private final CollectorInfoProperties infoProperties;
private RemotingClient remotingClient;
private ScheduledExecutorService scheduledExecutor;
@@ -97,13 +96,13 @@ public class CollectServer implements CommandLineRunner {
nettyClientConfig.setServerPort(nettyProperties.getManagerPort());
this.remotingClient = new NettyRemotingClient(nettyClientConfig, new CollectNettyEventListener(), threadPool);
this.remotingClient.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new GoOfflineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new GoOnlineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_CLOSE, new GoCloseProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.HEARTBEAT, new HeartbeatProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_OFFLINE, new GoOfflineProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_ONLINE, new GoOnlineProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_CLOSE, new GoCloseProcessor(this));
}
public void shutdown() {
@@ -116,7 +115,7 @@ public class CollectServer implements CommandLineRunner {
return collectJobService;
}
public void sendMsg(final ClusterMsg.Message message) {
public void sendMsg(final ClusterMessage message) {
this.remotingClient.sendMsg(message);
}
@@ -143,10 +142,10 @@ public class CollectServer implements CommandLineRunner {
.build();
timerDispatch.goOnline();
// send online message
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(collectorInfo)))
ClusterMessage message = ClusterMessage.builder()
.identity(identity)
.type(ClusterMessage.MessageType.GO_ONLINE)
.msg(JsonUtil.toJsonBytes(collectorInfo))
.build();
CollectServer.this.sendMsg(message);
@@ -163,13 +162,13 @@ public class CollectServer implements CommandLineRunner {
// schedule send heartbeat message
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.HEARTBEAT)
ClusterMessage heartbeat = ClusterMessage.builder()
.identity(identity)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.HEARTBEAT)
.build();
CollectServer.this.sendMsg(heartbeat);
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
} catch (Exception e) {
log.error("schedule send heartbeat to server error.{}", e.getMessage());
}
@@ -182,4 +181,4 @@ public class CollectServer implements CommandLineRunner {
log.info("handle idle event triggered. collector is going offline.");
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.collector.dispatch.entrance.internal;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
@@ -25,7 +24,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.ArrowUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
@@ -117,10 +116,10 @@ public class CollectJobService {
workerPool.executeJob(() -> {
List<CollectRep.MetricsData> metricsDataList = this.collectSyncJobData(oneTimeJob);
byte[] msg = ArrowUtil.serializeMetricsData(metricsDataList);
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA)
ClusterMessage message = ClusterMessage.builder()
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_ONE_TIME_TASK_DATA)
.build();
this.collectServer.sendMsg(message);
});
@@ -153,22 +152,22 @@ public class CollectJobService {
*/
public void sendAsyncCollectData(CollectRep.MetricsData metricsData) {
byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData));
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(collectorIdentity)
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA)
ClusterMessage message = ClusterMessage.builder()
.identity(collectorIdentity)
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_CYCLIC_TASK_DATA)
.build();
this.collectServer.sendMsg(message);
}
public void sendAsyncServiceDiscoveryData(CollectRep.MetricsData metricsData) {
byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData));
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(collectorIdentity)
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA)
ClusterMessage message = ClusterMessage.builder()
.identity(collectorIdentity)
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA)
.build();
this.collectServer.sendMsg(message);
}
@@ -184,4 +183,4 @@ public class CollectJobService {
public void setCollectServer(CollectServer collectServer) {
this.collectServer = collectServer;
}
}
}
@@ -21,7 +21,7 @@ import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -37,8 +37,8 @@ public class CollectCyclicDataProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
Job job = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class);
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
Job job = JsonUtil.fromJson(message.getMsg(), Job.class);
if (job == null) {
log.error("collector receive cyclic task job is null");
return null;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import io.netty.channel.ChannelHandlerContext;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -35,8 +35,8 @@ public class CollectOneTimeDataProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
Job oneTimeJob = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class);
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
Job oneTimeJob = JsonUtil.fromJson(message.getMsg(), Job.class);
collectServer.getCollectJobService().collectSyncOneTimeJobData(oneTimeJob);
return null;
}
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -39,9 +39,9 @@ public class DeleteCyclicTaskProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
TypeReference<List<Long>> typeReference = new TypeReference<>() {};
List<Long> jobIds = JsonUtil.fromJson(message.getMsg().toStringUtf8(), typeReference);
List<Long> jobIds = JsonUtil.fromJson(message.getMsgString(), typeReference);
if (jobIds == null || jobIds.isEmpty()) {
log.error("collector receive delete cyclic task job ids is null");
return null;
@@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
import org.springframework.boot.SpringApplication;
@@ -41,12 +41,12 @@ public class GoCloseProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsg());
if (message.getMsgString().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsgString());
}
this.timerDispatch.goOffline();
this.collectServer.shutdown();
@@ -17,12 +17,12 @@
package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -36,20 +36,21 @@ public class GoOfflineProcessor implements NettyRemotingProcessor {
private TimerDispatch timerDispatch;
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
timerDispatch.goOffline();
log.info("receive offline message and handle success");
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go offline. {}", message.getMsg());
if (message.getMsgString().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go offline. {}", message.getMsgString());
return null;
}
return ClusterMsg.Message.newBuilder()
.setIdentity(message.getIdentity())
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(String.valueOf(CommonConstants.SUCCESS_CODE)))
return ClusterMessage.builder()
.identity(message.getIdentity())
.direction(ClusterMessage.Direction.RESPONSE)
.type(ClusterMessage.MessageType.GO_OFFLINE)
.msg(String.valueOf(CommonConstants.SUCCESS_CODE).getBytes(StandardCharsets.UTF_8))
.build();
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,13 +17,11 @@
package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
@@ -39,28 +37,26 @@ public class GoOnlineProcessor implements NettyRemotingProcessor {
private TimerDispatch timerDispatch;
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().isEmpty()) {
String msgString = message.getMsgString();
if (message.getMsg() == null || msgString == null || msgString.isEmpty()) {
log.warn("The message that server response to collector is empty, please upgrade server");
} else {
ServerInfo serverInfo = JsonUtil.fromJson(message.getMsg().toStringUtf8(), ServerInfo.class);
// Use the new JsonUtil.fromJson(byte[], Class) method
ServerInfo serverInfo = JsonUtil.fromJson(message.getMsg(), ServerInfo.class);
if (serverInfo == null || serverInfo.getAesSecret() == null) {
log.warn("The message that server response to collector has not secret empty, please check");
} else {
AesUtil.setDefaultSecretKey(serverInfo.getAesSecret());
}
}
if (ClusterMsg.Direction.REQUEST.equals(message.getDirection())) {
timerDispatch.goOnline();
}
timerDispatch.goOnline();
log.info("receive online message and handle success");
return ClusterMsg.Message.newBuilder()
.setIdentity(message.getIdentity())
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(String.valueOf(CommonConstants.SUCCESS_CODE)))
.build();
// Return null to stop the ping-pong loop.
// The collector should not reply to the server's confirmation response.
return null;
}
}
}
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
/**
@@ -28,7 +28,7 @@ import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@Slf4j
public class HeartbeatProcessor implements NettyRemotingProcessor {
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
log.info("collector receive manager server response heartbeat, time: {}. ", System.currentTimeMillis());
return null;
}
@@ -25,8 +25,6 @@ import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* for collector instance
* send collect response data by netty
@@ -110,24 +108,4 @@ public class NettyDataQueue implements CommonDataQueue {
public LogEntry pollLogEntryToStorage() throws InterruptedException {
return null;
}
@Override
public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) {
}
@Override
public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException {
return List.of();
}
@Override
public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) {
}
@Override
public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException {
return List.of();
}
}
@@ -1,105 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatcher;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.lang.reflect.Field;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test for GoOnlineProcessor
*/
class GoOnlineProcessorTest {
private GoOnlineProcessor goOnlineProcessor;
private TimerDispatcher timerDispatcher;
@Mock
private ChannelHandlerContext channelHandlerContext;
private MockedStatic<SpringContextHolder> springContextHolderMockedStatic;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
goOnlineProcessor = new GoOnlineProcessor();
timerDispatcher = new TimerDispatcher();
springContextHolderMockedStatic = Mockito.mockStatic(SpringContextHolder.class);
springContextHolderMockedStatic.when(() -> SpringContextHolder.getBean(TimerDispatch.class)).thenReturn(timerDispatcher);
}
@AfterEach
void tearDown() throws Exception {
springContextHolderMockedStatic.close();
timerDispatcher.destroy();
}
@Test
void verifyTaskMapPreservation() throws Exception {
Job job = Job.builder()
.app("test")
.id(12345L)
.metrics(Lists.newArrayList(Metrics.builder().interval(100L).build()))
.configmap(Lists.newArrayList())
.isCyclic(true)
.build();
timerDispatcher.addJob(job, null);
Field cyclicTaskMapField = TimerDispatcher.class.getDeclaredField("currentCyclicTaskMap");
cyclicTaskMapField.setAccessible(true);
Map<?, ?> currentCyclicTaskMap = (Map<?, ?>) cyclicTaskMapField.get(timerDispatcher);
assertEquals(1, currentCyclicTaskMap.size(), "Task map should have 1 job initially");
ClusterMsg.Message responseMsg = ClusterMsg.Message.newBuilder()
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
goOnlineProcessor.handle(channelHandlerContext, responseMsg);
assertEquals(1, currentCyclicTaskMap.size(), "Task map should still have 1 job after receiving RESPONSE");
ClusterMsg.Message requestMsg = ClusterMsg.Message.newBuilder()
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.REQUEST)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
goOnlineProcessor.handle(channelHandlerContext, requestMsg);
assertEquals(0, currentCyclicTaskMap.size(), "Task map should be empty after receiving REQUEST");
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.collector.collect.nebulagraph;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
@@ -25,7 +24,17 @@ import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
@@ -37,18 +46,9 @@ import org.apache.hertzbeat.common.entity.job.protocol.NebulaGraphProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
/**
* NebulaGraph collect
* NebulaGraph collect
*/
@Slf4j
public class NebulaGraphCollectImpl extends AbstractCollect {
@@ -66,6 +66,27 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
private static final String STORAGE_API = "/rocksdb_stats";
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
if (metrics == null || metrics.getNebulaGraph() == null) {
@@ -91,37 +112,50 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
return;
}
String resp;
long responseTime;
HashMap<String, String> resultMap = new HashMap<>(64);
HttpContext httpContext = createHttpContext(nebulaGraph.getHost(), nebulaGraph.getPort());
HttpUriRequest request = createHttpRequest(nebulaGraph.getHost(), nebulaGraph.getPort(),
// Create Request and Config
HttpRequestWithConfig requestWithConfig = createHttpRequest(nebulaGraph.getHost(), nebulaGraph.getPort(),
nebulaGraph.getUrl(), nebulaGraph.getTimeout());
// Create Context and set Config
HttpContext httpContext = createHttpContext(requestWithConfig.getRequestConfig());
ClassicHttpRequest request = requestWithConfig.getRequest();
// Send an HTTP request to obtain response data
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("StatusCode " + statusCode);
return;
}
resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
responseTime = System.currentTimeMillis() - startTime;
resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime));
// Parse the response differently depending on the API
if (GRAPH_API.equals(nebulaGraph.getUrl())) {
parseStatsResponse(resp, nebulaGraph.getTimePeriod(), resultMap);
} else if (STORAGE_API.equals(nebulaGraph.getUrl())) {
parseStorageResponse(resp, resultMap);
}
List<String> aliasFields = metrics.getAliasFields();
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String field : aliasFields) {
String fieldValue = resultMap.get(field);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
}
builder.addValueRow(valueRowBuilder.build());
} catch (IOException e) {
try {
// Use ResponseHandler to avoid manual resource management and CloseableHttpResponse deprecation issues
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("StatusCode " + statusCode);
return null;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
long responseTime = System.currentTimeMillis() - startTime;
HashMap<String, String> resultMap = new HashMap<>(64);
resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime));
// Parse the response differently depending on the API
if (GRAPH_API.equals(nebulaGraph.getUrl())) {
parseStatsResponse(resp, nebulaGraph.getTimePeriod(), resultMap);
} else if (STORAGE_API.equals(nebulaGraph.getUrl())) {
parseStorageResponse(resp, resultMap);
}
List<String> aliasFields = metrics.getAliasFields();
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String field : aliasFields) {
String fieldValue = resultMap.get(field);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
}
builder.addValueRow(valueRowBuilder.build());
return null;
};
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
@@ -134,43 +168,47 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
return DispatchConstants.PROTOCOL_NEBULAGRAPH;
}
private HttpContext createHttpContext(String host, String port) {
HttpHost httpHost = new HttpHost(host, Integer.parseInt(port));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(httpHost);
return httpClientContext;
private HttpContext createHttpContext(RequestConfig requestConfig) {
HttpClientContext context = HttpClientContext.create();
if (requestConfig != null) {
context.setRequestConfig(requestConfig);
}
return context;
}
private HttpUriRequest createHttpRequest(String host, String port, String url, String timeoutStr) {
RequestBuilder requestBuilder = RequestBuilder.get();
// uri
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(String host, String port, String url, String timeoutStr) {
// HttpClient 5 uses ClassicRequestBuilder
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// uri construction
String uri = CollectUtil.replaceUriSpecialChar(url);
if (IpDomainUtil.isHasSchema(host)) {
requestBuilder.setUri(host + ":" + port + uri);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(host);
String baseUri = CollectorConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s", host, port + uri)
: String.format("%s:%s", host, port + uri);
? String.format("[%s]:%s%s", host, port, uri)
: String.format("%s:%s%s", host, port, uri);
requestBuilder.setUri(CollectorConstants.HTTP_HEADER + baseUri);
}
requestBuilder.addHeader(HttpHeaders.CONNECTION, "keep-alive");
requestBuilder.addHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36");
requestBuilder.addHeader(HttpHeaders.ACCEPT, "text/plain");
RequestConfig requestConfig = null;
int timeout = Integer.parseInt(timeoutStr);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
// Use setConnectTimeout despite deprecation to allow per-request connection timeout override on a shared client
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -178,6 +216,7 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
*
* @param responseBody response body
* @param timePeriod time period
* @param resultMap result map
*/
private void parseStatsResponse(String responseBody, String timePeriod, HashMap<String, String> resultMap) {
// Set up regular expression matching
@@ -188,22 +227,26 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
Matcher matcher = pattern.matcher(str);
if (matcher.find()) {
String[] split = str.split(timeRegex);
resultMap.put(split[0], split[1]);
if (split.length > 1) {
resultMap.put(split[0], split[1]);
}
}
}
}
/**
* Parse the Storage response and filter by time period
*
* @param responseBody response body
* @param resultMap result map
*/
private void parseStorageResponse(String responseBody, HashMap<String, String> resultMap) {
String[] strArray = responseBody.split(STR_SPLIT);
for (String str : strArray) {
String[] split = str.split(STORAGE_SPLIT_KEY_VALUE);
resultMap.put(split[0], split[1]);
if (split.length > 1) {
resultMap.put(split[0], split[1]);
}
}
}
}
}
+1 -1
View File
@@ -28,7 +28,7 @@
<name>${project.artifactId}</name>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
+18 -19
View File
@@ -46,20 +46,9 @@
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
<version>${eclipse-jpa.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- validation -->
<dependency>
@@ -76,6 +65,10 @@
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson2</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>org.springdoc</groupId>
@@ -95,11 +88,6 @@
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
</dependency>
<!-- Use apache.http.conn.util instead of sun.net.util -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@@ -171,7 +159,7 @@
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
@@ -184,8 +172,19 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<groupId>org.apache.fory</groupId>
<artifactId>fory-core</artifactId>
<version>${fory.version}</version>
</dependency>
<!-- row/arrow format support -->
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-format</artifactId>
<version>${fory.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
</dependencies>
@@ -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.
*/
package org.apache.hertzbeat.common.config;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.persistence.config.SessionCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.sessions.Session;
/**
* change column name that from upper-case to lower-case-with-underscore
*/
@Slf4j
public class EclipseLinkCustomizer implements SessionCustomizer {
@Override
public void customize(Session session) throws Exception {
for (ClassDescriptor descriptor : session.getDescriptors().values()) {
for (DatabaseMapping mapping : descriptor.getMappings()) {
if (mapping.isDirectToFieldMapping()) {
// update the column name to lower case with underscore
if (!mapping.getField().getName().equalsIgnoreCase(mapping.getAttributeName())) {
// already custom define the column name, ignore
continue;
}
// todo here i try to change column name that from upper-case to lower-case-with-underscore
// but failed, when update name, something relate exception happen more
mapping.getField().setName(convertCamelCase(mapping.getAttributeName()));
}
}
}
}
private String convertCamelCase(String camelCase) {
StringBuilder result = new StringBuilder();
if (camelCase != null && !camelCase.isEmpty()) {
result.append(Character.toLowerCase(camelCase.charAt(0)));
for (int i = 1; i < camelCase.length(); i++) {
char currentChar = camelCase.charAt(i);
if (Character.isUpperCase(currentChar)) {
result.append('_').append(Character.toLowerCase(currentChar));
} else {
result.append(currentChar);
}
}
}
return result.toString();
}
}
@@ -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.common.config;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.transaction.jta.JtaTransactionManager;
/**
* jpa eclipselink impl config
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
public class EclipseLinkJpaConfiguration extends JpaBaseConfiguration {
protected EclipseLinkJpaConfiguration(DataSource dataSource, JpaProperties properties,
ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
super(dataSource, properties, jtaTransactionManager);
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new EclipseLinkJpaVendorAdapter();
}
@Override
protected Map<String, Object> getVendorProperties() {
HashMap<String, Object> map = new HashMap<>(8);
map.put(PersistenceUnitProperties.DDL_GENERATION, "create-or-extend-tables");
map.put(PersistenceUnitProperties.SESSION_CUSTOMIZER, "org.apache.hertzbeat.common.config.EclipseLinkCustomizer");
map.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
map.put(PersistenceUnitProperties.WEAVING, "true");
return map;
}
}
@@ -97,6 +97,11 @@ public interface CommonConstants {
*/
String LABEL_ALERT_NAME = "alertname";
/**
* label key: instance host
*/
String LABEL_INSTANCE_HOST = "instancehost";
/**
* label key: instance name
*/
@@ -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.common.constants;
/**
* HertzBeat Reserved Keywords Enum
*/
public enum HertzBeatKeywordsEnum {
INSTANCE("instance", "metric_instance");
private final String keyword;
private final String alias;
HertzBeatKeywordsEnum(String keyword, String alias) {
this.keyword = keyword;
this.alias = alias;
}
public String getKeyword() {
return keyword;
}
public String getAlias() {
return alias;
}
}
@@ -38,5 +38,5 @@ public interface MetricDataConstants {
String CODE = "code";
String MSG = "msg";
String INSTANCE_NAME = "instancename";
String INSTANCE = "instance";
String INSTANCE_HOST = "instancehost";
}
@@ -1,50 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.constants;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* Theme Enum
*/
@Getter
@AllArgsConstructor
@ToString
public enum ThemeEnum {
COMPACT("compact"), DEFAULT("default"), LIGHT("light"), DARK("dark");
private final String value;
/**
* Convert theme to standard theme
*
* @param theme the input theme
* @return the converted theme
*/
public static String convert(String theme) {
if ((theme == null || theme.isEmpty())
|| ThemeEnum.COMPACT.getValue().equals(theme)
|| ThemeEnum.DEFAULT.getValue().equals(theme)) {
return ThemeEnum.LIGHT.getValue();
}
return theme;
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -55,7 +55,7 @@ public class ChatConversation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Schema(title = "conversation title")
private String title;
@@ -79,6 +79,6 @@ public class ChatConversation {
* List of messages in this conversation (one-to-many relationship)
*/
@OneToMany
@JoinColumn(name = "conversation_id")
@JoinColumn(name = "conversation_id", insertable = false, updatable = false)
private List<ChatMessage> messages;
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.common.entity.ai;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
@@ -48,7 +49,7 @@ import lombok.NoArgsConstructor;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "hzb_ai_message", indexes = {
@Index(name = "idx_message_conversation_id", columnList = "conversation_id")
@Index(name = "idx_message_conversation_id", columnList = "conversation_id")
})
@AllArgsConstructor
@NoArgsConstructor
@@ -57,8 +58,9 @@ public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Schema(title = "conversation id")
@Column(name = "conversation_id")
private Long conversationId;
@Schema(title = "message content")
@@ -84,5 +86,5 @@ public class ChatMessage {
@Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY)
@LastModifiedDate
private LocalDateTime gmtUpdate;
}
}
@@ -101,6 +101,7 @@ public class AlertDefine {
private String datasource;
@Schema(title = "Is Enabled", example = "true")
@Builder.Default
private boolean enable = true;
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
@@ -48,8 +48,8 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
*/
@Entity
@Table(name = "hzb_alert_define_monitor_bind", indexes = {
@Index(name = "idx_alert_define_id", columnList = "alert_define_id"),
@Index(name = "idx_monitor_id", columnList = "monitor_id")
@Index(name = "index_alert_define_monitor", columnList = "alertDefineId"),
@Index(name = "index_alert_define_monitor", columnList = "monitor_id")
})
@Data
@Builder
@@ -69,9 +69,11 @@ public class AlertSilence {
private String name;
@Schema(title = "Whether to enable this policy", example = "true")
@Builder.Default
private boolean enable = true;
@Schema(title = "Whether to match all", example = "true")
@Builder.Default
private boolean matchAll = true;
@Schema(title = "Silence type 0: once, 1:cyc", example = "1")
@@ -103,11 +103,13 @@ public class NoticeRule {
@Schema(title = "Whether to enable this policy",
description = "Whether to enable this policy",
example = "true", accessMode = READ_WRITE)
@Builder.Default
private boolean enable = true;
@Schema(title = "Whether to forward all",
description = "Whether to forward all",
example = "false", accessMode = READ_WRITE)
@Builder.Default
private boolean filterAll = true;
@Schema(title = "Labels", example = "{\"alertname\": \"HighCPUUsage\", \"priority\": \"critical\", \"instance\": \"343483943\"}")
@@ -85,6 +85,7 @@ public class NoticeTemplate {
description = "Is it a preset template: true- preset template false- custom template.",
accessMode = READ_WRITE)
@Column(columnDefinition = "boolean default false")
@Builder.Default
private boolean preset = false;
@Schema(title = "Template content",

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