Compare commits

...
Author SHA1 Message Date
tomsun28 d2bfa4fc5d [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:55:22 +08:00
tomsun28 bc865d971e [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:54:01 +08:00
tomsun28 070ab41966 [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:51:53 +08:00
bd1924b016 [bugfix] fix service discovery host field NullPointerException (#3767)
Co-authored-by: kangli <1363539513@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-09-23 21:23:40 +08:00
1c781d5bc5 [bugfix] webhook url query parameters missing (#3779)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-09-23 09:31:47 +08:00
586aa53e84 [OSPP] Implement a mcp server to run any scripts or commands securely (#3547)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-09-22 19:54:59 +08:00
c5f0389138 [improve] Prometheus streaming parsing optimization enhancements (#3761)
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-21 23:34:06 +08:00
b93613c25e [feat] Support jenkins monitoring (#3774)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-09-21 15:38:14 +08:00
a8b0c1024d [Improve] Optimise and use the label-selector component for label selection within pages (#3762)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-20 19:16:32 +08:00
Duansg 25a63ccb25 [improve] Improving the operation of threshold rules and the output of expression logs (#3780) 2025-09-20 17:11:19 +08:00
59 changed files with 9002 additions and 126 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"files": [
"README.md",
"README_CN.md",
"README_JP.md"
"README_JP.md",
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
+97
View File
@@ -0,0 +1,97 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: MCP Bash Server CI
on:
push:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
pull_request:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
env:
CARGO_TERM_COLOR: always
RUST_VERSION: 1.88.0
jobs:
test:
name: Test MCP Bash Server
runs-on: ubuntu-latest
strategy:
matrix:
profile: [dev, release]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target/
key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-${{ matrix.profile }}-
${{ runner.os }}-cargo-build-
- name: Check code format
run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check
- name: Run cargo check
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
- name: Run clippy
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
else
cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
fi
- name: Run tests
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
+12 -2
View File
@@ -361,7 +361,7 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -385,7 +385,7 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -529,6 +529,16 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a> <a href="#design-tuzuy" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+12 -2
View File
@@ -359,7 +359,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -383,7 +383,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -527,6 +527,16 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a> <a href="#design-tuzuy" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+12 -2
View File
@@ -363,7 +363,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -387,7 +387,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -531,6 +531,16 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a> <a href="#design-tuzuy" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+1 -1
View File
@@ -57,7 +57,7 @@ items:
"credential": "hertzbeat"
}
expect:
statusCode: 409
statusCode: 500
- name: monitorList
request:
api: /api/monitors?pageIndex=0&pageSize=8
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.alert.notice.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -39,6 +40,13 @@ final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
try {
String hookUrl = receiver.getHookUrl();
// Basic URL validation
if (StringUtils.isBlank(hookUrl)) {
throw new AlertNoticeException("Webhook URL is null or empty");
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// alert.setContent(escapeJsonStr(alert.getContent()));
@@ -46,11 +54,11 @@ final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
webhookJson = webhookJson.replace(",\n }", "\n }");
HttpEntity<String> alertHttpEntity = new HttpEntity<>(webhookJson, headers);
ResponseEntity<String> entity = restTemplate.postForEntity(receiver.getHookUrl(), alertHttpEntity, String.class);
ResponseEntity<String> entity = restTemplate.postForEntity(hookUrl, alertHttpEntity, String.class);
if (entity.getStatusCode().value() < HttpStatus.BAD_REQUEST.value()) {
log.debug("Send WebHook: {} Success", receiver.getHookUrl());
log.debug("Send WebHook: {} Success", hookUrl);
} else {
log.warn("Send WebHook: {} Failed: {}", receiver.getHookUrl(), entity.getBody());
log.warn("Send WebHook: {} Failed: {}", hookUrl, entity.getBody());
throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode());
}
} catch (Exception e) {
@@ -106,14 +106,14 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException {
alertDefine = alertDefineDao.save(alertDefine);
alertDefine = alertDefineDao.saveAndFlush(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
}
@Override
public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException {
alertDefineDao.save(alertDefine);
alertDefineDao.saveAndFlush(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
}
@@ -87,7 +87,7 @@ public class DataSourceServiceImpl implements DataSourceService {
try {
return evaluate(expr, executor);
} catch (AlertExpressionException ae) {
log.error("Calculate query parse error {}: {}", datasource, ae.getMessage());
log.error("Calculate query parse error, datasource: {}, expr: {}, msg: {}", datasource, expr, ae.getMessage(), ae);
throw ae;
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
@@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -64,7 +63,7 @@ public class CryptoUtils {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data.getBytes(UTF8));
return DatatypeConverter.printHexBinary(digest).toLowerCase();
return bytesToHex(digest).toLowerCase();
} catch (Exception e) {
log.error("Failed to calculate SHA256: {}", e.getMessage());
throw new RuntimeException("Failed to calculate SHA256", e);
@@ -101,6 +100,19 @@ public class CryptoUtils {
*/
public static String hmacSha256Hex(byte[] key, String data) {
byte[] hmacResult = hmac256(key, data);
return DatatypeConverter.printHexBinary(hmacResult).toLowerCase();
return bytesToHex(hmacResult).toLowerCase();
}
/**
* Convert byte array to hexadecimal string
* @param bytes byte array to convert
* @return hexadecimal string representation
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte byteValue : bytes) {
result.append(String.format("%02x", byteValue));
}
return result.toString();
}
}
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.alert.notice.impl;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
@@ -105,7 +106,76 @@ class WebHookAlertNotifyHandlerImplTest {
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> webHookAlertNotifyHandler.send(receiver, template, groupAlert));
}
@Test
public void testNotifyAlertWithQueryParametersSuccess() {
receiver.setHookUrl("https://prod-12.chinaeast2.logic.azure.cn:443/workflows/test/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=testSignature123");
ResponseEntity<String> responseEntity =
new ResponseEntity<>("null", HttpStatus.OK);
when(restTemplate.postForEntity(eq(receiver.getHookUrl()), any(), eq(String.class))).thenReturn(responseEntity);
webHookAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertWithNullOrEmptyUrl() {
// Test null URL
receiver.setHookUrl(null);
assertThrows(AlertNoticeException.class,
() -> webHookAlertNotifyHandler.send(receiver, template, groupAlert));
// Test empty URL
receiver.setHookUrl("");
assertThrows(AlertNoticeException.class,
() -> webHookAlertNotifyHandler.send(receiver, template, groupAlert));
// Test blank URL
receiver.setHookUrl(" ");
assertThrows(AlertNoticeException.class,
() -> webHookAlertNotifyHandler.send(receiver, template, groupAlert));
}
@Test
public void testNotifyAlertWithValidUrls() {
ResponseEntity<String> responseEntity =
new ResponseEntity<>("null", HttpStatus.OK);
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
// Test various valid URLs that should work
receiver.setHookUrl("https://hooks.slack.com/services/T123/B456/complete-token");
webHookAlertNotifyHandler.send(receiver, template, groupAlert);
receiver.setHookUrl("https://discord.com/api/webhooks/123456789/complete-token");
webHookAlertNotifyHandler.send(receiver, template, groupAlert);
receiver.setHookUrl("https://example.com/webhook?complete=true&valid=yes");
webHookAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertWithLongUrlSuccess() {
// Test that long URLs (over 300 chars) can now be handled with the increased field length
StringBuilder longUrl = new StringBuilder("https://prod-12.chinaeast2.logic.azure.cn:443/workflows/test/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=");
for (int i = 0; i < 200; i++) {
longUrl.append("x");
}
receiver.setHookUrl(longUrl.toString());
ResponseEntity<String> responseEntity =
new ResponseEntity<>("null", HttpStatus.OK);
when(restTemplate.postForEntity(eq(receiver.getHookUrl()), any(), eq(String.class))).thenReturn(responseEntity);
webHookAlertNotifyHandler.send(receiver, template, groupAlert);
// Verify the URL is longer than the old 300 char limit
assertTrue(receiver.getHookUrl().length() > 300,
"URL should be longer than old 300 char limit to test the field length increase");
}
}
@@ -101,17 +101,17 @@ class AlertDefineServiceTest {
@Test
void addAlertDefine() {
assertDoesNotThrow(() -> alertDefineService.addAlertDefine(alertDefine));
when(alertDefineDao.save(alertDefine)).thenThrow(new RuntimeException());
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.addAlertDefine(alertDefine));
}
@Test
void modifyAlertDefine() {
AlertDefine alertDefine = AlertDefine.builder().id(1L).build();
when(alertDefineDao.save(alertDefine)).thenReturn(alertDefine);
when(alertDefineDao.saveAndFlush(alertDefine)).thenReturn(alertDefine);
assertDoesNotThrow(() -> alertDefineService.modifyAlertDefine(alertDefine));
reset();
when(alertDefineDao.save(alertDefine)).thenThrow(new RuntimeException());
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.modifyAlertDefine(alertDefine));
}
@@ -627,33 +627,35 @@ public class HttpCollectImpl extends AbstractCollect {
}
private void parseResponseByPrometheusExporter(InputStream content, List<String> aliasFields, CollectRep.MetricsData.Builder builder) throws IOException {
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(content);
String metrics = builder.getMetrics();
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(content, metrics);
if (metricFamilyMap == null || metricFamilyMap.isEmpty()) {
return;
}
String metrics = builder.getMetrics();
if (metricFamilyMap.containsKey(metrics)) {
MetricFamily metricFamily = metricFamilyMap.get(metrics);
for (MetricFamily.Metric metric : metricFamily.getMetricList()) {
Map<String, String> labelMap = metric.getLabels()
.stream()
.collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue));
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String aliasField : aliasFields) {
String columnValue = labelMap.get(aliasField);
if (columnValue != null) {
valueRowBuilder.addColumn(columnValue);
} 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);
}
MetricFamily metricFamily = metricFamilyMap.get(metrics);
if (null == metricFamily || CollectionUtils.isEmpty(metricFamily.getMetricList())) {
return;
}
for (MetricFamily.Metric metric : metricFamily.getMetricList()) {
Map<String, String> labelMap = metric.getLabels()
.stream()
.collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue));
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String aliasField : aliasFields) {
String columnValue = labelMap.get(aliasField);
if (columnValue != null) {
valueRowBuilder.addColumn(columnValue);
} 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);
}
builder.addValueRow(valueRowBuilder.build());
}
builder.addValueRow(valueRowBuilder.build());
}
}
private void parseResponseByDefault(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
JsonElement element = JsonParser.parseString(resp);
@@ -81,6 +81,114 @@ public class OnlineParser {
return metricFamilyMap;
}
/**
* Parses Prometheus metrics from the given {@link InputStream}, but only for the specified metric name.
* <p>
* This method differs from {@link #parseMetrics(InputStream)} in that it filters and parses only the metric
* with the given name, rather than parsing all available metrics from the input stream.
*
* @param inputStream the input stream containing Prometheus metrics data
* @param metric the name of the metric to filter and parse (case-insensitive)
* @return a map of metric family names to {@link MetricFamily} objects, or {@code null} if parsing fails
* @throws IOException if an I/O error occurs while reading from the input stream
*/
public static Map<String, MetricFamily> parseMetrics(InputStream inputStream, String metric) throws IOException {
Map<String, MetricFamily> metricFamilyMap = new ConcurrentHashMap<>(10);
try {
int i = getChar(inputStream);
while (i != -1) {
if (i == '#' || i == '\n') {
skipToLineEnd(inputStream).maybeEol().maybeEof().noElse();
} else {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append((char) i);
// parse the metricName to filter.
int next = parseMetricName(inputStream, stringBuilder).maybeSpace().maybeLeftBracket().noElse();
String metricName = stringBuilder.toString();
stringBuilder.delete(0, stringBuilder.length());
// step2: Determine whether this metric should be parsed.
if (metric.equals(metricName)) {
parseMetricFromName(inputStream, stringBuilder, metricFamilyMap, metricName, next);
} else {
skipToLineEnd(inputStream).maybeEol().maybeEof().noElse();
}
}
i = getChar(inputStream);
// To address the `\n\r` scenario, it is necessary to skip
if (i == '\r') {
i = getChar(inputStream);
}
}
} catch (FormatException e) {
log.error("prometheus parser failed because of wrong input format. {}", e.getMessage());
return null;
}
return metricFamilyMap;
}
/**
* Start parsing the complete metric from the already parsed metric name.
*
* @param inputStream the input stream containing the metric data
* @param stringBuilder the StringBuilder used for parsing and temporary storage
* @param metricFamilyMap the map to store parsed MetricFamily objects, keyed by metric name
* @param metricName the name of the metric to parse
* @param next the next character to process from the input stream
* @return a CharChecker containing the next character after parsing the metric
* @throws IOException if an I/O error occurs while reading the input stream
* @throws FormatException if the input format is invalid
*/
private static CharChecker parseMetricFromName(InputStream inputStream, StringBuilder stringBuilder,
Map<String, MetricFamily> metricFamilyMap,
String metricName, int next) throws IOException, FormatException {
MetricFamily metricFamily;
MetricFamily.Metric metric = new MetricFamily.Metric();
if (!metricFamilyMap.containsKey(metricName)) {
metricFamily = new MetricFamily();
metricFamily.setMetricList(new ArrayList<>());
metricFamily.setName(metricName);
metricFamilyMap.put(metricName, metricFamily);
} else {
metricFamily = metricFamilyMap.get(metricName);
}
int i = next;
if (i == ' ') {
i = skipSpaces(inputStream).getInt();
}
List<MetricFamily.Label> labelList = new LinkedList<>();
metric.setLabels(labelList);
if (i == '{') {
parseLabels(inputStream, stringBuilder, labelList);
i = skipSpaces(inputStream).getInt();
}
stringBuilder.delete(0, stringBuilder.length());
stringBuilder.append((char) i);
i = parseOneDouble(inputStream, stringBuilder).maybeSpace().maybeEol().maybeEof().noElse();
metric.setValue(toDouble(stringBuilder.toString()));
if (i == '\n' || i == -1) {
metricFamily.getMetricList().add(metric);
return new CharChecker(i);
}
i = skipSpaces(inputStream).getInt();
stringBuilder.delete(0, stringBuilder.length());
stringBuilder.append((char) i);
i = skipOneLong(inputStream).maybeSpace().maybeEol().maybeEof().noElse();
if (i == '\n' || i == -1) {
metricFamily.getMetricList().add(metric);
return new CharChecker(i);
}
i = skipSpaces(inputStream).maybeEol().maybeEof().noElse();
metricFamily.getMetricList().add(metric);
return new CharChecker(i);
}
private static class FormatException extends Exception {
public FormatException() {
@@ -0,0 +1,450 @@
/*
* 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.collect.prometheus.parser;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* Parse a Single metric in prometheus test
*/
public class OnlineParserSingleTest {
@Disabled // Disabled due to the fact that the URL is not reachable unless you have the Prometheus server running
@Test
void parseMetrics() throws Exception {
URL url = new URL("http://localhost:9090/metrics");
InputStream inputStream = url.openStream();
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "go_gc_duration_seconds");
assertNotNull(metricFamilyMap);
}
@Disabled // because unless you have already saved the locally tested files
@Test
void parseTestFile() throws Exception {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("test_file.txt");
assertNotNull(inputStream, "Failed to load test_file.txt resource");
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_textfile_scrape_error");
assertNotNull(metricFamilyMap);
}
@Test
void parseMetricsOutOfOrder() throws Exception {
String str = """
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.139
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.02
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 5.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.082
go_gc_duration_seconds{quantile="0"} 2.0209e-05
go_gc_duration_seconds{quantile="0"} 2.0209e-05
go_gc_duration_seconds{quantile="0.25"} 6.6917e-05
go_gc_duration_seconds{quantile="0.5"} -Inf
go_gc_duration_seconds{quantile="0.75"} +Inf
go_gc_duration_seconds{quantile="1"} NaN
go_gc_duration_seconds_sum 0.001134793
go_gc_duration_seconds_count 5
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 32
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.21.6"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 1.5716224e+07
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 2.0707544e+07
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.457881e+06
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 50438
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 4.614808e+06""";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap1 = parseMetrics(inputStream, "go_memstats_buck_hash_sys_bytes");
assertNotNull(metricFamilyMap1);
MetricFamily metricFamily = metricFamilyMap1.get("go_memstats_buck_hash_sys_bytes");
assertNotNull(metricFamily);
assertNotNull(metricFamily.getName());
assertEquals("go_memstats_buck_hash_sys_bytes", metricFamily.getName());
assertEquals(1, metricFamily.getMetricList().size());
assertEquals(1457881.0, metricFamily.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithCrLf() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\r\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\r\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\r\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\r\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\r\n";
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
assertEquals(0.139, metricFamily.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithLf() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\n";
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
assertEquals(0.139, metricFamily.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithoutFinalNewline() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\r\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
assertEquals(0.139, metricFamily.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithEmptyLabelsAndCrLf() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\r\n"
+ "jvm_gc_pause_seconds_count 1.0\r\n"
+ "jvm_gc_pause_seconds_sum{} 0.139";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithEmptyLabelsAndLf() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count 1.0\n"
+ "jvm_gc_pause_seconds_sum{} 0.139";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithMixedLineEndings() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count 1.0\n"
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithLfCrLineEnding() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n\r"
+ "# TYPE go_gc_duration_seconds summary\n\r"
+ "jvm_gc_pause_seconds_count 1.0\n\r"
+ "jvm_gc_pause_seconds_sum{} 0.139\n\r";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithMixedLineEndingsIncludingLfCr() throws Exception {
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "jvm_gc_pause_seconds_count 1.0\n\r"
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n"
+ "jvm_gc_pause_seconds_max{} 0.139\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_max");
assertNotNull(metricFamilyMap);
MetricFamily metricFamily2 = metricFamilyMap.get("jvm_gc_pause_seconds_max");
assertEquals("jvm_gc_pause_seconds_max", metricFamily2.getName());
assertEquals(0.139, metricFamily2.getMetricList().get(0).getValue());
}
@Test
void testParseMetricsWithOnlyLfCr() throws Exception {
String str = "jvm_gc_pause_seconds_sum 42.0\n\r";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
assertEquals(42.0, metricFamily.getMetricList().get(0).getValue());
}
@Test
void testParseMetricEscape1() throws Exception {
// test escape '\\'
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "windows_service_info{display_name=\"\\\\Application Layer \nGateway Service\",name=\"alg\\\\\",process_id=\"0\",run_as=\"NT AUTHORITY\\\\LocalService\"} 1\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
assertEquals("windows_service_info", metricFamily.getName());
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
assertEquals("\\Application Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
assertEquals("alg\\", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
assertEquals("NT AUTHORITY\\LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
}
@Test
void testParseMetricEscape2() throws Exception {
// test escape '\"'
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "windows_service_info{display_name=\"\\\"Application Layer \\\"Gateway Service\",name=\"alg\\\"\",process_id=\"0\",run_as=\"NT AUTHORITY\\\"LocalService\"} 1\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
assertEquals("windows_service_info", metricFamily.getName());
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
assertEquals("\"Application Layer \"Gateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
assertEquals("alg\"", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
assertEquals("NT AUTHORITY\"LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
}
@Test
void testParseMetricEscape3() throws Exception {
// test escape '\n'
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
+ "# TYPE go_gc_duration_seconds summary\n"
+ "windows_service_info{display_name=\"\nApplication Layer \nGateway Service\",name=\"alg\n\",process_id=\"0\",run_as=\"NT AUTHORITY\nLocalService\"} 1\n";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info");
assertNotNull(metricFamilyMap);
assertEquals(1, metricFamilyMap.values().size());
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
assertEquals("windows_service_info", metricFamily.getName());
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
assertEquals("\nApplication Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
assertEquals("alg\n", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
}
private Map<String, MetricFamily> parseMetrics(InputStream inputStream, String metric) throws IOException {
return OnlineParser.parseMetrics(inputStream, metric);
}
}
@@ -24,6 +24,7 @@ import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.job.Job;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.Map;
@@ -58,6 +59,10 @@ public class HertzBeatMetricsCollector {
Map<String, String> metadata = job.getMetadata();
String monitorName = metadata != null ? metadata.get("instancename") : "unknown";
String monitorTarget = metadata != null ? metadata.get("instancehost") : "unknown";
// todo can not get the host from service discovery
if (!StringUtils.hasText(monitorTarget)) {
monitorTarget = "unknown";
}
// Record collection count
Counter.builder("hertzbeat.collect.total")
@@ -94,8 +94,8 @@ public class NoticeReceiver {
@Schema(title = "URL address: The notification method is valid for webhook",
description = "URL address: The notification method is valid for webhook",
example = "https://www.tancloud.cn", accessMode = READ_WRITE)
@Size(max = 300)
@Column(length = 300)
@Size(max = 1000)
@Column(length = 1000)
private String hookUrl;
@Schema(title = "openId : The notification method is valid for WeChat official account, enterprise WeChat robot or FlyBook robot",
@@ -276,7 +276,8 @@ public class MonitorServiceImpl implements MonitorService {
public void validate(MonitorDto monitorDto, Boolean isModify) throws IllegalArgumentException {
// The request monitoring parameter matches the monitoring parameter definition mapping check
Monitor monitor = monitorDto.getMonitor();
monitor.setHost(monitor.getHost().trim());
// The Service Discovery host field may be null
monitor.setHost(StringUtils.hasText(monitor.getHost()) ? monitor.getHost().trim() : null);
monitor.setName(monitor.getName().trim());
Map<String, Param> paramMap = monitorDto.getParams()
.stream()
@@ -316,9 +317,14 @@ public class MonitorServiceImpl implements MonitorService {
// Parameter definition structure verification
List<ParamDefine> paramDefines = appService.getAppParamDefines(monitorDto.getMonitor().getApp());
if (!CollectionUtils.isEmpty(paramDefines)) {
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
for (ParamDefine paramDefine : paramDefines) {
String field = paramDefine.getField();
Param param = paramMap.get(field);
// Get the host from service discovery
if (!isStatic && "host".equals(field)) {
continue;
}
if (paramDefine.isRequired() && (param == null || param.getParamValue() == null)) {
throw new IllegalArgumentException("Params field " + field + " is required.");
}
@@ -0,0 +1,691 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring.
category: CI/CD
# The monitoring application type name (consistent with the file name)
app: jenkins
# The monitoring i18n name
name:
zh-CN: Jenkins
en-US: Jenkins
# The description and help of this monitoring type
help:
zh-CN: HertzBeat 对 Jenkins 通用指标进行测量监控。<br>您可以点击 “<i>新建 Jenkins</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。
en-US: HertzBeat monitors Jenkins generic metrics. You can click "<i>New Jenkins</i>" to configure it, or select "<i>More Actions</i>" to import an existing configuration.
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/jenkins
en-US: https://hertzbeat.apache.org/docs/help/jenkins
# Input params define for monitoring(render web ui by the definition)
params:
# field-param field key
- field: host
# name-param field display i18n name
name:
zh-CN: 目标Host
en-US: Target Host
ja-JP: 目標ホスト
# type-param field type(most mapping the html input type)
type: host
# required-true or false
required: true
# field-param field key
- field: port
# name-param field display i18n name
name:
zh-CN: 端口
en-US: Port
ja-JP: ポート
# type-param field type(most mapping the html input type)
type: number
# when type is number, range is required
range: '[0,65535]'
required: true
defaultValue: 8080
# field-param field key
- field: timeout
# name-param field display i18n name
name:
zh-CN: 查询超时时间
en-US: Query Timeout
ja-JP: クエリタイムアウト
type: number
required: false
# hide param-true or false
hide: true
# default value
defaultValue: 6000
# collect metrics config list
metrics:
# Basic information indicators
- name: jenkins_health_check_score
i18n:
zh-CN: Jenkins 健康检查分值
en-US: Jenkins health check score
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
# priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
priority: 0
# collect metrics content
fields:
# field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
- field: metric_value
type: 0
i18n:
zh-CN: 分值
en-US: score
# the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
protocol: http
# the config content when protocol is http
http:
# http host: ipv4 ipv6 domain
host: ^_^host^_^
# http port
port: ^_^port^_^
# http url
url: /prometheus
# http method: GET POST PUT DELETE PATCH
method: GET
# http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule
parseType: prometheus
- name: default_jenkins_version_info
i18n:
zh-CN: Jenkins 版本信息
en-US: Jenkins version info
priority: 1
fields:
- field: version
type: 1
i18n:
zh-CN: 版本
en-US: version
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_up
i18n:
zh-CN: Jenkins 是否存活
en-US: Jenkins up
priority: 2
fields:
- field: metric_value
type: 0
i18n:
zh-CN: up
en-US: up
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_uptime
i18n:
zh-CN: Jenkins 运行时间
en-US: Jenkins uptime
priority: 3
fields:
- field: metric_value
type: 0
unit: 'ms'
i18n:
zh-CN: 运行时间
en-US: uptime
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: vm_uptime_milliseconds
i18n:
zh-CN: JVM 运行时间
en-US: JVM uptime
priority: 4
fields:
- field: metric_value
type: 0
unit: 'ms'
i18n:
zh-CN: 运行时间
en-US: uptime
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: system_cpu_load
i18n:
zh-CN: 系统负载
en-US: System load
priority: 5
fields:
- field: metric_value
type: 0
i18n:
zh-CN: value
en-US: value
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: vm_count
i18n:
zh-CN: JVM 线程总数
en-US: Total JVM threads
priority: 6
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 总数
en-US: total
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: vm_memory_heap_max
i18n:
zh-CN: JVM 可使用的最大内存限制
en-US: JVM max heap memory
priority: 7
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 大小
en-US: size
units:
- metric_value=B->MB
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: vm_memory_heap_used
i18n:
zh-CN: JVM 当前使用的内存大小
en-US: JVM heap memory used
priority: 8
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 大小
en-US: size
units:
- metric_value=B->MB
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: jenkins_project_count_value
i18n:
zh-CN: Jenkins 项目数量
en-US: Jenkins project count
priority: 9
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: jenkins_project_enabled_count_value
i18n:
zh-CN: 已启用的项目数量
en-US: Enabled project count
priority: 10
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: jenkins_queue_size_value
i18n:
zh-CN: 构建队列中的任务数量
en-US: Build queue size
priority: 11
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: jenkins_node_online_value
i18n:
zh-CN: 当前在线的构建节点数量
en-US: Online build nodes count
priority: 12
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: jenkins_job_count_value
i18n:
zh-CN: 作业数量
en-US: Job count
priority: 13
fields:
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_executors_available
i18n:
zh-CN: 可用的执行器数量
en-US: Available executors count
priority: 14
fields:
- field: label
type: 1
label: true
i18n:
zh-CN: 标签
en-US: label
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_executors_busy
i18n:
zh-CN: 忙碌的执行器数量
en-US: Busy executors count
priority: 15
fields:
- field: label
type: 1
label: true
i18n:
zh-CN: 标签
en-US: label
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_executors_connecting
i18n:
zh-CN: 正在连接的执行器数量
en-US: Connecting executors count
priority: 16
fields:
- field: label
type: 1
label: true
i18n:
zh-CN: 标签
en-US: label
- field: metric_value
type: 0
i18n:
zh-CN: 数量
en-US: quantity
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_duration_milliseconds_summary_sum
i18n:
zh-CN: 任务构建时长汇总(毫秒)
en-US: Job build duration sum (ms)
priority: 17
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
unit: 'ms'
i18n:
zh-CN: value
en-US: value
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_last_build_duration_milliseconds
i18n:
zh-CN: 最近一次构建的构建时间(毫秒)
en-US: Last build time (ms)
priority: 18
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
unit: 'ms'
i18n:
zh-CN: value
en-US: value
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_success_build_count_total
i18n:
zh-CN: 构建成功次数
en-US: Successful build count
priority: 19
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
i18n:
zh-CN: 总数
en-US: total
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_failed_build_count_total
i18n:
zh-CN: 构建失败次数
en-US: Failed build count
priority: 20
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
i18n:
zh-CN: 总数
en-US: total
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_unstable_build_count_total
i18n:
zh-CN: 不稳定构建次数
en-US: Unstable build count
priority: 21
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
i18n:
zh-CN: 总数
en-US: total
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_total_build_count_total
i18n:
zh-CN: 总构建次数(不包括未构建状态)
en-US: Total builds count (excluding not built)
priority: 22
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
i18n:
zh-CN: 总数
en-US: total
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
- name: default_jenkins_builds_last_build_result_ordinal
i18n:
zh-CN: 任务构建状态(最近一次构建)
en-US: Last build status
priority: 23
fields:
- field: jenkins_job
label: true
i18n:
zh-CN: 任务
en-US: job
type: 1
- field: repo
label: true
i18n:
zh-CN: 仓库
en-US: repo
type: 1
- field: buildable
label: true
i18n:
zh-CN: 是否可构建
en-US: buildable
type: 1
- field: metric_value
i18n:
zh-CN: 状态
en-US: status
type: 0
protocol: http
http:
host: ^_^host^_^
port: ^_^port^_^
url: /prometheus
method: GET
parseType: prometheus
@@ -21,6 +21,7 @@ import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
import org.apache.hertzbeat.manager.support.GlobalExceptionHandler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,6 +35,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.Set;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_CONFLICT_CODE;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
@@ -70,6 +72,7 @@ class MetricsFavoriteControllerTest {
@BeforeEach
void setUp() {
this.mockMvc = MockMvcBuilders.standaloneSetup(metricsFavoriteController)
.setControllerAdvice(new GlobalExceptionHandler())
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.build();
}
@@ -80,7 +83,7 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
doNothing().when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
@@ -99,15 +102,15 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
doThrow(new RuntimeException("Metrics favorite already exists: " + testMetricsName))
.when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
.andExpect(jsonPath("$.msg").value("Add failed! Metrics favorite already exists: " + testMetricsName));
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE))
.andExpect(jsonPath("$.msg").value("Metrics favorite already exists: " + testMetricsName));
verify(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
}
@@ -117,7 +120,7 @@ class MetricsFavoriteControllerTest {
void testAddMetricsFavoriteMissingParameters() throws Exception {
mockMvc.perform(post("/api/metrics/favorite")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
.andExpect(status().is5xxServerError());
verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString());
}
@@ -128,7 +131,7 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
// Given
doNothing().when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
@@ -149,15 +152,15 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
doThrow(new RuntimeException("Database error"))
.when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
.andExpect(jsonPath("$.msg").value("Remove failed! Database error"));
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE))
.andExpect(jsonPath("$.msg").value("Database error"));
verify(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
}
@@ -169,7 +172,7 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
Set<String> favoriteMetrics = Set.of("cpu", "memory", "disk");
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
.thenReturn(favoriteMetrics);
@@ -191,7 +194,7 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
Set<String> favoriteMetrics = Set.of();
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
.thenReturn(favoriteMetrics);
@@ -213,15 +216,15 @@ class MetricsFavoriteControllerTest {
when(subjectSum.getPrincipal()).thenReturn(testUserId);
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
.thenThrow(new RuntimeException("Service error"));
mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
.andExpect(jsonPath("$.msg").value("Failed to get favorited metrics!"));
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE))
.andExpect(jsonPath("$.msg").value("Service error"));
verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId);
}
@@ -231,7 +234,7 @@ class MetricsFavoriteControllerTest {
void testGetUserFavoritedMetricsMissingMonitorId() throws Exception {
mockMvc.perform(get("/api/metrics/favorite")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
.andExpect(status().is5xxServerError());
verify(metricsFavoriteService, never()).getUserFavoritedMetrics(anyString(), anyLong());
}
@@ -20,10 +20,9 @@ package org.apache.hertzbeat.manager.dao;
import jakarta.annotation.Resource;
import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
import org.apache.hertzbeat.manager.AbstractSpringIntegrationTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@@ -42,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Test case for {@link MetricsFavoriteDao}
*/
@Transactional
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
@Resource
@@ -80,11 +80,6 @@ class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
.build();
}
@AfterEach
void tearDown() {
metricsFavoriteDao.deleteAll();
}
@Test
void testSaveAndFindById() {
MetricsFavorite saved = metricsFavoriteDao.saveAndFlush(testFavorite1);
@@ -198,7 +193,6 @@ class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
}
@Test
@Rollback(false)
void testUniqueConstraint() {
metricsFavoriteDao.saveAndFlush(testFavorite1);
@@ -208,11 +202,8 @@ class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
.metricsName(testMetricsName1)
.createTime(LocalDateTime.now()).build();
assertThrows(java.sql.SQLException.class, () -> {
assertThrows(Throwable.class, () -> {
metricsFavoriteDao.saveAndFlush(duplicate);
});
// Clean up the test data
metricsFavoriteDao.deleteAll();
}
}
@@ -0,0 +1,123 @@
/*
* 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.manager.service;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.AbstractSpringIntegrationTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test case for {@link AlertDefineService}
*/
public class AlertDefineServiceIntegrationTest extends AbstractSpringIntegrationTest {
@Autowired
private AlertDefineService alertDefineService;
@Autowired
private AlertDefineDao alertDefineDao;
private List<Long> createdIds = new ArrayList<>();
@BeforeEach
void setUp() {
createdIds.clear();
}
@AfterEach
void tearDown() {
for (Long id : createdIds) {
if (alertDefineDao.existsById(id)) {
alertDefineDao.deleteById(id);
}
}
}
@Test
void testAddAlertDefine() {
AlertDefine alertDefine = AlertDefine.builder()
.name("test-cpu-alert")
.expr("usage > 80")
.times(3)
.type("periodic")
.enable(true)
.period(10)
.template("CPU usage is excessively high: ${usage}%")
.creator("integration-test")
.modifier("integration-test")
.gmtCreate(LocalDateTime.now())
.gmtUpdate(LocalDateTime.now())
.build();
assertNull(alertDefine.getId());
assertDoesNotThrow(() -> alertDefineService.addAlertDefine(alertDefine));
assertNotNull(alertDefine.getId());
assertTrue(alertDefine.getId() > 0);
createdIds.add(alertDefine.getId());
AlertDefine savedAlertDefine = alertDefineDao.findById(alertDefine.getId()).orElse(null);
assertNotNull(savedAlertDefine);
assertEquals("usage > 80", savedAlertDefine.getExpr());
assertEquals("integration-test", savedAlertDefine.getCreator());
}
@Test
void testModifyAlertDefine() {
AlertDefine alertDefine = AlertDefine.builder()
.name("test-cpu-alert")
.expr("usage > 80")
.times(3)
.type("periodic")
.enable(true)
.period(10)
.template("CPU usage is excessively high: ${usage}%")
.creator("integration-test")
.modifier("integration-test")
.gmtCreate(LocalDateTime.now())
.gmtUpdate(LocalDateTime.now())
.build();
assertNull(alertDefine.getId());
assertDoesNotThrow(() -> alertDefineService.modifyAlertDefine(alertDefine));
assertNotNull(alertDefine.getId());
assertTrue(alertDefine.getId() > 0);
createdIds.add(alertDefine.getId());
AlertDefine savedAlertDefine = alertDefineDao.findById(alertDefine.getId()).orElse(null);
assertNotNull(savedAlertDefine);
assertEquals("usage > 80", savedAlertDefine.getExpr());
assertEquals("integration-test", savedAlertDefine.getCreator());
}
}
@@ -125,6 +125,9 @@ class MonitorServiceTest {
@Mock
private ApplicationContext applicationContext;
@Mock
private MetricsFavoriteService metricsFavoriteService;
/**
* Properties cannot be directly mock, test execution before - manual assignment
*/
+80
View File
@@ -0,0 +1,80 @@
---
id: Jenkins
title: Monitoring Jenkins
sidebar_label: Jenkins
keywords: [ open-source, monitoring system, CI/CD, DevOps, Jenkins monitoring ]
---
> Monitoring of Jenkins' general metrics is performed by invoking the Jenkins Prometheus Plugin.
### Prerequisites
1. Set up Jenkins-related services according to
the [deployment documentation](https://www.jenkins.io/doc/book/installing/).
2. Requires installation of the [plugin](https://www.jenkins.io/doc/book/managing/plugins/) Exposed metric information
has been accessed; refer to the [prometheus-plugin](https://plugins.jenkins.io/prometheus/) for details.
3. The externally exposed metrics endpoint is ```<jenkin_url>/prometheus```. Verify whether you can access the metrics
data.
### Configuration parameters
| Parameter Name | Parameter Help Description |
|-------------------|-----------------------------------------------------------------------------------------------------------------------|
| Target Host | The monitored peer's IPv4, IPv6, or domain name. Note⚠️: Do not include protocol headers (e.g., https://, http://). |
| Port | Jenkins port value, default is 8080. |
| Task Name | The name identifying this monitoring task. The name must be unique. |
| Query Timeout | Set the connection timeout in milliseconds (ms). Default is 3000 ms. |
| Monitoring Cycle | The interval time for periodic data collection, measured in seconds. The minimum configurable interval is 30 seconds. |
| Binding Tags | Used for classifying and managing monitored resources |
| Description Notes | Additional identifiers and descriptive notes for this monitoring task. Users can add notes here. |
### Collection indicators
#### Indicator Set: System Information Indicators
| Indicator name | Unit | Metric Help Description |
|------------------------------|-------------|-----------------------------|
| default_jenkins_uptime | millisecond | Jenkins runtime |
| default_jenkins_up | None | Jenkins still running |
| default_jenkins_version_info | None | Jenkins Version Information |
| jenkins_health_check_score | None | Jenkins Health Check Score |
#### Metric Set: jvm
| Indicator name | Unit | Metric Help Description |
|------------------------|-------------|-------------------------------------------|
| vm_uptime_milliseconds | millisecond | JVM runtime |
| system_cpu_load | None | System Load |
| vm_count | None | Total Number of JVM Threads |
| vm_memory_heap_max | MB | Maximum memory limit available to the JVM |
| vm_memory_heap_used | MB | Current memory usage of the JVM |
#### Indicator set: Basic information indicators
| Indicator name | Unit | Metric Help Description |
|-------------------------------------|------|----------------------------------------|
| jenkins_project_count_value | None | Number of projects |
| jenkins_project_enabled_count_value | None | Number of enabled items |
| jenkins_queue_size_value | None | Number of tasks in the build queue |
| jenkins_node_online_value | None | Number of currently online build nodes |
#### Indicator Set: Actuator Information Indicators
| Indicator name | Unit | Metric Help Description |
|--------------------------------------|------|-----------------------------------------|
| default_jenkins_executors_available | None | Number of available actuators |
| default_jenkins_executors_busy | None | Number of active actuators |
| default_jenkins_executors_connecting | None | Number of actuators currently connected |
#### Indicator Set: Task Information Indicators
| Indicator name | Unit | Metric Help Description |
|----------------------------------------------------------|----------------------------------------------------------|---------------------------------------------------|
| jenkins_job_count_value | None | Number of assignments |
| default_jenkins_builds_duration_milliseconds_summary_sum | millisecond | Task Construction Duration Summary |
| default_jenkins_builds_last_build_duration_milliseconds | millisecond | Time of the most recent build |
| default_jenkins_builds_success_build_count_total | None | Number of successful builds |
| default_jenkins_builds_failed_build_count_total | None | Number of build failures |
| default_jenkins_builds_unstable_build_count_total | None | Number of unstable builds |
| default_jenkins_builds_total_build_count_total | None | Total number of builds (excluding unbuilt states) |
| default_jenkins_builds_last_build_result_ordinal | 0=Success, 1=Unstable, 2=Failure, 3=Not Built, 4=Aborted | Task Build Status (Last Build) |
+2 -2
View File
@@ -18,9 +18,9 @@ Use the `sureness.yml` provided by Sureness to configure user accounts, roles, A
## Monitoring Template Security
Apache HertzBeat™ provides monitoring template functions, and users can define monitoring rules by configuring custom scripts in the monitoring template.
Apache HertzBeat™ provides a monitoring template feature that allows users to define monitoring rules by configuring custom monitoring templates and custom scripts.
The script types include `SQL` `SHELL` `JMX` `URL` `API`, etc. When users customize scripts, they need to ensure the security of the custom scripts themselves to avoid malicious code in the scripts.
A monitoring template may include scripts and other content such as SQL, SHELL, JMX, URL, and API. When creating custom monitoring templates, users are responsible for ensuring the security of the template content and avoiding malicious code or other unsafe elements.
## Custom Plugin Security
@@ -0,0 +1,78 @@
---
id: Jenkins
title: 监控:Jenkins
sidebar_label: Jenkins
keywords: [ 开源监控系统, CI/CD, DevOps, Jenkins监控 ]
---
> 通过调用 Jenkins Prometheus Plugin 对 Jenkins 的通用指标进行采集监控。
### 前置条件
1. 按照[部署文档](https://www.jenkins.io/doc/book/installing/)搭建好Jenkins相关服务。
2. 需要安装[插件](https://www.jenkins.io/doc/book/managing/plugins/)
已用来访问暴露的指标信息,可参考[prometheus-plugin](https://plugins.jenkins.io/prometheus/)。
3. 对外暴露指标的地址是```<jenkin_url>/prometheus```,查看是否能访问到metrics数据。
## 配置参数
| 参数名称 | 参数帮助描述 |
|--------|------------------------------------------------------|
| 目标Host | 被监控的对端IPV4,IPV6或域名。注意⚠️不带协议头(eg: https://, http://)。 |
| 端口 | Jenkins Prot值,默认为8080。 |
| 任务名称 | 标识此监控的名称,名称需要保证唯一性。 |
| 查询超时时间 | 设置连接的超时时间,单位ms毫秒,默认3000毫秒。 |
| 监控周期 | 监控周期性采集数据间隔时间,单位秒,可设置的最小间隔为30秒 |
| 绑定标签 | 用于对监控资源进行分类管理 |
| 描述备注 | 更多标识和描述此监控的备注信息,用户可以在这里备注信息 |
### 采集指标
#### 指标集合:系统信息指标
| 指标名称 | 指标单位 | 指标帮助描述 |
|------------------------------|------|----------------|
| default_jenkins_uptime | 毫秒 | Jenkins 运行时间 |
| default_jenkins_up | 无 | Jenkins 是否存活 |
| default_jenkins_version_info | 无 | Jenkins 版本信息 |
| jenkins_health_check_score | 无 | Jenkins 健康检查分值 |
#### 指标集合:jvm
| 指标名称 | 指标单位 | 指标帮助描述 |
|------------------------|------|---------------|
| vm_uptime_milliseconds | 毫秒 | JVM 运行时间 |
| system_cpu_load | 无 | 系统负载 |
| vm_count | 无 | JVM 线程总数 |
| vm_memory_heap_max | MB | JVM可使用的最大内存限制 |
| vm_memory_heap_used | MB | JVM 当前使用的内存大小 |
#### 指标集合:基础信息指标
| 指标名称 | 指标单位 | 指标帮助描述 |
|-------------------------------------|------|-------------|
| jenkins_project_count_value | 无 | 项目数量 |
| jenkins_project_enabled_count_value | 无 | 已启用的项目数量 |
| jenkins_queue_size_value | 无 | 构建队列中的任务数量 |
| jenkins_node_online_value | 无 | 当前在线的构建节点数量 |
#### 指标集合:执行器信息指标
| 指标名称 | 指标单位 | 指标帮助描述 |
|--------------------------------------|------|------------|
| default_jenkins_executors_available | 无 | 可用的执行器数量 |
| default_jenkins_executors_busy | 无 | 忙碌的执行器数量 |
| default_jenkins_executors_connecting | 无 | 正在连接的执行器数量 |
#### 指标集合:任务信息指标
| 指标名称 | 指标单位 | 指标帮助描述 |
|----------------------------------------------------------|-----------------------------|-----------------|
| jenkins_job_count_value | 无 | 作业数量 |
| default_jenkins_builds_duration_milliseconds_summary_sum | 毫秒 | 任务构建时长汇总 |
| default_jenkins_builds_last_build_duration_milliseconds | 毫秒 | 最近一次构建的构建时间 |
| default_jenkins_builds_success_build_count_total | 无 | 构建成功次数 |
| default_jenkins_builds_failed_build_count_total | 无 | 构建失败次数 |
| default_jenkins_builds_unstable_build_count_total | 无 | 不稳定构建次数 |
| default_jenkins_builds_total_build_count_total | 无 | 总构建次数(不包括未构建状态) |
| default_jenkins_builds_last_build_result_ordinal | 0=成功,1=不稳定,2=失败,3=未构建,4=已中止 | 任务构建状态(最近一次构建) |
@@ -18,9 +18,9 @@ Apache HertzBeat™ 使用 [Sureness](https://github.com/dromara/sureness) 来
## 监控模板安全
Apache HertzBeat™ 提供了监控模板功能,用户可以通过配置监控模板里面的自定义脚本来定义监控规则。
Apache HertzBeat™ 提供了监控模板功能,用户可以通过配置自定义监控模板自定义脚本来定义监控规则。
脚本类型包含 `SQL` `SHELL` `JMX` `URL` `API` 等,当用户自定义脚本时需要自行保证自定义脚本的安全性,避免脚本中包含恶意代码等。
监控模版内包含 `SQL` `SHELL` `JMX` `URL` `API`脚本和其它内容,当用户自定义监控模版时需要自行保证模版内容的安全性,避免包含恶意代码等。
## 自定义插件安全
+12 -2
View File
@@ -271,7 +271,7 @@ export default function () {
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -295,7 +295,7 @@ export default function () {
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests"></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests"></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests"></a></td>
@@ -439,6 +439,16 @@ export default function () {
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests"></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests"></a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests"></a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a> <a href="#design-tuzuy" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+13
View File
@@ -0,0 +1,13 @@
# Introduction
This file has a short description of each mcp server in `mcp-servers` directory
# MCP servers
## mcp-bash-server
This mcp server provide the ability to execute shell command or ASCII text executable
script in the machine running this server.
Use a simple Oauth2.0 for authentication.
+2
View File
@@ -0,0 +1,2 @@
/target
logs/
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
[package]
name = "mcp-bash-server"
version = "0.1.0"
edition = "2024"
[dependencies]
tracing-appender = "0.2"
anyhow = "1.0.98"
rmcp = { version = "0.4.0", features = ["server", "transport-sse-server", "transport-io", "transport-streamable-http-server", "auth"]}
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
tokio-stream = { version = "0.1", features = ["net"] }
tokio-util = { version = "0.7.15", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"std",
"fmt",
] }
axum = { version = "0.8", features = ["macros"] }
chrono = "0.4"
tower-http = { version = "0.6", features = ["cors"] }
askama = { version = "0.14"}
rand = { version = "0.8", features = ["std"] }
uuid = { version = "1.6", features = ["v4", "serde"] }
serde_urlencoded = "0.7"
oauth2 = "5.0"
toml = "0.8"
regex = "1.11.1"
shlex = "1.3"
[dev-dependencies]
tokio-test = "0.4"
tempfile = "3.0"
tower = "0.5"
hyper = "1.0"
axum-test = "14.1"
[[bin]]
name = "mcp-bash-server"
path = "src/main.rs"
+97
View File
@@ -0,0 +1,97 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Multi-stage build for mcp-bash-server
# Stage 1: Build stage
FROM ubuntu:noble AS builder
# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV RUST_VERSION=stable
ENV PATH="/root/.cargo/bin:${PATH}"
# Set proxy environment variables via build arguments
ARG HTTPS_PROXY
ARG HTTP_PROXY
ENV https_proxy=${HTTPS_PROXY}
ENV http_proxy=${HTTP_PROXY}
# Install build dependencies
RUN apt-get update && apt-get install -y \
curl \
build-essential \
pkg-config \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${RUST_VERSION}
# Set working directory
WORKDIR /app
# Copy the mcp-bash-server project
COPY mcp-bash-server/ .
# Build the project in release mode
RUN cargo build --release
# Stage 2: Runtime stage
FROM ubuntu:noble AS runtime
# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
bash \
coreutils \
procps \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user for security
RUN useradd -m -s /bin/bash mcpuser
# Set working directory
WORKDIR /app
# Copy the compiled binary from builder stage
COPY --from=builder /app/target/release/mcp-bash-server /app/mcp-bash-server
# Copy configuration and templates
COPY --from=builder /app/config.toml /app/config.toml
COPY --from=builder /app/templates/ /app/templates/
# Create logs directory and set permissions
RUN mkdir -p logs && chown -R mcpuser:mcpuser /app
# Switch to non-root user
USER mcpuser
# Expose the port
EXPOSE 4000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:4000/ || exit 1
# Set the entrypoint to run the mcp-bash-server
CMD ["./mcp-bash-server"]
+333
View File
@@ -0,0 +1,333 @@
# mcp-bash-server
A HertzBeat MCP server for running scripts with security command blacklist and logging capabilities
## Dependencies
### Rust
If you need to deploy this MCP Server locally, you will need a Rust runtime environment.
Visit [rust-lang.org](https://www.rust-lang.org/tools/install) to learn how to install the Rust runtime environment.
Version `1.88.0` can absolutely work, and we recommend using the latest version of Rust.
## Deployment
### Local Deployment
If you want to run this MCP server locally using the default settings provided by the project, simply run the following command in the project root directory:
```Rust
cargo run
```
This MCP server will be deployed at `http://127.0.0.1:4000/mcp`, and you can use the `modelcontextprotocol/inspector` tool to connect to and use this MCP server.
For information on how to use the modelcontextprotocol/inspector tool, refer to the [inspector documentation](https://github.com/modelcontextprotocol/inspector).
If you encounter any issues while using Inspector, it is recommended to use version `v0.16.2`. Other versions may also work.
### Container Deployment
Using container deployment for this MCP Server is an excellent way to try out the tools provided by the server without polluting your machine, as all MCP Server operations are completed within the container.
Refer to the `Dockerfile` in the code repository and create your own Dockerfile. After creating it, run the following build command:
```shell
docker build -t apache/hertzbeat-mcp-bash-server:latest .
```
You can use the proxy in docker build
```shell
docker build --build-arg HTTPS_PROXY=<your https_proxy> --build-arg HTTP_PROXY=<your http_proxy> -t apache/hertzbeat-mcp-bash-server:latest .
```
After building, use the following command to run it:
```shell
docker run -d --name mcp-bash-server -p 4000:4000 --restart unless-stopped apache/hertzbeat-mcp-bash-server:latest
```
The MCP Server inside the container runs on 0.0.0.0:4000. On the host machine, use the inspector with URL `http://localhost:4000/mcp` to connect to the MCP Server inside the container.
#### Use custom config in container
Container's workdir is `/app` and it will run the `/app/mcp-bash-server` when it start, this program will read the `config.toml` at the same directory, so you can put the `config.toml` in the `/app` directory to cover the default config in image. Use the command below to do it.
```shell
docker run -d --name mcp-bash-server -p 4000:4000 -v `pwd`/config.toml:/app/config.toml --restart unless-stopped apache/hertzbeat-mcp-bash-server:latest
```
If you are using SELinux, you may need to run the command instead to let the container access the file in host.
```shell
docker run -d --name mcp-bash-server -p 4000:4000 -v `pwd`/config.toml:/app/config.toml:Z --restart unless-stopped apache/hertzbeat-mcp-bash-server:latest
```
To check if the config.toml is used, do this
```shell
docker logs mcp-bash-server
```
or check the config.toml in container
```shell
docker exec mcp-bash-server ls /app
docker exec mcp-bash-server cat /app/config.toml
```
## Use it by Agents
### Vscode Copilot
Start the MCP Server in daemon mode, then add the settings to your Vscode Copilot mcp config. Note that you can use modelcontextprotocol/inspector to finish the OAuth flow and get the access token, put it in config and restart server, then Copilot can connect to the server and use the tools.
```json
{
"servers": {
"bash-server": {
"url": "http://localhost:4000/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}
```
**Currently Vscode MCP OAuth can not automatically authorize this bash-server**
The vscode mcp OAuth flow is:
1. GET /.well-known/oauth-authorization-server
2. GET /authorize with query-params
3. ...
But we requires the client registration before accessing endpoint `/authorize` with query-params that contains invalid client-id. So we can only set the token manually now.
## Configuration
The `config.toml` file contains the settings for this MCP server. The configuration items currently supported by the configuration file are as follows:
```toml
# This is the configuration file for mcp-bash-server
[settings]
# Port for MCP Server deployment
port = 4000
# IP for MCP Server deployment
host = "127.0.0.1"
# Usage environment for MCP Server, can be either development or production environment.
# Set env to "development" or "production"; production uses OAuth 2.0.
env = "development"
[whitelist]
# Whitelist of allowed commands (exact match), a string list.
# Only commands where the complete command string exactly matches an item in this list will be allowed
commands = [
"echo hello",
"ls -la",
"pwd",
# Add your allowed commands here
]
# Whitelist of allowed command regex patterns, a list of regex expression strings
# Commands where the complete command string matches any of these regex patterns will be allowed
regex = [
'^echo [a-zA-Z0-9 ]+$',
'^ls [a-zA-Z0-9 /-]*$',
# Add your whitelist regex patterns here
]
[blacklist]
# Blacklist of forbidden commands (exact match), a string list
# Blacklist has higher priority than whitelist. If the complete command string exactly matches an item in this list, the entire command will be blocked, even if it would be allowed by the whitelist
# Note: Only exact matches are blocked. For example, if "rm" is blacklisted, only the exact command "rm" is blocked, not commands like "rm -rf /tmp/test"
commands = [
# Dangerous file operations
"rm -rf /",
"shutdown",
# Add your forbidden commands here
]
# Blacklist of forbidden command regex patterns, a list of regex expression strings
# Blacklist has higher priority than whitelist. If the complete command string matches any of these regex patterns, it will be blocked, even if it would be allowed by the whitelist
regex = [
# Block any command with dangerous operators
'.*[|&;`$()><].*',
# Block commands that try to write to system directories
'.*/etc/.*',
'.*/root/.*',
# Block commands with sudo or su
'^sudo .*',
'^su .*',
# Add your blacklist regex patterns here
]
```
## Testing
### Unit Tests
Run `cargo test` to execute all unit tests.
The unit tests include:
#### Configuration Tests (`config.rs`)
- **Config Parsing**: Tests parsing of TOML configuration files with various settings
- **Invalid File Handling**: Tests error handling for non-existent configuration files
- **Invalid TOML Handling**: Tests error handling for malformed TOML syntax
- **Whitelist Creation**: Tests creation and validation of command whitelists
- **Blacklist Creation**: Tests creation and validation of command blacklists
- **Settings Creation**: Tests creation of server settings with port, host, and environment configurations
#### Validator Tests (`validator.rs`)
- **Validator Creation**: Tests creation of command validators with blacklist and whitelist rules (both commands and regex)
- **Whitelisted Command Validation**: Tests allowing commands that exactly match whitelist entries
- **Whitelisted Regex Validation**: Tests allowing commands that match whitelist regex patterns
- **Blacklisted Command Blocking**: Tests blocking of commands that exactly match blacklist entries (complete command string)
- **Blacklisted Regex Blocking**: Tests blocking of commands matching blacklist regex patterns
- **Priority Testing**: Tests that blacklist has higher priority than whitelist
- **Exact vs Partial Match Testing**: Tests that blacklist only blocks exact command matches, not partial matches
- **Default Deny Behavior**: Tests that commands not in whitelist are denied by default
- **Empty Command Handling**: Tests that empty commands are denied by default
- **Case Sensitivity Testing**: Tests case-sensitive command matching behavior
- **Invalid Regex Handling**: Tests error handling for invalid regex patterns
- **Complex Command Testing**: Tests validation of multi-argument commands
- **Nested Shell Command Testing**: Tests validation of commands within shell invocations
#### Bash Server Tests (`bash_server.rs`)
- **Response Success/Failure**: Tests command execution response handling for both successful and failed commands
- **Command Stringification**: Tests conversion of Command objects to readable string representations
- **Command Execution with Timeout**: Tests command execution with timeout protection
- **Server Creation**: Tests BashServer instantiation with and without configuration
- **Request/Response Serialization**: Tests JSON serialization and deserialization of command requests and responses
- **Environment Variable Handling**: Tests setting and using environment variables in commands
- **Working Directory Handling**: Tests command execution in specific working directories
- **Debug and Clone Traits**: Tests that BashServer properly implements Debug and Clone traits
- **Timeout Behavior**: Tests that commands properly timeout when exceeding time limits
- **Unix script and Python Execution**: Tests tool `execute_script` and `execute_python`
### Manual Testing with Inspector
Use the official MCP debugging tool modelcontextprotocol/inspector. For usage instructions, refer to the [inspector documentation](https://github.com/modelcontextprotocol/inspector).
If you deploy the MCP Server locally using the default method, run the inspector debugging tool after the server starts.
```shell
npx @modelcontextprotocol/inspector
```
Then set the connection method to `Streamable HTTP` and set the URL to `http://127.0.0.1:4000/mcp`.
### Using OAuth in Inspector (Optional)
If your running mode is `development`, you don't need to go through `Open Auth Settings` and can directly click Connect to connect to the MCP Server. If your running mode is `production`, you need to complete the `Open Auth Settings` verification.
The method for using OAuth verification and connection is as follows:
1. Click `Open Auth Settings`
2. Click `Quick OAuth Flow`
3. Click `Approve` on the pop-up webpage
4. Return to the MCP inspector, click on the Access Tokens under `Authentication Complete` in `OAuth Flow Progress`. Copy the `access_token` from there
5. Click `Authentication`, paste the previously copied token into the `Bearer Token` field, then click Connect
After connecting, you can test the various tools provided by the MCP Server in the inspector.
## Tools
The MCP Server currently provides the following tools:
### Command Execution Tools
1. **all_execute_via_default_shell**
- Description: Execute commands using the default shell on all types of operating systems
- Parameters:
- `command`: The bash command or script to execute
- `working_dir`: Working directory for the command (optional)
- `env_vars`: Environment variables (optional)
- `timeout_seconds`: Timeout in seconds (default: 30)
- Returns: Execution result with stdout, stderr, exit code, and success status
2. **execute_python**
- Description: Execute a Python script
- Parameters: Same as above, but command should be Python code
- Returns: Python script execution result
3. **execute_script**
- Description: Execute a Unix script by writing it to a temporary file
- Parameters: Same as above, command should be shell script content
- Returns: Script execution result
### System Information Tools
1. **unix_get_system_info_via_default_shell**
- Description: Get comprehensive system information including OS, kernel, memory, and disk usage
- Parameters: None
- Returns: Detailed system information including hostname, OS, kernel, architecture, uptime, memory usage, and disk usage
2. **unix_preset_get_system_info_via_default_shell**
- Description: Get formatted system information in a structured format
- Parameters: None
- Returns: Version, hostname, and uptime in a single line format
3. **unix_get_available_shell**
- Description: Get the available shells on Unix-like operating systems
- Parameters: None
- Returns: List of available shells from /etc/shells
### Performance Monitoring Tools
1. **unix_preset_get_nic_info_via_default_shell**
- Description: Get network interface statistics including receive and transmit bytes
- Parameters: None
- Returns: Interface names and traffic data in tabular format
2. **unix_preset_get_cpu_info_via_default_shell**
- Description: Get detailed CPU information including model, core count, load averages, and performance metrics
- Parameters: None
- Returns: CPU model, core count, load averages, idle percentage, and other performance metrics
3. **unix_preset_get_disk_free_info_via_default_shell**
- Description: Get disk usage information for all mounted filesystems
- Parameters: None
- Returns: Filesystem usage data with columns for filesystem, used, available, usage percentage, and mount point
4. **unix_preset_get_top10_cpu_processes_via_default_shell**
- Description: Get top 10 processes consuming the most CPU
- Parameters: None
- Returns: Process information sorted by CPU usage in descending order
5. **unix_preset_get_top10_mem_processes_via_default_shell**
- Description: Get top 10 processes consuming the most memory
- Parameters: None
- Returns: Process information sorted by memory usage in descending order
### Security Features
All command execution tools support comprehensive security validation:
- **Default Deny Policy**: All commands are denied by default unless explicitly allowed
- **Blacklist Priority**: Blacklist rules have higher priority than whitelist rules
- **Dual Validation Modes**: Both exact string matching and regex pattern matching for blacklist and whitelist
- **Command Validation Logic**:
1. Empty commands are denied by default
2. If the **complete command string** exactly matches any blacklist command entry, deny immediately
3. If the **complete command string** matches any blacklist regex pattern, deny immediately
4. If command doesn't match blacklist and the **complete command string** exactly matches any whitelist command entry, allow
5. If command doesn't match blacklist and the **complete command string** matches any whitelist regex pattern, allow
6. Otherwise, deny by default
- **Important**: Blacklist validation checks the **entire command string**, not individual arguments. For example, if `"rm"` is blacklisted, only the exact command `"rm"` is blocked, not commands like `"rm -rf /tmp/test"` (which would be evaluated separately against the whitelist)
- **Timeout Protection**: Commands have configurable timeout limits to prevent hanging
- **Environment Isolation**: Optional working directory and environment variable settings
- **Logging**: All command executions and validation results are logged for audit purposes
- **Nested Shell Protection**: Validates commands within shell invocations (e.g., `bash -c "command"`)
### Tool Categories
- **General Execution**: Tools 1-3 for executing arbitrary commands, Python scripts, and shell scripts
- **System Monitoring**: Tools 1-3 for gathering basic system information
- **Performance Analysis**: Tools 1-5 for detailed performance monitoring and process analysis
+68
View File
@@ -0,0 +1,68 @@
# 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.
# This is the configuration file for mcp-bash-server
[settings]
# Port for MCP Server deployment
port = 4000
# IP for MCP Server deployment
host = "127.0.0.1"
# Usage environment for MCP Server, can be development or production environment.
# Set env to "development" or "production", production uses oauth2.0.
env = "production"
[whitelist]
# Whitelist of allowed commands (exact match), a string list.
# Only commands where the complete command string exactly matches an item in this list will be allowed
commands = [
"echo hello",
"ls -la",
"pwd",
# Add your allowed commands here
]
# Whitelist of allowed command regex patterns, a regex expression string list
# Commands where the complete command string matches any of these regex patterns will be allowed
regex = [
'^echo [a-zA-Z0-9 ]+$',
'^ls [a-zA-Z0-9 /-]*$',
# Add your whitelist regex patterns here
]
[blacklist]
# Blacklist of forbidden commands (exact match), a string list
# Blacklist has higher priority than whitelist. If the complete command string exactly matches an item in this list, the entire command will be blocked, even if it would be allowed by the whitelist
# Note: Only exact matches are blocked. For example, if "rm" is blacklisted, only the exact command "rm" is blocked, not commands like "rm -rf /tmp/test"
commands = [
# Dangerous file operations
"rm -rf /",
"shutdown",
# Add your forbidden commands here
]
# Blacklist of forbidden command regex patterns, a regex expression string list
# Blacklist has higher priority than whitelist. If the complete command string matches any of these regex patterns, it will be blocked, even if it would be allowed by the whitelist
regex = [
# Block any command with dangerous operators
'.*[|&;`$()><].*',
# Block commands that try to write to system directories
'.*/etc/.*',
'.*/root/.*',
# Block commands with sudo or su
'^sudo .*',
'^su .*',
# Add your blacklist regex patterns here
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,190 @@
/*
* 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 management for the MCP Bash Server
//!
//! This module handles reading and parsing configuration from TOML files,
//! including server settings and security blacklists for command validation.
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub settings: Settings,
pub blacklist: Blacklist,
pub whitelist: Whitelist,
}
/// Security whitelist configuration for command validation
/// Contains lists of allowed commands and operations
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Whitelist {
/// List of command names that are allowed to be executed
pub commands: Vec<String>,
/// List of regular expressions for commands that are allowed
/// These patterns are matched against the full command line
pub regex: Vec<String>,
}
/// Security blacklist configuration for command validation
/// Contains lists of forbidden commands and regex patterns
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Blacklist {
/// List of command names that are not allowed to be executed
pub commands: Vec<String>,
/// List of regular expressions for commands that are not allowed
/// These patterns are matched against the full command line
pub regex: Vec<String>,
}
/// Server runtime settings including network configuration
#[derive(Debug, Deserialize, Serialize)]
pub struct Settings {
pub port: u16,
pub host: String,
pub env: Option<String>, // "development" or "production"
}
impl Config {
/// Read and parse configuration from a TOML file
/// Returns parsed Config structure or error if file cannot be read/parsed
pub fn read_config(file: &str) -> Result<Config> {
let toml_str = fs::read_to_string(file)?;
let config: Config = toml::from_str(&toml_str)?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_config_parsing() {
let config_content = r#"
[settings]
port = 4000
host = "127.0.0.1"
env = "development"
[whitelist]
commands = ["echo hello", "ls -la"]
regex = ["echo.*", "ls.*"]
[blacklist]
commands = ["rm", "dd"]
regex = [".*[|&].*", "^sudo .*"]
"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{config_content}").unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let config = Config::read_config(temp_path).unwrap();
assert_eq!(config.settings.port, 4000);
assert_eq!(config.settings.host, "127.0.0.1");
assert_eq!(config.settings.env, Some("development".to_string()));
assert_eq!(config.whitelist.commands.len(), 2);
assert!(
config
.whitelist
.commands
.contains(&"echo hello".to_string())
);
assert!(config.whitelist.commands.contains(&"ls -la".to_string()));
assert_eq!(config.whitelist.regex.len(), 2);
assert!(config.whitelist.regex.contains(&"echo.*".to_string()));
assert!(config.whitelist.regex.contains(&"ls.*".to_string()));
assert_eq!(config.blacklist.commands.len(), 2);
assert!(config.blacklist.commands.contains(&"rm".to_string()));
assert!(config.blacklist.commands.contains(&"dd".to_string()));
assert_eq!(config.blacklist.regex.len(), 2);
assert!(config.blacklist.regex.contains(&".*[|&].*".to_string()));
assert!(config.blacklist.regex.contains(&"^sudo .*".to_string()));
}
#[test]
fn test_config_invalid_file() {
let result = Config::read_config("non_existent_file.toml");
assert!(result.is_err());
}
#[test]
fn test_config_invalid_toml() {
let invalid_config = r#"
[settings
port = 4000
"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{invalid_config}").unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let result = Config::read_config(temp_path);
assert!(result.is_err());
}
#[test]
fn test_whitelist_creation() {
let commands = vec!["echo".to_string(), "ls".to_string()];
let regex = vec!["test.*".to_string()];
let whitelist = Whitelist {
commands: commands.clone(),
regex: regex.clone(),
};
assert_eq!(whitelist.commands, commands);
assert_eq!(whitelist.regex, regex);
}
#[test]
fn test_blacklist_creation() {
let commands = vec!["rm".to_string(), "dd".to_string()];
let regex = vec![".*[|&].*".to_string(), "^sudo .*".to_string()];
let blacklist = Blacklist {
commands: commands.clone(),
regex: regex.clone(),
};
assert_eq!(blacklist.commands, commands);
assert_eq!(blacklist.regex, regex);
}
#[test]
fn test_settings_creation() {
let settings = Settings {
port: 8080,
host: "localhost".to_string(),
env: Some("production".to_string()),
};
assert_eq!(settings.port, 8080);
assert_eq!(settings.host, "localhost");
assert_eq!(settings.env, Some("production".to_string()));
}
}
@@ -0,0 +1,29 @@
/*
* 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.
*/
//! Common utilities and shared components for the MCP Bash Server
//!
//! This module contains the core functionality including:
//! - Bash command execution server implementation
//! - Configuration management
//! - OAuth2 authentication system
//! - Command validation and security enforcement
pub mod bash_server;
pub mod config;
pub mod oauth;
pub mod validator;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,410 @@
/*
* 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.
*/
//! Command validation module for security enforcement
//!
//! This module provides command validation functionality to prevent execution
//! of dangerous commands and operations based on configurable blacklists.
use tracing::debug;
use crate::{common::config::Whitelist, config::Blacklist};
use rmcp::model::ErrorData;
use tracing::error;
/// Command validator that checks commands against security blacklists
/// Prevents execution of dangerous commands and operations
#[derive(Debug, Clone)]
pub struct Validator {
/// Security blacklist configuration containing forbidden commands and operations
blacklist: Blacklist,
/// Security whitelist configuration containing secure commands
whitelist: Whitelist,
}
impl Validator {
/// Create a new validator with the specified blacklist configuration
pub fn new(blacklist: Blacklist, whitelist: Whitelist) -> Self {
Validator {
blacklist,
whitelist,
}
}
/// Validate a command against the security blacklist and whitelist
/// Returns Ok(()) if command is safe, Err(ErrorData) if command is blocked
///
/// Security Logic:
/// 1. All commands are denied by default
/// 2. Blacklist has higher priority than whitelist
/// 3. If command matches any blacklist pattern (exact or regex), deny
/// 4. If command doesn't match blacklist and matches whitelist (exact or regex), allow
/// 5. Otherwise, deny
pub fn is_unsafe_command(&self, args: &str) -> Result<(), ErrorData> {
// Handle empty command - deny by default
if args.is_empty() {
return Err(ErrorData::invalid_request(
"Empty command not allowed".to_string(),
None,
));
}
let full_cmd = args.to_string();
debug!("Validating command: {}", full_cmd);
// First check blacklist - if any match, deny immediately
// Check blacklist exact commands - match against the full command
for blacklisted_cmd in &self.blacklist.commands {
if &full_cmd == blacklisted_cmd {
error!(
"Command blocked by blacklist exact match: {}, full command: {}",
blacklisted_cmd, full_cmd
);
return Err(ErrorData::invalid_request(
format!("Command blocked by blacklist: {blacklisted_cmd}"),
None,
));
}
}
// Check blacklist regex patterns
for pattern in &self.blacklist.regex {
match regex::Regex::new(pattern) {
Ok(regex) => {
if regex.is_match(&full_cmd) {
error!(
"Command blocked by blacklist regex pattern: {}, full command: {}",
pattern, full_cmd
);
return Err(ErrorData::invalid_request(
format!("Command blocked by blacklist pattern: {pattern}"),
None,
));
}
}
Err(e) => {
error!(
"Invalid regex pattern in blacklist: {}, error: {}",
pattern, e
);
}
}
}
// Now check whitelist - if any match, allow
// Check whitelist exact commands
if self.whitelist.commands.contains(&full_cmd) {
debug!("Command allowed by whitelist exact match: {}", full_cmd);
return Ok(());
}
// Check whitelist regex patterns
for pattern in &self.whitelist.regex {
match regex::Regex::new(pattern) {
Ok(regex) => {
if regex.is_match(&full_cmd) {
debug!("Command allowed by whitelist regex pattern: {}", pattern);
return Ok(());
}
}
Err(e) => {
error!(
"Invalid regex pattern in whitelist: {}, error: {}",
pattern, e
);
}
}
}
// Default deny - command didn't match whitelist
error!("Command denied - not in whitelist: {full_cmd}");
Err(ErrorData::invalid_request(
format!("Command not allowed: {full_cmd}"),
None,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_validator() -> Validator {
let blacklist = Blacklist {
commands: vec![
"rm".to_string(),
"dd".to_string(),
"shutdown".to_string(),
"kill".to_string(),
],
regex: vec![
".*[|&;><].*".to_string(), // Block commands with dangerous operators
"^sudo .*".to_string(), // Block sudo commands
".*/etc/.*".to_string(), // Block access to /etc
".*passwd.*".to_string(), // Block password-related commands
],
};
let whitelist = Whitelist {
commands: vec![
"echo hello".to_string(),
"ls -la".to_string(),
"pwd".to_string(),
],
regex: vec![
"^echo [a-zA-Z0-9 ]+$".to_string(), // Only simple echo commands
"^ls [a-zA-Z0-9 /-]*$".to_string(), // Only simple ls commands
"^(pwd|whoami|date|uptime)$".to_string(), // Basic system info commands
],
};
Validator::new(blacklist, whitelist)
}
#[test]
fn test_validator_creation() {
let validator = create_test_validator();
assert_eq!(validator.blacklist.commands.len(), 4);
assert_eq!(validator.blacklist.regex.len(), 4);
assert_eq!(validator.whitelist.commands.len(), 3);
assert_eq!(validator.whitelist.regex.len(), 3);
}
#[test]
fn test_safe_command() {
let validator = create_test_validator();
let result = validator.is_unsafe_command("pwd");
assert!(result.is_ok()); // pwd is whitelisted
}
#[test]
fn test_whitelisted_command() {
let validator = create_test_validator();
let result = validator.is_unsafe_command("echo hello");
assert!(result.is_ok()); // "echo hello" is exactly whitelisted
}
#[test]
fn test_whitelisted_regex_command() {
let validator = create_test_validator();
let result = validator.is_unsafe_command("echo test123");
assert!(result.is_ok()); // Matches whitelist regex "^echo [a-zA-Z0-9 ]+$"
}
#[test]
fn test_blacklisted_command() {
let validator = create_test_validator();
// Test a command that contains a blacklisted command but isn't exact match
let result = validator.is_unsafe_command("rm -rf /");
// Should be denied because "rm -rf /" is not in whitelist (default deny)
// not because "rm" is blacklisted (since we need exact command match)
assert!(result.is_err());
}
#[test]
fn test_blacklisted_exact_command() {
let validator = create_test_validator();
// Test exact match against blacklist
let result = validator.is_unsafe_command("rm");
assert!(result.is_err()); // "rm" exactly matches blacklist
}
#[test]
fn test_blacklisted_regex_operation() {
let validator = create_test_validator();
// Test pipe operation which should be blocked by regex
let result = validator.is_unsafe_command("echo test | cat");
assert!(result.is_err()); // Contains "|" which matches ".*[|&;><].*" regex
}
#[test]
fn test_multiple_blacklisted_commands() {
let validator = create_test_validator();
// Test commands that are exactly in the blacklist
let dangerous_commands = vec!["rm", "dd", "shutdown", "kill"];
for cmd in dangerous_commands {
let result = validator.is_unsafe_command(cmd);
assert!(result.is_err(), "Command '{cmd}' should be blocked");
}
}
#[test]
fn test_multiple_blacklisted_regex_operations() {
let validator = create_test_validator();
let dangerous_ops = vec!["|", "&", ";", ">"];
for op in dangerous_ops {
let cmd = format!("echo test {op}");
let result = validator.is_unsafe_command(&cmd);
assert!(
result.is_err(),
"Operation '{op}' should be blocked by regex"
);
}
}
#[test]
fn test_empty_command() {
let validator = create_test_validator();
let result = validator.is_unsafe_command("");
assert!(result.is_err()); // Empty commands are now denied by default
}
#[test]
fn test_complex_safe_command() {
let validator = create_test_validator();
let result = validator.is_unsafe_command("find /tmp -name *.txt");
assert!(result.is_err()); // Not in whitelist, so denied by default
}
#[test]
fn test_invalid_regex_in_whitelist() {
let blacklist = Blacklist {
commands: vec![],
regex: vec![],
};
let whitelist = Whitelist {
commands: vec![],
regex: vec!["[invalid_regex".to_string()], // Invalid regex
};
let validator = Validator::new(blacklist, whitelist);
let result = validator.is_unsafe_command("test");
// Should be denied because command is not in whitelist and default is deny
assert!(result.is_err());
}
#[test]
fn test_case_sensitivity() {
let validator = create_test_validator();
// Test uppercase version of blacklisted command
let result = validator.is_unsafe_command("RM");
// Should be denied because not in whitelist (default deny)
assert!(result.is_err());
// Test exact case match for blacklisted command
let result = validator.is_unsafe_command("rm");
assert!(result.is_err()); // Should be blocked by blacklist
}
#[test]
fn test_partial_command_match() {
let validator = create_test_validator();
// Test command that contains blacklisted word but isn't exact match
let result = validator.is_unsafe_command("remove"); // contains "rm" but shouldn't match
assert!(result.is_err()); // Should be denied because not in whitelist (default deny)
}
#[test]
fn test_blacklist_priority_over_whitelist() {
// Test that blacklist has higher priority than whitelist
let blacklist = Blacklist {
commands: vec!["echo hello".to_string()], // Changed to exact match
regex: vec![],
};
let whitelist = Whitelist {
commands: vec!["echo hello".to_string()],
regex: vec!["^echo .*".to_string()],
};
let validator = Validator::new(blacklist, whitelist);
let result = validator.is_unsafe_command("echo hello");
assert!(result.is_err()); // Should be blocked because "echo hello" is in blacklist
}
#[test]
fn test_blacklist_regex_priority() {
// Test that blacklist regex has higher priority than whitelist
let blacklist = Blacklist {
commands: vec![],
regex: vec![".*sudo.*".to_string()],
};
let whitelist = Whitelist {
commands: vec!["sudo ls".to_string()],
regex: vec!["^sudo .*".to_string()],
};
let validator = Validator::new(blacklist, whitelist);
let result = validator.is_unsafe_command("sudo ls");
assert!(result.is_err()); // Should be blocked by blacklist regex
}
#[test]
fn test_default_deny_behavior() {
// Test that commands not in whitelist are denied by default
let blacklist = Blacklist {
commands: vec![],
regex: vec![],
};
let whitelist = Whitelist {
commands: vec!["pwd".to_string()],
regex: vec![],
};
let validator = Validator::new(blacklist, whitelist);
// Command in whitelist should be allowed
let result = validator.is_unsafe_command("pwd");
assert!(result.is_ok());
// Command not in whitelist should be denied
let result = validator.is_unsafe_command("ls");
assert!(result.is_err());
}
#[test]
fn test_exact_vs_partial_blacklist_matching() {
// Test the fix: blacklist should match exactly, not partially
let blacklist = Blacklist {
commands: vec!["rm".to_string()], // Only "rm" exactly
regex: vec![],
};
let whitelist = Whitelist {
commands: vec!["rm -rf /tmp/test".to_string()], // Allow this specific command
regex: vec![],
};
let validator = Validator::new(blacklist, whitelist);
// "rm" exactly should be blocked by blacklist
let result = validator.is_unsafe_command("rm");
assert!(
result.is_err(),
"Exact match 'rm' should be blocked by blacklist"
);
// "rm -rf /tmp/test" should be allowed by whitelist (not blocked by blacklist)
let result = validator.is_unsafe_command("rm -rf /tmp/test");
assert!(
result.is_ok(),
"'rm -rf /tmp/test' should be allowed by whitelist"
);
// "rm -rf /" should be denied (not in whitelist, not exact blacklist match)
let result = validator.is_unsafe_command("rm -rf /");
assert!(result.is_err(), "'rm -rf /' should be denied by default");
}
}
@@ -0,0 +1,82 @@
<!--
~ 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.
-->
<!DOCTYPE html>
<html>
<head>
<title>MCP OAuth Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px auto; max-width: 800px; line-height: 1.6; }
h1, h2 { color: #333; }
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; }
.endpoint { background: #f9f9f9; padding: 15px; border-radius: 5px; margin-bottom: 15px; }
.flow { background: #e8f5e9; padding: 15px; border-radius: 5px; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>MCP OAuth Server</h1>
<p>This is an MCP server with OAuth 2.0 integration to a third-party authorization server.</p>
<h2>Available Endpoints:</h2>
<div class="endpoint">
<h3>Authorization Endpoint</h3>
<p><code>GET /authorize</code></p>
<p>Parameters:</p>
<ul>
<li><code>response_type</code> - Must be "code"</li>
<li><code>client_id</code> - Client identifier (e.g., "mcp-client")</li>
<li><code>redirect_uri</code> - URI to redirect after authorization</li>
<li><code>scope</code> - Optional requested scope</li>
<li><code>state</code> - Optional state value for CSRF prevention</li>
</ul>
</div>
<div class="endpoint">
<h3>Token Endpoint</h3>
<p><code>POST /token</code></p>
<p>Parameters:</p>
<ul>
<li><code>grant_type</code> - Must be "authorization_code"</li>
<li><code>code</code> - The authorization code</li>
<li><code>client_id</code> - Client identifier</li>
<li><code>client_secret</code> - Client secret</li>
<li><code>redirect_uri</code> - Redirect URI used in authorization request</li>
</ul>
</div>
<div class="endpoint">
<h3>MCP streamablehttp Endpoints</h3>
<p><code>/mcp</code> - Streamablehttp connection endpoint (requires OAuth token)</p>
</div>
<div class="flow">
<h2>OAuth Flow:</h2>
<ol>
<li>MCP Client initiates OAuth flow with this MCP Server</li>
<li>MCP Server redirects to Third-Party OAuth Server</li>
<li>User authenticates with Third-Party Server</li>
<li>Third-Party Server redirects back to MCP Server with auth code</li>
<li>MCP Server exchanges the code for a third-party access token</li>
<li>MCP Server generates its own token bound to the third-party session</li>
<li>MCP Server completes the OAuth flow with the MCP Client</li>
</ol>
</div>
</body>
</html>
+618
View File
@@ -0,0 +1,618 @@
/*
* 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.
*/
//! MCP Bash Server - A Model Context Protocol server for executing bash commands
//!
//! This server provides secure bash command execution capabilities through the MCP protocol.
//! It includes OAuth2 authentication, command validation, and cross-platform support.
//!
//! Features:
//! - Secure command execution with blacklist validation
//! - OAuth2 authentication for client authorization
//! - Cross-platform shell support (Linux, Windows, macOS)
//! - Built-in system information tools
//! - Configurable timeout and environment settings
use std::sync::OnceLock;
use std::{net::SocketAddr, sync::Arc};
use anyhow::Result;
use axum::{
Router,
body::Body,
http::{HeaderMap, Request},
middleware::{self, Next},
response::{Html, IntoResponse, Response},
routing::{get, post},
};
use rmcp::transport::streamable_http_server::{
StreamableHttpService, session::local::LocalSessionManager,
};
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// Import modules
mod common;
use common::bash_server::BashServer;
use common::config;
use common::oauth::{
McpOAuthStore, oauth_approve, oauth_authorization_server, oauth_authorize, oauth_register,
oauth_token, validate_token_middleware,
};
const INDEX_HTML: &str = include_str!("html/mcp_oauth_index.html");
/// Global storage for server bind address, initialized once at startup
// Init once from environment variable BIND_ADDRESS
pub static BIND_ADDRESS: OnceLock<String> = OnceLock::new();
/// Root path handler
/// Serves the main OAuth authorization index page
async fn index() -> Html<&'static str> {
Html(INDEX_HTML)
}
/// Wrapper function for oauth_authorization_server to handle BIND_ADDRESS
async fn oauth_authorization_server_handler(headers: HeaderMap) -> impl IntoResponse {
let bind_address = BIND_ADDRESS
.get()
.expect("BIND_ADDRESS must be initialized in main()");
oauth_authorization_server(bind_address, headers).await
}
/// HTTP request logging middleware
/// Logs all incoming requests including method, URI, headers and response status
async fn log_request(request: Request<Body>, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let version = request.version();
// Log headers
let headers = request.headers().clone();
let mut header_log = String::new();
for (key, value) in headers.iter() {
let value_str = value.to_str().unwrap_or("<binary>");
header_log.push_str(&format!("\n {key}: {value_str}"));
}
// Try to get request body for form submissions
let content_type = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let request_info = if content_type.contains("application/x-www-form-urlencoded")
|| content_type.contains("application/json")
{
format!("{method} {uri} {version:?}{header_log}\nContent-Type: {content_type}")
} else {
format!("{method} {uri} {version:?}{header_log}")
};
info!("REQUEST: {}", request_info);
// Call the actual handler
let response = next.run(request).await;
// Log response status
let status = response.status();
info!("RESPONSE: {} for {} {}", status, method, uri);
response
}
/// Main application entry point
/// Sets up logging, OAuth store, HTTP server, and starts the MCP bash server
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
let logs = tracing_appender::rolling::daily("logs", "mcp.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(logs);
let log_setting = tracing_subscriber::fmt::layer().with_writer(non_blocking);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "debug".to_string().into()),
)
.with(tracing_subscriber::fmt::layer())
.with(log_setting)
.init();
// Read environment mode from config file, default to "production"
let config = config::Config::read_config("config.toml")?;
let env_mode = config
.settings
.env
.clone()
.unwrap_or_else(|| "production".to_string());
let is_dev = env_mode == "development";
// Create the OAuth store
let oauth_store = Arc::new(McpOAuthStore::new());
let host = config.settings.host.clone();
let port = config.settings.port;
let bind_address = format!("{host}:{port}");
let addr = bind_address.parse::<SocketAddr>()?;
let _ = BIND_ADDRESS.set(bind_address);
// Create StreamableHttpServer
let service = StreamableHttpService::new(
|| Ok(BashServer::new()),
LocalSessionManager::default().into(),
Default::default(),
);
let server_router = Router::new().nest_service("/mcp", service);
// Add OAuth authentication middleware only if not in development mode
let protected_server_router = if is_dev {
server_router
} else {
server_router.layer(middleware::from_fn_with_state(
oauth_store.clone(),
validate_token_middleware,
))
};
// Create CORS layer for the oauth authorization server endpoint
let cors_layer = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
// Create a sub-router for the oauth authorization server endpoint with CORS
let oauth_server_router = Router::new()
.route(
"/.well-known/oauth-authorization-server",
get(oauth_authorization_server_handler).options(oauth_authorization_server_handler),
)
.route("/token", post(oauth_token).options(oauth_token))
.route("/register", post(oauth_register).options(oauth_register))
.layer(cors_layer)
.with_state(oauth_store.clone());
// Create HTTP router with request logging middleware
let app = Router::new()
.route("/", get(index))
.route("/authorize", get(oauth_authorize))
.route("/approve", post(oauth_approve))
.merge(oauth_server_router) // Merge the CORS-enabled oauth server router
.merge(protected_server_router)
.with_state(oauth_store.clone())
.layer(middleware::from_fn(log_request));
// Start HTTP server
info!("MCP OAuth Server started on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
let _ = axum::serve(listener, app)
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() })
.await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Method;
use axum::http::Request;
#[tokio::test]
async fn test_index_handler() {
let response = index().await;
let html_content = response.0;
// Verify it returns the expected HTML content
assert_eq!(html_content, INDEX_HTML);
assert!(html_content.contains("OAuth"));
}
#[tokio::test]
async fn test_oauth_authorization_server_handler() {
use axum::http::HeaderMap;
// Set up BIND_ADDRESS for testing
let _ = BIND_ADDRESS.set("localhost:8080".to_string());
let mut headers = HeaderMap::new();
headers.insert("host", "localhost:8080".parse().unwrap());
let response = oauth_authorization_server_handler(headers).await;
// Test that the handler returns a response
// We can't easily test the exact content without mocking, but we can verify it doesn't panic
let _response_body = response.into_response();
}
#[test]
fn test_bind_address_initialization() {
// Create a new OnceLock for testing to avoid conflicts
let test_bind_address: OnceLock<String> = OnceLock::new();
// Test that we can set the value once
let result = test_bind_address.set("127.0.0.1:9090".to_string());
assert!(result.is_ok());
// Test that we can get the value
let value = test_bind_address.get();
assert!(value.is_some());
assert_eq!(value.unwrap(), "127.0.0.1:9090");
// Test that we can't set it again
let result2 = test_bind_address.set("different:port".to_string());
assert!(result2.is_err());
}
#[test]
fn test_index_html_constant() {
// Test that INDEX_HTML is not empty and contains expected content
assert!(INDEX_HTML.contains("html") || INDEX_HTML.contains("HTML"));
}
#[tokio::test]
async fn test_log_request_middleware_functionality() {
// Test basic properties of log_request function
// Since it requires complex setup with actual middleware,
// we focus on testing the types and structure
let request = Request::builder()
.method(Method::GET)
.uri("/test")
.body(Body::empty())
.unwrap();
// Verify request properties that log_request would process
assert_eq!(request.method(), Method::GET);
assert_eq!(request.uri().path(), "/test");
assert!(request.headers().is_empty());
}
#[test]
fn test_module_imports() {
// Test that our modules are properly imported and accessible
let _server = BashServer::new();
let _store = McpOAuthStore::new();
// Test config module
let config_result = config::Config::read_config("nonexistent.toml");
assert!(config_result.is_err()); // Should fail gracefully
}
#[test]
fn test_error_handling_types() {
// Test that Result type is properly used
let test_result: Result<String> = Ok("test".to_string());
assert!(test_result.is_ok());
let test_error: Result<String> = Err(anyhow::anyhow!("test error"));
assert!(test_error.is_err());
}
#[test]
fn test_dependencies_availability() {
// Test that critical dependencies are available
use std::sync::Arc;
let _arc_store = Arc::new(McpOAuthStore::new());
// Test that we can create basic types
let _socket_addr: Result<SocketAddr, _> = "127.0.0.1:8080".parse();
}
// ========== OAuth Mock Tests ==========
#[tokio::test]
async fn test_oauth_store_functionality() {
use common::oauth::{AuthToken, McpOAuthStore};
use oauth2::{AccessToken, EmptyExtraTokenFields};
let store = McpOAuthStore::new();
// Test client validation
let client = store
.validate_client("mcp-client", "http://localhost:8080/callback")
.await;
assert!(client.is_some());
let invalid_client = store
.validate_client("invalid-client", "http://localhost:8080/callback")
.await;
assert!(invalid_client.is_none());
// Test auth session creation
let session_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile email".to_string()),
Some("test-state".to_string()),
"test-session-123".to_string(),
)
.await;
assert_eq!(session_id, "test-session-123");
// Test token update and MCP token creation
let auth_token = AuthToken::new(
AccessToken::new("mock-third-party-token".to_string()),
oauth2::basic::BasicTokenType::Bearer,
EmptyExtraTokenFields {},
);
let update_result = store
.update_auth_session_token(&session_id, auth_token)
.await;
assert!(update_result.is_ok());
let mcp_token = store.create_mcp_token(&session_id).await;
assert!(mcp_token.is_ok());
let token = mcp_token.unwrap();
assert!(token.access_token.starts_with("mcp-token-"));
assert_eq!(token.client_id, "mcp-client");
// Test token validation
let validated = store.validate_token(&token.access_token).await;
assert!(validated.is_some());
}
#[tokio::test]
async fn test_oauth_authorization_flow_mock() {
use common::oauth::{AuthorizeQuery, McpOAuthStore};
use std::sync::Arc;
let store = Arc::new(McpOAuthStore::new());
// Mock authorization request
let auth_query = AuthorizeQuery {
response_type: "code".to_string(),
client_id: "mcp-client".to_string(),
redirect_uri: "http://localhost:8080/callback".to_string(),
scope: Some("profile email".to_string()),
state: Some("test-state-456".to_string()),
};
// Test that oauth_authorize function can be called
// Note: In a real test, we'd use test frameworks like tower::ServiceExt
// but here we're testing the basic functionality
let store_clone = store.clone();
let sessions_before = store_clone.auth_sessions.read().await.len();
// Verify store is accessible and functional
assert_eq!(sessions_before, 0);
// Test client validation within the flow
let client_validation = store
.validate_client(&auth_query.client_id, &auth_query.redirect_uri)
.await;
assert!(client_validation.is_some());
}
#[tokio::test]
async fn test_oauth_token_exchange_mock() {
use common::oauth::AuthToken;
use common::oauth::{McpOAuthStore, TokenRequest};
use oauth2::{AccessToken, EmptyExtraTokenFields};
use std::sync::Arc;
let store = Arc::new(McpOAuthStore::new());
// Create a session and add auth token (simulating successful OAuth flow)
let session_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile".to_string()),
Some("test-state".to_string()),
"token-exchange-session".to_string(),
)
.await;
let auth_token = AuthToken::new(
AccessToken::new("mock-external-token".to_string()),
oauth2::basic::BasicTokenType::Bearer,
EmptyExtraTokenFields {},
);
store
.update_auth_session_token(&session_id, auth_token)
.await
.unwrap();
// Mock token request (just for structure validation)
let _token_request = TokenRequest {
grant_type: "authorization_code".to_string(),
code: "mock-auth-code".to_string(),
client_id: "mcp-client".to_string(),
client_secret: "mcp-client-secret".to_string(),
redirect_uri: "http://localhost:8080/callback".to_string(),
code_verifier: None,
refresh_token: "".to_string(),
};
// Test token creation
let mcp_token_result = store.create_mcp_token(&session_id).await;
assert!(mcp_token_result.is_ok());
let mcp_token = mcp_token_result.unwrap();
assert_eq!(mcp_token.token_type, "bearer");
assert_eq!(mcp_token.expires_in, Some(3600));
assert!(mcp_token.refresh_token.is_some());
// Verify token can be validated
let validation_result = store.validate_token(&mcp_token.access_token).await;
assert!(validation_result.is_some());
}
#[tokio::test]
async fn test_oauth_middleware_functionality() {
use common::oauth::AuthToken;
use common::oauth::McpOAuthStore;
use oauth2::{AccessToken, EmptyExtraTokenFields};
use std::sync::Arc;
let store = Arc::new(McpOAuthStore::new());
// Create a valid token for middleware testing
let session_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile".to_string()),
None,
"middleware-test-session".to_string(),
)
.await;
let auth_token = AuthToken::new(
AccessToken::new("middleware-test-token".to_string()),
oauth2::basic::BasicTokenType::Bearer,
EmptyExtraTokenFields {},
);
store
.update_auth_session_token(&session_id, auth_token)
.await
.unwrap();
let mcp_token = store.create_mcp_token(&session_id).await.unwrap();
// Test token validation (simulating middleware behavior)
let valid_token_check = store.validate_token(&mcp_token.access_token).await;
assert!(valid_token_check.is_some());
// Test invalid token
let invalid_token_check = store.validate_token("invalid-token-12345").await;
assert!(invalid_token_check.is_none());
// Test empty token
let empty_token_check = store.validate_token("").await;
assert!(empty_token_check.is_none());
}
#[tokio::test]
async fn test_oauth_error_handling() {
use common::oauth::McpOAuthStore;
use std::sync::Arc;
let store = Arc::new(McpOAuthStore::new());
// Test creating MCP token without session
let no_session_result = store.create_mcp_token("nonexistent-session").await;
assert!(no_session_result.is_err());
assert_eq!(no_session_result.unwrap_err(), "Session not found");
// Test creating MCP token without auth token in session
let session_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile".to_string()),
None,
"no-auth-token-session".to_string(),
)
.await;
let no_auth_token_result = store.create_mcp_token(&session_id).await;
assert!(no_auth_token_result.is_err());
assert_eq!(
no_auth_token_result.unwrap_err(),
"No third-party token available for session"
);
// Test updating nonexistent session
let auth_token = oauth2::StandardTokenResponse::new(
oauth2::AccessToken::new("test-token".to_string()),
oauth2::basic::BasicTokenType::Bearer,
oauth2::EmptyExtraTokenFields {},
);
let update_nonexistent = store
.update_auth_session_token("nonexistent", auth_token)
.await;
assert!(update_nonexistent.is_err());
assert_eq!(update_nonexistent.unwrap_err(), "Session not found");
}
#[tokio::test]
async fn test_oauth_security_validations() {
use common::oauth::McpOAuthStore;
use std::sync::Arc;
let store = Arc::new(McpOAuthStore::new());
// Test invalid client ID
let invalid_client = store
.validate_client("malicious-client", "http://localhost:8080/callback")
.await;
assert!(invalid_client.is_none());
// Test invalid redirect URI (potential open redirect attack)
let malicious_redirect = store
.validate_client("mcp-client", "http://evil.com/steal-tokens")
.await;
assert!(malicious_redirect.is_none());
// Test valid client with valid redirect URI
let valid_client = store
.validate_client("mcp-client", "http://localhost:8080/callback")
.await;
assert!(valid_client.is_some());
// Test that tokens are properly random and unique
let session1_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile".to_string()),
None,
"security-test-1".to_string(),
)
.await;
let session2_id = store
.create_auth_session(
"mcp-client".to_string(),
Some("profile".to_string()),
None,
"security-test-2".to_string(),
)
.await;
// Add auth tokens to both sessions
for (i, session_id) in [&session1_id, &session2_id].iter().enumerate() {
let auth_token = oauth2::StandardTokenResponse::new(
oauth2::AccessToken::new(format!("security-token-{i}")),
oauth2::basic::BasicTokenType::Bearer,
oauth2::EmptyExtraTokenFields {},
);
store
.update_auth_session_token(session_id, auth_token)
.await
.unwrap();
}
let token1 = store.create_mcp_token(&session1_id).await.unwrap();
let token2 = store.create_mcp_token(&session2_id).await.unwrap();
// Tokens should be different
assert_ne!(token1.access_token, token2.access_token);
assert_ne!(token1.refresh_token, token2.refresh_token);
// Both should be valid
assert!(store.validate_token(&token1.access_token).await.is_some());
assert!(store.validate_token(&token2.access_token).await.is_some());
}
}
@@ -0,0 +1,125 @@
<!--
~ 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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP OAuth</title>
<style>
:root {
--primary-color: #4285f4;
--secondary-color: #f1f1f1;
--text-color: #333;
--border-color: #ddd;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
color: var(--text-color);
}
.container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 90%;
margin: 1rem;
}
h1 {
margin: 0 0 1.5rem 0;
font-size: 1.8rem;
text-align: center;
}
.client-info {
background: var(--secondary-color);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.btn-group {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
border: none;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #3367d6;
}
.btn-secondary {
background-color: var(--secondary-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<h1>MCP OAuth</h1>
<div class="client-info">
<p><strong>{{ client_id }}</strong> requests access to your account.</p>
<p>requested scopes: {{ scopes }}</p>
</div>
<form action="/approve" method="post">
<input type="hidden" name="client_id" value="{{ client_id }}">
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="state" value="{{ state }}">
<div class="btn-group">
<button type="submit" name="approved" value="true" class="btn btn-primary">Approve</button>
<button type="submit" name="approved" value="false" class="btn btn-secondary">Reject</button>
</div>
</form>
</div>
</body>
</html>
@@ -166,7 +166,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
type: 'label-selector',
required: true
}"
[name]="'sourceLabels'"
@@ -182,7 +182,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
type: 'label-selector',
required: true
}"
[name]="'targetLabels'"
@@ -189,7 +189,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
type: 'label-selector',
required: true
}"
[name]="'labels'"
@@ -570,7 +570,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
type: 'label-selector'
}"
[name]="'labels'"
[(ngModel)]="define.labels"
@@ -182,7 +182,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
type: 'label-selector',
required: true
}"
[name]="'labels'"
@@ -187,7 +187,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
type: 'label-selector',
}"
[name]="'labels'"
[(ngModel)]="filterLabels"
@@ -41,6 +41,7 @@
[collectors]="collectors"
[labelKeys]="labelKeys"
[labelMap]="labelMap"
[labelIsCustom]="labelIsCustom"
[sdParams]="sdParams"
[sdDefines]="sdDefines"
(scrapeChange)="onScrapeChange($event)"
@@ -72,6 +72,7 @@ export class MonitorEditComponent implements OnInit {
spinningTip: string = 'Loading...';
labelKeys: string[] = [];
labelMap: { [key: string]: string[] } = {};
labelIsCustom: boolean = true;
ngOnInit(): void {
this.route.paramMap
@@ -184,7 +184,8 @@
}"
[extra]="{
labelKeys: labelKeys,
labelMap: labelMap
labelMap: labelMap,
labelIsCustom: labelIsCustom
}"
[name]="'labels'"
[(ngModel)]="monitor.labels"
@@ -50,6 +50,7 @@ export class MonitorFormComponent implements OnChanges {
@Input() sdParams!: Param[];
@Input() labelKeys!: string[];
@Input() labelMap!: { [key: string]: string[] };
@Input() labelIsCustom!: boolean;
@Output() readonly formSubmit = new EventEmitter<any>();
@Output() readonly formCancel = new EventEmitter<any>();
@@ -40,6 +40,7 @@
[collectors]="collectors"
[labelKeys]="labelKeys"
[labelMap]="labelMap"
[labelIsCustom]="labelIsCustom"
[sdParams]="sdParams"
[sdDefines]="sdDefines"
(scrapeChange)="onScrapeChange($event)"
@@ -33,7 +33,6 @@ import { Param } from '../../../pojo/Param';
import { ParamDefine } from '../../../pojo/ParamDefine';
import { AppDefineService } from '../../../service/app-define.service';
import { CollectorService } from '../../../service/collector.service';
import { LabelService } from '../../../service/label.service';
import { MonitorService } from '../../../service/monitor.service';
import { generateReadableRandomString } from '../../../shared/utils/common-util';
@@ -59,6 +58,7 @@ export class MonitorNewComponent implements OnInit {
spinningTip: string = 'Loading...';
labelKeys: string[] = [];
labelMap: { [key: string]: string[] } = {};
labelIsCustom: boolean = true;
constructor(
private appDefineSvc: AppDefineService,
private monitorSvc: MonitorService,
@@ -67,8 +67,7 @@ export class MonitorNewComponent implements OnInit {
private notifySvc: NzNotificationService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private titleSvc: TitleService,
private collectorSvc: CollectorService,
private labelSvc: LabelService
private collectorSvc: CollectorService
) {
this.monitor = new Monitor();
this.grafanaDashboard = new GrafanaDashboard();
@@ -214,7 +213,6 @@ export class MonitorNewComponent implements OnInit {
this.isSpinning = false;
}
);
this.loadLabels();
}
onScrapeChange(scrapeValue: string) {
@@ -333,34 +331,4 @@ export class MonitorNewComponent implements OnInit {
onCancel() {
this.router.navigateByUrl(`/monitors`);
}
loadLabels() {
let labelsInit$ = this.labelSvc.loadLabels(undefined, undefined, 0, 9999).subscribe(
message => {
if (message.code === 0) {
let page = message.data;
this.labelKeys = [...new Set(page.content.map(label => label.name))];
this.labelMap = {};
page.content.forEach(label => {
if (!this.labelMap[label.name]) {
this.labelMap[label.name] = [];
}
if (label.tagValue && !this.labelMap[label.name].includes(label.tagValue)) {
this.labelMap[label.name].push(label.tagValue);
}
});
} else {
console.warn(message.msg);
}
labelsInit$.unsubscribe();
},
error => {
labelsInit$.unsubscribe();
console.error(error.msg);
}
);
}
}
@@ -417,7 +417,7 @@
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
type: 'label-selector',
required: true
}"
[name]="'sourceLabels'"
@@ -224,5 +224,6 @@
[id]="item.field"
[labelKeys]="extra.labelKeys"
[labelMap]="extra.labelMap"
[labelIsCustom]="extra.labelIsCustom"
/>
</ng-container>
@@ -32,7 +32,7 @@
>
<nz-option *ngFor="let keyOption of labelKeys" [nzLabel]="keyOption" [nzValue]="keyOption"></nz-option>
<nz-option
*ngIf="customInputKey && !hasMatchingOption(customInputKey, labelKeys)"
*ngIf="labelIsCustom && customInputKey && !hasMatchingOption(customInputKey, labelKeys)"
[nzLabel]="customInputKey"
[nzValue]="customInputKey"
></nz-option>
@@ -53,7 +53,7 @@
>
<nz-option *ngFor="let valueOption of getLabelValues(value[i].key)" [nzLabel]="valueOption" [nzValue]="valueOption"></nz-option>
<nz-option
*ngIf="customInputValue && !hasMatchingOption(customInputValue, getLabelValues(value[i].key))"
*ngIf="labelIsCustom && customInputValue && !hasMatchingOption(customInputValue, getLabelValues(value[i].key))"
[nzLabel]="customInputValue"
[nzValue]="customInputValue"
></nz-option>
@@ -17,9 +17,11 @@
* under the License.
*/
import { Component, Input, forwardRef, Output, EventEmitter } from '@angular/core';
import { Component, Input, forwardRef, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LabelService } from '../../../service/label.service';
@Component({
selector: 'app-label-selector',
templateUrl: './label-selector.component.html',
@@ -32,11 +34,14 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
}
]
})
export class LabelSelectorComponent implements ControlValueAccessor {
export class LabelSelectorComponent implements ControlValueAccessor, OnInit {
@Input() name!: string;
@Input() id!: string;
@Input() labelKeys: string[] = [];
@Input() labelMap: { [key: string]: string[] } = {};
@Input() labelIsCustom: boolean = false;
constructor(private labelSvc: LabelService) {}
value: Array<{ key: string; value: string }> = [];
customInputKey: string = '';
@@ -99,4 +104,42 @@ export class LabelSelectorComponent implements ControlValueAccessor {
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
ngOnInit(): void {
if (!this.labelKeys || this.labelKeys.length == 0) {
this.labelMap = {};
this.labelKeys = [];
this.loadLabels();
}
}
loadLabels() {
let labelsInit$ = this.labelSvc.loadLabels(undefined, undefined, 0, 9999).subscribe(
message => {
if (message.code === 0) {
let page = message.data;
this.labelKeys = [...new Set(page.content.map(label => label.name))];
this.labelMap = {};
page.content.forEach(label => {
if (!this.labelMap[label.name]) {
this.labelMap[label.name] = [];
}
if (label.tagValue && !this.labelMap[label.name].includes(label.tagValue)) {
this.labelMap[label.name].push(label.tagValue);
}
});
} else {
console.warn(message.msg);
}
labelsInit$.unsubscribe();
},
error => {
labelsInit$.unsubscribe();
console.error(error.msg);
}
);
}
}