mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e690df869a | ||
|
|
fa0a952f1e | ||
|
|
3a33d8e96c | ||
|
|
a85bd46c18 | ||
|
|
ec061f87db | ||
|
|
79d15ed5de | ||
|
|
3089f72580 | ||
|
|
999af65ebc | ||
|
|
f859f1a29e | ||
|
|
252f9a5d65 | ||
|
|
5022ec4dcc | ||
|
|
74c79d1ea3 | ||
|
|
fc3a858f81 | ||
|
|
664a25334c | ||
|
|
d80ecb434e | ||
|
|
d5995a9675 | ||
|
|
8e9f8da341 | ||
|
|
1f257a8870 | ||
|
|
eb753be1f5 | ||
|
|
3b3a8630fd | ||
|
|
eda420d118 | ||
|
|
b0d8516822 | ||
|
|
c884f11995 | ||
|
|
db73a43f9e | ||
|
|
fd8c6ef8cf | ||
|
|
dc10bcef97 | ||
|
|
7a8156a2d0 | ||
|
|
efa7e33672 | ||
|
|
dffb38b4c5 | ||
|
|
6671f4603b | ||
|
|
c752a12d88 | ||
|
|
2525797134 | ||
|
|
6fec531cf2 | ||
|
|
b279d27345 | ||
|
|
96a98d9a4b | ||
|
|
f64da5fcf7 | ||
|
|
0c4136cade | ||
|
|
b8148240f8 | ||
|
|
67c861a21a | ||
|
|
589741d068 | ||
|
|
6863ded5bd | ||
|
|
038ed5dea8 | ||
|
|
589e64414e | ||
|
|
6394a72926 | ||
|
|
dc10a4e564 | ||
|
|
9614041ed7 | ||
|
|
50ee6b1178 | ||
|
|
88cfd69dfa | ||
|
|
03fc1a7b8f |
+1
-5
@@ -3,11 +3,7 @@
|
||||
"projectOwner": "apache",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md",
|
||||
"README_CN.md",
|
||||
"README_JP.md"
|
||||
],
|
||||
"files": [],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
"commitConvention": "none",
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Build backend Maven E2E modules
|
||||
run: |
|
||||
mvnd clean -B package \
|
||||
-pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-log-e2e \
|
||||
-pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-observability-e2e \
|
||||
-am \
|
||||
-Dmaven.test.skip=false \
|
||||
--file pom.xml
|
||||
|
||||
@@ -39,12 +39,6 @@ jobs:
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
archive_ext: tar.gz
|
||||
- platform: macos-amd64
|
||||
runner: macos-13
|
||||
archive_ext: tar.gz
|
||||
- platform: macos-arm64
|
||||
runner: macos-14
|
||||
archive_ext: tar.gz
|
||||
- platform: windows-amd64
|
||||
runner: windows-latest
|
||||
archive_ext: zip
|
||||
@@ -81,6 +75,63 @@ jobs:
|
||||
}
|
||||
"archive=$($package.FullName)" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Smoke test native collector package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$archive = "${{ steps.package.outputs.archive }}"
|
||||
$work = Join-Path ([System.IO.Path]::GetTempPath()) "hzb-smoke"
|
||||
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $work | Out-Null
|
||||
|
||||
if ($archive.EndsWith(".zip")) {
|
||||
Expand-Archive -Path $archive -DestinationPath $work -Force
|
||||
} else {
|
||||
tar -xzf $archive -C $work
|
||||
}
|
||||
|
||||
$binary = Get-ChildItem -Path $work -Recurse -File |
|
||||
Where-Object { $_.Name -like "apache-hertzbeat-collector-native-*" -and $_.Name -notlike "*.txt" } |
|
||||
Where-Object { $_.Length -gt 10MB } | Select-Object -First 1
|
||||
if (-not $binary) { throw "native executable not found in $archive" }
|
||||
$conf = Join-Path $binary.DirectoryName "config"
|
||||
Write-Host "executable: $($binary.FullName)"
|
||||
|
||||
$out = Join-Path $work "stdout.log"
|
||||
$err = Join-Path $work "stderr.log"
|
||||
$env:MANAGER_HOST = "127.0.0.1"
|
||||
$env:IDENTITY = "ci-smoke-${{ matrix.platform }}"
|
||||
$proc = Start-Process -FilePath $binary.FullName `
|
||||
-ArgumentList "--spring.config.location=$conf$([IO.Path]::DirectorySeparatorChar)" `
|
||||
-RedirectStandardOutput $out -RedirectStandardError $err -PassThru
|
||||
|
||||
# The collector prints "Started Collector" before the runners execute, and past
|
||||
# failures crashed after that line, so wait for the ServiceLoader registration too
|
||||
# and then confirm the process is still alive.
|
||||
$deadline = (Get-Date).AddSeconds(90)
|
||||
$registered = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Seconds 2
|
||||
$log = (Get-Content $out, $err -ErrorAction SilentlyContinue) -join "`n"
|
||||
if ($log -match "collect strategies") { $registered = $true; break }
|
||||
if ($proc.HasExited) { break }
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 10
|
||||
$proc.Refresh()
|
||||
$alive = -not $proc.HasExited
|
||||
$exitCode = if ($alive) { $null } else { $proc.ExitCode }
|
||||
if ($alive) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$log = (Get-Content $out, $err -ErrorAction SilentlyContinue) -join "`n"
|
||||
Write-Host "----- collector output (tail) -----"
|
||||
Write-Host (($log -split "`n") | Select-Object -Last 40 | Out-String)
|
||||
Write-Host "-----------------------------------"
|
||||
|
||||
if (-not $alive) { throw "collector process exited (exit code $exitCode)" }
|
||||
if (-not $registered) { throw "collector stayed up but never registered its collect strategies" }
|
||||
Write-Host "smoke test passed for ${{ matrix.platform }}"
|
||||
|
||||
- name: Upload native collector package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
# Setup pnpm (must run before setup-node so the pnpm cache can be configured)
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 10
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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: Docker Compose Config
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
paths:
|
||||
- '.github/workflows/docker-compose-config-test.yml'
|
||||
- 'script/ci/check-quickstart-compose.sh'
|
||||
- 'script/docker-compose/**'
|
||||
pull_request:
|
||||
branches: [ master, dev ]
|
||||
paths:
|
||||
- '.github/workflows/docker-compose-config-test.yml'
|
||||
- 'script/ci/check-quickstart-compose.sh'
|
||||
- 'script/docker-compose/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
quickstart-config:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate quick-start listener bindings
|
||||
run: sh script/ci/check-quickstart-compose.sh
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -44,10 +44,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: rustfmt, clippy
|
||||
run: |
|
||||
rustup toolchain install "$RUST_VERSION" \
|
||||
--profile minimal \
|
||||
--component rustfmt \
|
||||
--component clippy
|
||||
rustup default "$RUST_VERSION"
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
run: |
|
||||
mvnd -B clean package -Prelease,cluster -Dmaven.test.skip=false --file pom.xml
|
||||
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
|
||||
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
|
||||
+2
-2
@@ -131,7 +131,7 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
## 🥐 Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -271,4 +271,4 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
### 模块
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
## 🥐 Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 Quick Start
|
||||
|
||||
@@ -136,411 +136,9 @@ Detailed steps refer to [Artifact Hub](https://artifacthub.io/packages/helm/hert
|
||||
Thanks to these wonderful people, welcome to join us:
|
||||
[Contributor Guide](CONTRIBUTING.md)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
</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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<a href="https://github.com/apache/hertzbeat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=apache/hertzbeat&max=500&columns=18&anon=1" alt="contributors"/>
|
||||
</a>
|
||||
|
||||
## 💬 Join discussion
|
||||
|
||||
|
||||
+4
-406
@@ -47,7 +47,7 @@
|
||||
|
||||
## 🥐 模块
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 快速开始
|
||||
|
||||
@@ -135,411 +135,9 @@
|
||||
Thanks these wonderful people, welcome to join us:
|
||||
[贡献者指南](CONTRIBUTING.md)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
</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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<a href="https://github.com/apache/hertzbeat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=apache/hertzbeat&max=500&columns=18&anon=1" alt="contributors"/>
|
||||
</a>
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
|
||||
+4
-406
@@ -46,7 +46,7 @@
|
||||
|
||||
## 🥐 モジュール
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 クイックスタート
|
||||
|
||||
@@ -138,411 +138,9 @@ Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタ
|
||||
Thanks these wonderful people, welcome to join us:
|
||||
[貢献ガイド](CONTRIBUTING.md)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
</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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<a href="https://github.com/apache/hertzbeat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=apache/hertzbeat&max=500&columns=18&anon=1" alt="contributors"/>
|
||||
</a>
|
||||
|
||||
## 💬 コミュニティ交流
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.dao;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -26,4 +28,8 @@ import org.springframework.stereotype.Repository;
|
||||
*/
|
||||
@Repository
|
||||
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
|
||||
|
||||
Optional<ChatConversation> findByIdAndCreator(Long id, String creator);
|
||||
|
||||
List<ChatConversation> findAllByCreatorOrderByIdDesc(String creator);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.ai.dao;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -37,14 +38,23 @@ public interface SopScheduleDao extends JpaRepository<SopSchedule, Long>, JpaSpe
|
||||
* @param conversationId The conversation ID
|
||||
* @return List of schedules
|
||||
*/
|
||||
List<SopSchedule> findByConversationId(Long conversationId);
|
||||
List<SopSchedule> findByConversationIdAndCreator(Long conversationId, String creator);
|
||||
|
||||
/**
|
||||
* Find a schedule only when it belongs to the supplied creator.
|
||||
* @param id schedule identity
|
||||
* @param creator authenticated creator
|
||||
* @return matching schedule
|
||||
*/
|
||||
Optional<SopSchedule> findByIdAndCreator(Long id, String creator);
|
||||
|
||||
/**
|
||||
* Find all enabled schedules that are due for execution.
|
||||
* @param currentTime The current time to compare against
|
||||
* @return List of due schedules
|
||||
*/
|
||||
@Query("SELECT s FROM SopSchedule s WHERE s.enabled = true AND s.nextRunTime <= :currentTime")
|
||||
@Query("SELECT s FROM SopSchedule s "
|
||||
+ "WHERE s.enabled = true AND s.creator IS NOT NULL AND s.nextRunTime <= :currentTime")
|
||||
List<SopSchedule> findDueSchedules(@Param("currentTime") LocalDateTime currentTime);
|
||||
|
||||
/**
|
||||
|
||||
+29
-2
@@ -21,6 +21,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
@@ -98,6 +99,12 @@ public class SopScheduleExecutor {
|
||||
* Execute a single scheduled SOP and push result to conversation.
|
||||
*/
|
||||
private void executeSchedule(SopSchedule schedule) {
|
||||
schedule = sopScheduleService.getScheduleForExecution(schedule.getId());
|
||||
if (schedule == null) {
|
||||
return;
|
||||
}
|
||||
String executionCreator = schedule.getCreator();
|
||||
Long executionConversationId = schedule.getConversationId();
|
||||
log.info("Executing scheduled SOP {} for conversation {}",
|
||||
schedule.getSopName(), schedule.getConversationId());
|
||||
|
||||
@@ -122,7 +129,13 @@ public class SopScheduleExecutor {
|
||||
|
||||
// Execute SOP
|
||||
SopResult result = sopEngine.executeSync(definition, params);
|
||||
|
||||
|
||||
SopSchedule deliverySchedule = sopScheduleService.getScheduleForExecution(schedule.getId());
|
||||
if (!hasSameExecutionTarget(deliverySchedule, executionCreator, executionConversationId)) {
|
||||
log.warn("Schedule {} lost its execution owner before result delivery", schedule.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create push message
|
||||
String messageContent = formatPushMessage(schedule, result);
|
||||
|
||||
@@ -131,6 +144,7 @@ public class SopScheduleExecutor {
|
||||
.conversationId(schedule.getConversationId())
|
||||
.role(ROLE_SYSTEM_PUSH)
|
||||
.content(messageContent)
|
||||
.creator(schedule.getCreator())
|
||||
.build();
|
||||
|
||||
chatMessageDao.save(pushMessage);
|
||||
@@ -141,7 +155,13 @@ public class SopScheduleExecutor {
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to execute scheduled SOP {} for conversation {}",
|
||||
schedule.getSopName(), schedule.getConversationId(), e);
|
||||
|
||||
|
||||
SopSchedule deliverySchedule = sopScheduleService.getScheduleForExecution(schedule.getId());
|
||||
if (!hasSameExecutionTarget(deliverySchedule, executionCreator, executionConversationId)) {
|
||||
log.warn("Schedule {} lost its execution owner before error delivery", schedule.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Still save an error message
|
||||
String errorContent = SopMessageUtil.getMessage("schedule.push.error.prefix") + " " + schedule.getSopName()
|
||||
+ "\n\n" + SopMessageUtil.getMessage("schedule.push.error.label") + " " + e.getMessage();
|
||||
@@ -149,6 +169,7 @@ public class SopScheduleExecutor {
|
||||
.conversationId(schedule.getConversationId())
|
||||
.role(ROLE_SYSTEM_PUSH)
|
||||
.content(errorContent)
|
||||
.creator(schedule.getCreator())
|
||||
.build();
|
||||
chatMessageDao.save(errorMessage);
|
||||
|
||||
@@ -158,6 +179,12 @@ public class SopScheduleExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasSameExecutionTarget(SopSchedule schedule, String creator, Long conversationId) {
|
||||
return schedule != null
|
||||
&& Objects.equals(creator, schedule.getCreator())
|
||||
&& Objects.equals(conversationId, schedule.getConversationId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the push message content with SOP result.
|
||||
*/
|
||||
|
||||
@@ -73,6 +73,15 @@ public interface SopScheduleService {
|
||||
*/
|
||||
List<SopSchedule> getDueSchedules();
|
||||
|
||||
/**
|
||||
* Re-read a schedule for background execution and verify that its persisted
|
||||
* creator still owns the target conversation. This method does not depend
|
||||
* on a request-thread subject.
|
||||
* @param id schedule ID
|
||||
* @return validated schedule, or {@code null} when it must not execute
|
||||
*/
|
||||
SopSchedule getScheduleForExecution(Long id);
|
||||
|
||||
/**
|
||||
* Update the execution times after a schedule runs.
|
||||
* @param id The schedule ID
|
||||
|
||||
+28
-13
@@ -33,7 +33,6 @@ import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -66,6 +65,10 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
@Override
|
||||
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
|
||||
String creator = requireCurrentUserId();
|
||||
ChatConversation conversation = conversationId == null
|
||||
? null
|
||||
: requireOwnedConversation(conversationId, creator);
|
||||
|
||||
// Check if provider is properly configured
|
||||
if (!chatClientProviderService.isConfigured()) {
|
||||
@@ -78,15 +81,12 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
.build());
|
||||
}
|
||||
|
||||
ChatConversation conversation;
|
||||
if (conversationId == null) {
|
||||
if (conversation == null) {
|
||||
// The API contract makes conversationId optional, so create a conversation for the first message.
|
||||
conversation = new ChatConversation();
|
||||
conversation.setTitle(buildConversationTitle(message));
|
||||
conversation.setCreator(creator);
|
||||
conversation = conversationDao.save(conversation);
|
||||
} else {
|
||||
conversation = conversationDao.findById(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId));
|
||||
}
|
||||
Long currentConversationId = conversation.getId();
|
||||
log.info("Starting streaming conversation: {}", currentConversationId);
|
||||
@@ -170,6 +170,7 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
public ChatConversation createConversation() {
|
||||
ChatConversation conversation = new ChatConversation();
|
||||
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
|
||||
conversation.setCreator(requireCurrentUserId());
|
||||
return conversationDao.save(conversation);
|
||||
}
|
||||
|
||||
@@ -182,17 +183,16 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
if (conversationId == null) {
|
||||
return null;
|
||||
}
|
||||
ChatConversation conversation = conversationDao.findById(conversationId).orElse(null);
|
||||
if (conversation != null) {
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
conversation.setMessages(messages);
|
||||
}
|
||||
ChatConversation conversation = requireOwnedConversation(conversationId, requireCurrentUserId());
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
conversation.setMessages(messages);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatConversation> getAllConversations() {
|
||||
List<ChatConversation> conversations = conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
|
||||
List<ChatConversation> conversations =
|
||||
conversationDao.findAllByCreatorOrderByIdDesc(requireCurrentUserId());
|
||||
if (conversations.isEmpty()) {
|
||||
return conversations;
|
||||
}
|
||||
@@ -213,6 +213,7 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteConversation(Long conversationId) {
|
||||
requireOwnedConversation(conversationId, requireCurrentUserId());
|
||||
// Delete associated schedules first to prevent tasks from writing orphaned messages.
|
||||
sopScheduleDao.deleteByConversationId(conversationId);
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
@@ -224,7 +225,8 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
@Override
|
||||
public Boolean saveSecurityData(SecurityData securityData) {
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findById(securityData.getConversationId());
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findByIdAndCreator(
|
||||
securityData.getConversationId(), requireCurrentUserId());
|
||||
if (chatConversation.isPresent()) {
|
||||
ChatConversation conversation = chatConversation.get();
|
||||
conversation.setSecurityData(AesUtil.aesEncode(securityData.getSecurityData()));
|
||||
@@ -234,4 +236,17 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String requireCurrentUserId() {
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
if (subject == null || subject.getPrincipal() == null) {
|
||||
throw new IllegalStateException("No authenticated user");
|
||||
}
|
||||
return String.valueOf(subject.getPrincipal());
|
||||
}
|
||||
|
||||
private ChatConversation requireOwnedConversation(Long conversationId, String creator) {
|
||||
return conversationDao.findByIdAndCreator(conversationId, creator)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-26
@@ -17,11 +17,16 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
@@ -36,23 +41,32 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class SopScheduleServiceImpl implements SopScheduleService {
|
||||
|
||||
private final SopScheduleDao sopScheduleDao;
|
||||
private final ChatConversationDao conversationDao;
|
||||
|
||||
@Autowired
|
||||
public SopScheduleServiceImpl(SopScheduleDao sopScheduleDao) {
|
||||
public SopScheduleServiceImpl(SopScheduleDao sopScheduleDao,
|
||||
ChatConversationDao conversationDao) {
|
||||
this.sopScheduleDao = sopScheduleDao;
|
||||
this.conversationDao = conversationDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule createSchedule(SopSchedule schedule) {
|
||||
// Validate cron expression
|
||||
String creator = requireCurrentUserId();
|
||||
requireOwnedConversation(schedule.getConversationId(), creator);
|
||||
validateCronExpression(schedule.getCronExpression());
|
||||
|
||||
// Calculate next run time
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
schedule.setEnabled(schedule.getEnabled() != null ? schedule.getEnabled() : true);
|
||||
|
||||
SopSchedule saved = sopScheduleDao.save(schedule);
|
||||
|
||||
SopSchedule persisted = SopSchedule.builder()
|
||||
.conversationId(schedule.getConversationId())
|
||||
.sopName(schedule.getSopName())
|
||||
.sopParams(schedule.getSopParams())
|
||||
.cronExpression(schedule.getCronExpression())
|
||||
.enabled(schedule.getEnabled() != null ? schedule.getEnabled() : true)
|
||||
.nextRunTime(calculateNextRunTime(schedule.getCronExpression()))
|
||||
.creator(creator)
|
||||
.build();
|
||||
SopSchedule saved = sopScheduleDao.save(persisted);
|
||||
log.info("Created schedule {} for conversation {} with SOP {}",
|
||||
saved.getId(), saved.getConversationId(), saved.getSopName());
|
||||
return saved;
|
||||
@@ -61,57 +75,59 @@ public class SopScheduleServiceImpl implements SopScheduleService {
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule updateSchedule(SopSchedule schedule) {
|
||||
SopSchedule existing = sopScheduleDao.findById(schedule.getId())
|
||||
SopSchedule existing = sopScheduleDao.findByIdAndCreator(
|
||||
schedule.getId(), requireCurrentUserId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + schedule.getId()));
|
||||
|
||||
// Update fields
|
||||
|
||||
existing.setSopName(schedule.getSopName());
|
||||
existing.setSopParams(schedule.getSopParams());
|
||||
|
||||
// If cron expression changed, recalculate next run time
|
||||
|
||||
if (!existing.getCronExpression().equals(schedule.getCronExpression())) {
|
||||
validateCronExpression(schedule.getCronExpression());
|
||||
existing.setCronExpression(schedule.getCronExpression());
|
||||
existing.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
}
|
||||
|
||||
|
||||
if (schedule.getEnabled() != null) {
|
||||
existing.setEnabled(schedule.getEnabled());
|
||||
}
|
||||
|
||||
|
||||
return sopScheduleDao.save(existing);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteSchedule(Long id) {
|
||||
SopSchedule schedule = sopScheduleDao.findByIdAndCreator(id, requireCurrentUserId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + id));
|
||||
log.info("Deleting schedule {}", id);
|
||||
sopScheduleDao.deleteById(id);
|
||||
sopScheduleDao.delete(schedule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SopSchedule getSchedule(Long id) {
|
||||
return sopScheduleDao.findById(id).orElse(null);
|
||||
return sopScheduleDao.findByIdAndCreator(id, requireCurrentUserId()).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SopSchedule> getSchedulesByConversation(Long conversationId) {
|
||||
return sopScheduleDao.findByConversationId(conversationId);
|
||||
String creator = requireCurrentUserId();
|
||||
requireOwnedConversation(conversationId, creator);
|
||||
return sopScheduleDao.findByConversationIdAndCreator(conversationId, creator);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule toggleSchedule(Long id, boolean enabled) {
|
||||
SopSchedule schedule = sopScheduleDao.findById(id)
|
||||
SopSchedule schedule = sopScheduleDao.findByIdAndCreator(id, requireCurrentUserId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + id));
|
||||
|
||||
|
||||
schedule.setEnabled(enabled);
|
||||
|
||||
// If enabling, recalculate next run time
|
||||
|
||||
if (enabled) {
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
}
|
||||
|
||||
|
||||
log.info("Schedule {} {} ", id, enabled ? "enabled" : "disabled");
|
||||
return sopScheduleDao.save(schedule);
|
||||
}
|
||||
@@ -123,16 +139,34 @@ public class SopScheduleServiceImpl implements SopScheduleService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateAfterExecution(Long id) {
|
||||
public SopSchedule getScheduleForExecution(Long id) {
|
||||
SopSchedule schedule = sopScheduleDao.findById(id).orElse(null);
|
||||
if (schedule == null || !Boolean.TRUE.equals(schedule.getEnabled())) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(schedule.getCreator())
|
||||
|| conversationDao.findByIdAndCreator(
|
||||
schedule.getConversationId(), schedule.getCreator()).isEmpty()) {
|
||||
schedule.setEnabled(false);
|
||||
sopScheduleDao.save(schedule);
|
||||
log.warn("Disabled schedule {} because its execution owner is missing", id);
|
||||
return null;
|
||||
}
|
||||
return schedule;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateAfterExecution(Long id) {
|
||||
SopSchedule schedule = getScheduleForExecution(id);
|
||||
if (schedule == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
schedule.setLastRunTime(LocalDateTime.now());
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
sopScheduleDao.save(schedule);
|
||||
|
||||
|
||||
log.debug("Updated schedule {} - Last run: {}, Next run: {}",
|
||||
id, schedule.getLastRunTime(), schedule.getNextRunTime());
|
||||
}
|
||||
@@ -161,4 +195,18 @@ public class SopScheduleServiceImpl implements SopScheduleService {
|
||||
throw new IllegalArgumentException("Failed to calculate next run time: " + cronExpression, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String requireCurrentUserId() {
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
if (subject == null || subject.getPrincipal() == null) {
|
||||
throw new IllegalStateException("No authenticated user");
|
||||
}
|
||||
return String.valueOf(subject.getPrincipal());
|
||||
}
|
||||
|
||||
private ChatConversation requireOwnedConversation(Long conversationId, String creator) {
|
||||
return conversationDao.findByIdAndCreator(conversationId, creator)
|
||||
.orElseThrow(() ->
|
||||
new IllegalArgumentException("Conversation not found: " + conversationId));
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -312,8 +312,16 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
|
||||
// Query and add sensitive parameters
|
||||
if (conversationId != null) {
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findById(conversationId);
|
||||
if (chatConversation.isPresent() && StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) {
|
||||
SubjectSum subject = McpContextHolder.getSubject();
|
||||
if (subject == null || subject.getPrincipal() == null) {
|
||||
return "Error: Authenticated conversation context is required";
|
||||
}
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findByIdAndCreator(
|
||||
conversationId, String.valueOf(subject.getPrincipal()));
|
||||
if (chatConversation.isEmpty()) {
|
||||
return "Error: Conversation not found or inaccessible";
|
||||
}
|
||||
if (StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) {
|
||||
List<Param> securityParams = JsonUtil.fromJson(
|
||||
AesUtil.aesDecode(chatConversation.get().getSecurityData()),
|
||||
new TypeReference<List<Param>>() {
|
||||
|
||||
+41
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.ai.schedule;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -77,6 +78,8 @@ class SopScheduleExecutorTest {
|
||||
.content("ok")
|
||||
.build();
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(first, second));
|
||||
when(scheduleService.getScheduleForExecution(1L)).thenReturn(first, first);
|
||||
when(scheduleService.getScheduleForExecution(2L)).thenReturn(second, second);
|
||||
when(skillRegistry.getSkill("daily_inspection")).thenReturn(definition);
|
||||
when(sopEngine.executeSync(any(SopDefinition.class), anyMap())).thenReturn(result);
|
||||
doThrow(new IllegalStateException("database unavailable"))
|
||||
@@ -85,6 +88,8 @@ class SopScheduleExecutorTest {
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
verify(sopEngine, times(2)).executeSync(any(SopDefinition.class), anyMap());
|
||||
verify(chatMessageDao, times(2)).save(argThat(
|
||||
message -> "alice".equals(message.getCreator())));
|
||||
verify(scheduleService).updateAfterExecution(2L);
|
||||
}
|
||||
|
||||
@@ -92,6 +97,7 @@ class SopScheduleExecutorTest {
|
||||
void checkShouldRejectInvalidScheduleParameters() {
|
||||
SopSchedule schedule = schedule(1L, "not-json");
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(scheduleService.getScheduleForExecution(1L)).thenReturn(schedule, schedule);
|
||||
when(skillRegistry.getSkill("daily_inspection"))
|
||||
.thenReturn(SopDefinition.builder().name("daily_inspection").build());
|
||||
|
||||
@@ -102,10 +108,44 @@ class SopScheduleExecutorTest {
|
||||
verify(scheduleService).updateAfterExecution(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldSkipScheduleWithoutValidatedOwner() {
|
||||
SopSchedule schedule = schedule(1L, null);
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(scheduleService.getScheduleForExecution(1L)).thenReturn(null);
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
verifyNoInteractions(sopEngine, chatMessageDao);
|
||||
verify(scheduleService, times(0)).updateAfterExecution(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldNotDeliverWhenOwnerChangesDuringExecution() {
|
||||
SopSchedule schedule = schedule(1L, null);
|
||||
SopSchedule changedOwner = schedule(1L, null);
|
||||
changedOwner.setCreator("bob");
|
||||
changedOwner.setConversationId(20L);
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(scheduleService.getScheduleForExecution(1L))
|
||||
.thenReturn(schedule, changedOwner, changedOwner);
|
||||
when(skillRegistry.getSkill("daily_inspection"))
|
||||
.thenReturn(SopDefinition.builder().name("daily_inspection").build());
|
||||
when(sopEngine.executeSync(any(SopDefinition.class), anyMap()))
|
||||
.thenReturn(SopResult.builder().status("SUCCESS").content("ok").build());
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
verify(sopEngine).executeSync(any(SopDefinition.class), anyMap());
|
||||
verifyNoInteractions(chatMessageDao);
|
||||
verify(scheduleService).updateAfterExecution(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldPushErrorWhenScheduledSkillNoLongerExists() {
|
||||
SopSchedule schedule = schedule(1L, null);
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(scheduleService.getScheduleForExecution(1L)).thenReturn(schedule, schedule);
|
||||
when(skillRegistry.getSkill("daily_inspection")).thenReturn(null);
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
@@ -123,6 +163,7 @@ class SopScheduleExecutorTest {
|
||||
.conversationId(10L)
|
||||
.sopName("daily_inspection")
|
||||
.sopParams(params)
|
||||
.creator("alice")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+111
-11
@@ -18,9 +18,13 @@
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -35,6 +39,7 @@ import org.apache.hertzbeat.ai.dao.ChatMessageDao;
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.SecurityData;
|
||||
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
@@ -79,29 +84,30 @@ class ConversationServiceImplTest {
|
||||
|
||||
@Test
|
||||
void streamChatShouldKeepCompleteConversationHistory() {
|
||||
SubjectSum subject = org.mockito.Mockito.mock(SubjectSum.class);
|
||||
SurenessContextHolder.bindSubject(subject);
|
||||
SubjectSum subject = bindSubject("alice");
|
||||
ChatConversation conversation = ChatConversation.builder()
|
||||
.id(CONVERSATION_ID)
|
||||
.title("已命名会话")
|
||||
.title("Named conversation")
|
||||
.creator("alice")
|
||||
.build();
|
||||
List<ChatMessage> history = List.of(
|
||||
ChatMessage.builder()
|
||||
.id(11L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("user")
|
||||
.content("上一轮问题")
|
||||
.content("Previous question")
|
||||
.build(),
|
||||
ChatMessage.builder()
|
||||
.id(12L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("assistant")
|
||||
.content("上一轮回答")
|
||||
.content("Previous answer")
|
||||
.build());
|
||||
AtomicLong messageId = new AtomicLong(20L);
|
||||
|
||||
when(chatClientProviderService.isConfigured()).thenReturn(true);
|
||||
when(conversationDao.findById(CONVERSATION_ID)).thenReturn(Optional.of(conversation));
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.of(conversation));
|
||||
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID)).thenReturn(history);
|
||||
when(messageDao.save(any(ChatMessage.class))).thenAnswer(invocation -> {
|
||||
ChatMessage savedMessage = invocation.getArgument(0);
|
||||
@@ -109,10 +115,10 @@ class ConversationServiceImplTest {
|
||||
return savedMessage;
|
||||
});
|
||||
when(chatClientProviderService.streamChat(any(ChatRequestContext.class)))
|
||||
.thenReturn(Flux.just("本轮回答"));
|
||||
.thenReturn(Flux.just("Current answer"));
|
||||
|
||||
List<ServerSentEvent<ChatResponseChunk>> events = conversationService
|
||||
.streamChat("本轮问题", CONVERSATION_ID)
|
||||
.streamChat("Current question", CONVERSATION_ID)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
@@ -129,6 +135,7 @@ class ConversationServiceImplTest {
|
||||
*/
|
||||
@Test
|
||||
void streamChatShouldCreateConversationWhenConversationIdIsMissing() {
|
||||
bindSubject("alice");
|
||||
AtomicLong messageId = new AtomicLong(20L);
|
||||
when(chatClientProviderService.isConfigured()).thenReturn(true);
|
||||
when(conversationDao.save(any(ChatConversation.class))).thenAnswer(invocation -> {
|
||||
@@ -143,10 +150,10 @@ class ConversationServiceImplTest {
|
||||
return savedMessage;
|
||||
});
|
||||
when(chatClientProviderService.streamChat(any(ChatRequestContext.class)))
|
||||
.thenReturn(Flux.just("本轮回答"));
|
||||
.thenReturn(Flux.just("Current answer"));
|
||||
|
||||
List<ServerSentEvent<ChatResponseChunk>> events = conversationService
|
||||
.streamChat("本轮问题", null)
|
||||
.streamChat("Initial question", null)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
@@ -161,7 +168,8 @@ class ConversationServiceImplTest {
|
||||
assertEquals(List.of(), contextCaptor.getValue().getConversationHistory());
|
||||
ArgumentCaptor<ChatConversation> conversationCaptor = ArgumentCaptor.forClass(ChatConversation.class);
|
||||
verify(conversationDao).save(conversationCaptor.capture());
|
||||
assertEquals("本轮问题", conversationCaptor.getValue().getTitle());
|
||||
assertEquals("Initial question", conversationCaptor.getValue().getTitle());
|
||||
assertEquals("alice", conversationCaptor.getValue().getCreator());
|
||||
verifyNoMoreInteractions(conversationDao);
|
||||
}
|
||||
|
||||
@@ -170,12 +178,20 @@ class ConversationServiceImplTest {
|
||||
*/
|
||||
@Test
|
||||
void deleteConversationShouldRemoveSchedulesMessagesAndConversationInOrder() {
|
||||
bindSubject("alice");
|
||||
ChatConversation conversation = ChatConversation.builder()
|
||||
.id(CONVERSATION_ID)
|
||||
.title("Owned conversation")
|
||||
.creator("alice")
|
||||
.build();
|
||||
ChatMessage message = ChatMessage.builder()
|
||||
.id(11L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("user")
|
||||
.content("message to delete")
|
||||
.build();
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.of(conversation));
|
||||
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID))
|
||||
.thenReturn(List.of(message));
|
||||
|
||||
@@ -186,4 +202,88 @@ class ConversationServiceImplTest {
|
||||
deletionOrder.verify(messageDao).deleteAll(List.of(message));
|
||||
deletionOrder.verify(conversationDao).deleteById(CONVERSATION_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listConversationsShouldExcludeOtherCreators() {
|
||||
bindSubject("alice");
|
||||
ChatConversation ownedConversation = ChatConversation.builder()
|
||||
.id(CONVERSATION_ID)
|
||||
.title("Owned conversation")
|
||||
.creator("alice")
|
||||
.build();
|
||||
when(conversationDao.findAllByCreatorOrderByIdDesc("alice"))
|
||||
.thenReturn(List.of(ownedConversation));
|
||||
when(messageDao.findByConversationIdInOrderByGmtCreateAsc(List.of(CONVERSATION_ID)))
|
||||
.thenReturn(List.of());
|
||||
|
||||
List<ChatConversation> result = conversationService.getAllConversations();
|
||||
|
||||
assertEquals(List.of(ownedConversation), result);
|
||||
verify(conversationDao).findAllByCreatorOrderByIdDesc("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getConversationShouldRejectAnotherCreator() {
|
||||
bindSubject("alice");
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> conversationService.getConversation(CONVERSATION_ID));
|
||||
verify(messageDao, never()).findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteConversationShouldRejectAnotherCreator() {
|
||||
bindSubject("alice");
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> conversationService.deleteConversation(CONVERSATION_ID));
|
||||
verify(sopScheduleDao, never()).deleteByConversationId(CONVERSATION_ID);
|
||||
verify(conversationDao, never()).deleteById(CONVERSATION_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamChatShouldRejectAnotherCreator() {
|
||||
bindSubject("alice");
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> conversationService.streamChat("Current question", CONVERSATION_ID));
|
||||
verify(messageDao, never()).save(any(ChatMessage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConversationShouldRecordCurrentCreator() {
|
||||
bindSubject("alice");
|
||||
when(conversationDao.save(any(ChatConversation.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
ChatConversation conversation = conversationService.createConversation();
|
||||
|
||||
assertEquals("alice", conversation.getCreator());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveSecurityDataShouldRejectAnotherCreator() {
|
||||
bindSubject("alice");
|
||||
SecurityData securityData = new SecurityData();
|
||||
securityData.setConversationId(CONVERSATION_ID);
|
||||
securityData.setSecurityData("sensitive-value");
|
||||
when(conversationDao.findByIdAndCreator(CONVERSATION_ID, "alice"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertFalse(conversationService.saveSecurityData(securityData));
|
||||
verify(conversationDao, never()).save(any(ChatConversation.class));
|
||||
}
|
||||
|
||||
private SubjectSum bindSubject(String principal) {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
when(subject.getPrincipal()).thenReturn(principal);
|
||||
SurenessContextHolder.bindSubject(subject);
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
|
||||
+147
-19
@@ -1,58 +1,186 @@
|
||||
/*
|
||||
* 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
|
||||
* 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.
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that SOP schedules with no future execution time are not persisted.
|
||||
* Ownership and scheduling contracts for user-facing SOP schedule operations.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SopScheduleServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private SopScheduleDao sopScheduleDao;
|
||||
private SopScheduleDao scheduleDao;
|
||||
|
||||
@InjectMocks
|
||||
private SopScheduleServiceImpl scheduleService;
|
||||
@Mock
|
||||
private ChatConversationDao conversationDao;
|
||||
|
||||
private SopScheduleServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SopScheduleServiceImpl(scheduleDao, conversationDao);
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
lenient().when(subject.getPrincipal()).thenReturn("alice");
|
||||
SurenessContextHolder.bindSubject(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearSubject() {
|
||||
SurenessContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShouldNotTrustRequestCreator() {
|
||||
SopSchedule request = schedule(1L, "bob");
|
||||
when(conversationDao.findByIdAndCreator(10L, "alice"))
|
||||
.thenReturn(Optional.of(ChatConversation.builder()
|
||||
.id(10L)
|
||||
.creator("alice")
|
||||
.build()));
|
||||
when(scheduleDao.save(any(SopSchedule.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
SopSchedule created = service.createSchedule(request);
|
||||
|
||||
assertEquals("alice", created.getCreator());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getShouldHideAnotherCreatorsSchedule() {
|
||||
when(scheduleDao.findByIdAndCreator(1L, "alice")).thenReturn(Optional.empty());
|
||||
|
||||
assertNull(service.getSchedule(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listShouldRejectAnotherCreatorsConversation() {
|
||||
when(conversationDao.findByIdAndCreator(10L, "alice")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.getSchedulesByConversation(10L));
|
||||
verify(scheduleDao, never()).findByConversationIdAndCreator(10L, "alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteShouldNotRemoveAnotherCreatorsSchedule() {
|
||||
when(scheduleDao.findByIdAndCreator(1L, "alice")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> service.deleteSchedule(1L));
|
||||
verify(scheduleDao, never()).delete(any(SopSchedule.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAndToggleShouldNotModifyAnotherCreatorsSchedule() {
|
||||
when(scheduleDao.findByIdAndCreator(1L, "alice")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.updateSchedule(schedule(1L, "bob")));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.toggleSchedule(1L, true));
|
||||
verify(scheduleDao, never()).save(any(SopSchedule.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backgroundExecutionShouldDisableMissingOwner() {
|
||||
SopSchedule schedule = schedule(1L, "legacy-owner");
|
||||
schedule.setEnabled(true);
|
||||
when(scheduleDao.findById(1L)).thenReturn(Optional.of(schedule));
|
||||
when(conversationDao.findByIdAndCreator(10L, "legacy-owner"))
|
||||
.thenReturn(Optional.empty());
|
||||
when(scheduleDao.save(schedule)).thenReturn(schedule);
|
||||
|
||||
assertNull(service.getScheduleForExecution(1L));
|
||||
assertFalse(schedule.getEnabled());
|
||||
verify(scheduleDao).save(schedule);
|
||||
}
|
||||
|
||||
@Test
|
||||
void backgroundExecutionUsesPersistedOwnerWithoutRequestSubject() {
|
||||
SurenessContextHolder.clear();
|
||||
SopSchedule schedule = schedule(1L, "alice");
|
||||
schedule.setEnabled(true);
|
||||
when(scheduleDao.findById(1L)).thenReturn(Optional.of(schedule));
|
||||
when(conversationDao.findByIdAndCreator(10L, "alice"))
|
||||
.thenReturn(Optional.of(ChatConversation.builder()
|
||||
.id(10L)
|
||||
.creator("alice")
|
||||
.build()));
|
||||
|
||||
assertSame(schedule, service.getScheduleForExecution(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScheduleShouldRejectCronWithoutFutureExecutionTime() {
|
||||
SopSchedule schedule = SopSchedule.builder()
|
||||
.conversationId(1L)
|
||||
.conversationId(10L)
|
||||
.sopName("daily_inspection")
|
||||
.cronExpression("0 0 0 31 2 *")
|
||||
.build();
|
||||
when(conversationDao.findByIdAndCreator(10L, "alice"))
|
||||
.thenReturn(Optional.of(ChatConversation.builder()
|
||||
.id(10L)
|
||||
.creator("alice")
|
||||
.build()));
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class, () -> scheduleService.createSchedule(schedule));
|
||||
IllegalArgumentException.class, () -> service.createSchedule(schedule));
|
||||
|
||||
assertTrue(exception.getMessage().contains("no future execution time"));
|
||||
verifyNoInteractions(sopScheduleDao);
|
||||
verifyNoInteractions(scheduleDao);
|
||||
}
|
||||
|
||||
private SopSchedule schedule(Long id, String creator) {
|
||||
return SopSchedule.builder()
|
||||
.id(id)
|
||||
.conversationId(10L)
|
||||
.sopName("daily_inspection")
|
||||
.cronExpression("0 0 9 * * ?")
|
||||
.creator(creator)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.tools.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
import org.apache.hertzbeat.manager.service.AppService;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that protected monitor creation cannot load another user's
|
||||
* conversation credentials.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MonitorToolsImplTest {
|
||||
|
||||
@Mock
|
||||
private MonitorService monitorService;
|
||||
|
||||
@Mock
|
||||
private AppService appService;
|
||||
|
||||
@Mock
|
||||
private ChatConversationDao conversationDao;
|
||||
|
||||
@InjectMocks
|
||||
private MonitorToolsImpl monitorTools;
|
||||
|
||||
@AfterEach
|
||||
void clearContext() {
|
||||
McpContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void protectedAddShouldRejectConversationOutsideCurrentCreator() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
when(subject.getPrincipal()).thenReturn("alice");
|
||||
McpContextHolder.setSubject(subject);
|
||||
when(conversationDao.findByIdAndCreator(10L, "alice")).thenReturn(Optional.empty());
|
||||
|
||||
String result = monitorTools.addMonitorProtected(
|
||||
10L, "database", "mysql", 60, "{\"host\":\"db.local\"}", null);
|
||||
|
||||
assertEquals("Error: Conversation not found or inaccessible", result);
|
||||
verify(conversationDao).findByIdAndCreator(10L, "alice");
|
||||
verifyNoInteractions(monitorService);
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,11 @@
|
||||
<version>${easy-poi.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.afterturn</groupId>
|
||||
<artifactId>easypoi-base</artifactId>
|
||||
<version>${easy-poi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.huaweicloud.sdk</groupId>
|
||||
<artifactId>huaweicloud-sdk-smn</artifactId>
|
||||
|
||||
+27
-39
@@ -19,62 +19,50 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.support.SseEmitterRegistry;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* SSE manager for alert
|
||||
* SSE manager for alert.
|
||||
*
|
||||
* <p>Note: the lifecycle of a subscription - its timeout, the ceiling on how many may be held
|
||||
* and the cleanup of the ones that went away - belongs to {@link SseEmitterRegistry}; what is
|
||||
* alert specific is only the event these subscribers are waiting for.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AlertSseManager {
|
||||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
||||
|
||||
private static final String ALERT_EVENT = "ALERT_EVENT";
|
||||
|
||||
private final SseEmitterRegistry registry = new SseEmitterRegistry("alert");
|
||||
|
||||
/**
|
||||
* Registers a subscription for the given client.
|
||||
*
|
||||
* @param clientId Identifier of the subscriber, unique per subscription
|
||||
* @return The emitter the controller returns to spring
|
||||
*/
|
||||
public SseEmitter createEmitter(Long clientId) {
|
||||
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
|
||||
emitter.onCompletion(() -> removeEmitter(clientId));
|
||||
emitter.onTimeout(() -> removeEmitter(clientId));
|
||||
emitter.onError((ex) -> removeEmitter(clientId));
|
||||
emitters.put(clientId, emitter);
|
||||
return emitter;
|
||||
return registry.createEmitter(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers one alert to every live subscriber.
|
||||
*
|
||||
* @param data Serialised alert payload
|
||||
*/
|
||||
@Async
|
||||
public void broadcast(String data) {
|
||||
emitters.forEach((clientId, emitter) -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.id(String.valueOf(System.currentTimeMillis()))
|
||||
.name("ALERT_EVENT")
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
}
|
||||
});
|
||||
registry.broadcast(ALERT_EVENT, data);
|
||||
}
|
||||
|
||||
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||
} catch (Throwable e) {
|
||||
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
// execute clear
|
||||
removeEmitter(clientId);
|
||||
void setMaxEmitters(int maxEmitters) {
|
||||
registry.setMaxEmitters(maxEmitters);
|
||||
}
|
||||
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
int subscriptionCount() {
|
||||
return registry.subscriptionCount();
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -21,6 +21,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
@@ -63,6 +64,17 @@ public class AlertsController {
|
||||
return ResponseEntity.ok(Message.success(alertPage));
|
||||
}
|
||||
|
||||
@GetMapping("/export")
|
||||
@Operation(summary = "Export Alarms", description = "Export single alarms matching the filters as an Excel sheet")
|
||||
public void exportAlerts(
|
||||
@Parameter(description = "Alarm Status", example = "resolved") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "Alarm content fuzzy query", example = "linux") @RequestParam(required = false) String search,
|
||||
@Parameter(description = "Sort field, default activeAt", example = "activeAt") @RequestParam(defaultValue = "activeAt") String sort,
|
||||
@Parameter(description = "Sort Type", example = "desc") @RequestParam(defaultValue = "desc") String order,
|
||||
HttpServletResponse response) {
|
||||
alertService.exportSingleAlerts(status, search, sort, order, response);
|
||||
}
|
||||
|
||||
@GetMapping("/group")
|
||||
@Operation(summary = "Query Group Alarms")
|
||||
public ResponseEntity<Message<Page<GroupAlert>>> getGroupAlerts(
|
||||
|
||||
+20
@@ -29,8 +29,11 @@ import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||
import org.apache.hertzbeat.alert.notice.NoticeTemplateRenderer;
|
||||
import org.apache.hertzbeat.alert.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -55,6 +58,9 @@ public class NoticeConfigController {
|
||||
@Autowired
|
||||
private NoticeConfigService noticeConfigService;
|
||||
|
||||
@Autowired
|
||||
private AlerterProperties alerterProperties;
|
||||
|
||||
@PostMapping(path = "/receiver")
|
||||
@Operation(summary = "Add a recipient", description = "Add a recipient")
|
||||
public ResponseEntity<Message<Void>> addNewNoticeReceiver(@Valid @RequestBody NoticeReceiver noticeReceiver) {
|
||||
@@ -228,4 +234,18 @@ public class NoticeConfigController {
|
||||
}
|
||||
return ResponseEntity.ok(Message.fail(FAIL_CODE, "Notify service not available, please check config!"));
|
||||
}
|
||||
|
||||
@PostMapping(path = "/template/preview")
|
||||
@Operation(summary = "Preview how a notice template renders against a sample alert",
|
||||
description = "Preview how a notice template renders against a sample alert, without sending anything")
|
||||
public ResponseEntity<Message<String>> previewNoticeTemplate(@Valid @RequestBody NoticeTemplate noticeTemplate) {
|
||||
try {
|
||||
String rendered = NoticeTemplateRenderer.renderContent(
|
||||
noticeTemplate, NoticeTemplateRenderer.sampleGroupAlert(), alerterProperties.getConsoleUrl(),
|
||||
ResourceBundleUtil.getBundle("alerter"));
|
||||
return ResponseEntity.ok(Message.successWithData(rendered));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(Message.fail(FAIL_CODE, "Failed to render template: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.dto;
|
||||
|
||||
import cn.afterturn.easypoi.excel.annotation.Excel;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* A SingleAlert with its Map and timestamp fields pre-rendered to strings, since easypoi cannot map those to cells.
|
||||
*/
|
||||
@Data
|
||||
public class SingleAlertExportDTO {
|
||||
|
||||
@Excel(name = "Status", width = 12)
|
||||
private String status;
|
||||
|
||||
@Excel(name = "Content", width = 60)
|
||||
private String content;
|
||||
|
||||
@Excel(name = "Labels", width = 40)
|
||||
private String labels;
|
||||
|
||||
@Excel(name = "Annotations", width = 40)
|
||||
private String annotations;
|
||||
|
||||
@Excel(name = "Fingerprint", width = 24)
|
||||
private String fingerprint;
|
||||
|
||||
@Excel(name = "Trigger Times", width = 12)
|
||||
private Integer triggerTimes;
|
||||
|
||||
@Excel(name = "Start At", width = 20)
|
||||
private String startAt;
|
||||
|
||||
@Excel(name = "Active At", width = 20)
|
||||
private String activeAt;
|
||||
|
||||
@Excel(name = "End At", width = 20)
|
||||
private String endAt;
|
||||
}
|
||||
+9
-5
@@ -39,6 +39,11 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
private static final String VALUE = "__value__";
|
||||
private static final String TIMESTAMP = "__timestamp__";
|
||||
|
||||
/**
|
||||
* Every statement this visitor evaluates goes to this executor, so whether a statement is
|
||||
* allowed to run is decided there rather than at each visit method, see
|
||||
* {@code DataSourceServiceImpl}.
|
||||
*/
|
||||
private final QueryExecutor executor;
|
||||
private final CommonTokenStream tokens;
|
||||
|
||||
@@ -259,17 +264,16 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
|
||||
return callSqlOrPromql(tokens.getText(ctx.string()));
|
||||
return executor.execute(unquote(tokens.getText(ctx.string())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
|
||||
return callSqlOrPromql(tokens.getText(ctx.string()));
|
||||
return executor.execute(unquote(tokens.getText(ctx.string())));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> callSqlOrPromql(String text) {
|
||||
String script = text.substring(1, text.length() - 1);
|
||||
return executor.execute(script);
|
||||
private String unquote(String text) {
|
||||
return text.substring(1, text.length() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.notice;
|
||||
|
||||
import freemarker.cache.StringTemplateLoader;
|
||||
import freemarker.core.TemplateClassResolver;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
|
||||
|
||||
/**
|
||||
* Renders a {@link NoticeTemplate} against a {@link GroupAlert} using FreeMarker.
|
||||
*
|
||||
* <p>This is shared between the real notify dispatch path ({@code AlertNotifyHandler}
|
||||
* implementations) and the notice template preview endpoint, so both render a template
|
||||
* exactly the same way.
|
||||
*/
|
||||
public final class NoticeTemplateRenderer {
|
||||
|
||||
private static final String NUMBER_FORMAT = "0";
|
||||
|
||||
private NoticeTemplateRenderer() {
|
||||
}
|
||||
|
||||
public static String renderContent(NoticeTemplate noticeTemplate, GroupAlert alert, String consoleUrl,
|
||||
ResourceBundle bundle) throws TemplateException, IOException {
|
||||
StringTemplateLoader stringLoader = new StringTemplateLoader();
|
||||
Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
|
||||
cfg.setNumberFormat(NUMBER_FORMAT);
|
||||
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||
Map<String, Object> model = new HashMap<>(16);
|
||||
model.put("title", bundle.getString("alerter.notify.title"));
|
||||
model.put("status", alert.getStatus());
|
||||
model.put("groupLabels", alert.getGroupLabels());
|
||||
model.put("commonLabels", alert.getCommonLabels());
|
||||
model.put("commonAnnotations", alert.getCommonAnnotations());
|
||||
model.put("alerts", alert.getAlerts());
|
||||
if (consoleUrl != null) {
|
||||
model.put("consoleUrl", consoleUrl);
|
||||
}
|
||||
// TODO Single instance reuse cache considers multiple-threading issues
|
||||
String templateName = "freeMakerTemplate";
|
||||
stringLoader.putTemplate(templateName, noticeTemplate.getContent());
|
||||
cfg.setTemplateLoader(stringLoader);
|
||||
freemarker.template.Template templateRes = cfg.getTemplate(templateName, Locale.CHINESE);
|
||||
String template = FreeMarkerTemplateUtils.processTemplateIntoString(templateRes, model);
|
||||
return template.replaceAll("((\r\n)|\n)[\\s\t ]*(\\1)+", "$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a representative {@link GroupAlert} for previewing a template, so a user can see
|
||||
* what a real notification would look like without waiting for (or faking) a real alert.
|
||||
*/
|
||||
public static GroupAlert sampleGroupAlert() {
|
||||
long now = System.currentTimeMillis();
|
||||
SingleAlert singleAlert = SingleAlert.builder()
|
||||
.labels(Map.of("alertname", "HighCPUUsage", "instance", "server1.example.com", "severity", "critical"))
|
||||
.annotations(Map.of("summary", "High CPU usage detected"))
|
||||
.content("CPU usage is above 80% for the last 5 minutes on instance server1.example.com.")
|
||||
.status("firing")
|
||||
.triggerTimes(1)
|
||||
.startAt(now)
|
||||
.activeAt(now)
|
||||
.build();
|
||||
return GroupAlert.builder()
|
||||
.groupKey("HighCPUUsage{alertname=\"HighCPUUsage\", instance=\"server1.example.com\"}")
|
||||
.status("firing")
|
||||
.groupLabels(Map.of("alertname", "HighCPUUsage"))
|
||||
.commonLabels(Map.of("alertname", "HighCPUUsage", "instance", "server1.example.com", "severity", "critical"))
|
||||
.commonAnnotations(Map.of("summary", "High CPU usage detected"))
|
||||
.gmtCreate(LocalDateTime.now())
|
||||
.alerts(List.of(singleAlert))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+3
-29
@@ -17,14 +17,8 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.notice.impl;
|
||||
|
||||
import freemarker.cache.StringTemplateLoader;
|
||||
import freemarker.core.TemplateClassResolver;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||
@@ -33,9 +27,9 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent;
|
||||
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||
import org.apache.hertzbeat.alert.notice.AlertNotifyHandler;
|
||||
import org.apache.hertzbeat.alert.notice.NoticeTemplateRenderer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
@@ -53,28 +47,8 @@ abstract class AbstractAlertNotifyHandlerImpl implements AlertNotifyHandler {
|
||||
|
||||
|
||||
protected String renderContent(NoticeTemplate noticeTemplate, GroupAlert alert) throws TemplateException, IOException {
|
||||
StringTemplateLoader stringLoader = new StringTemplateLoader();
|
||||
freemarker.template.Template templateRes;
|
||||
Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
|
||||
cfg.setNumberFormat(NUMBER_FORMAT);
|
||||
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||
Map<String, Object> model = new HashMap<>(16);
|
||||
model.put("title", bundle.getString("alerter.notify.title"));
|
||||
model.put("status", alert.getStatus());
|
||||
model.put("groupLabels", alert.getGroupLabels());
|
||||
model.put("commonLabels", alert.getCommonLabels());
|
||||
model.put("commonAnnotations", alert.getCommonAnnotations());
|
||||
model.put("alerts", alert.getAlerts());
|
||||
if (alerterProperties != null) {
|
||||
model.put("consoleUrl", alerterProperties.getConsoleUrl());
|
||||
}
|
||||
// TODO Single instance reuse cache considers multiple-threading issues
|
||||
String templateName = "freeMakerTemplate";
|
||||
stringLoader.putTemplate(templateName, noticeTemplate.getContent());
|
||||
cfg.setTemplateLoader(stringLoader);
|
||||
templateRes = cfg.getTemplate(templateName, Locale.CHINESE);
|
||||
String template = FreeMarkerTemplateUtils.processTemplateIntoString(templateRes, model);
|
||||
return template.replaceAll("((\r\n)|\n)[\\s\t ]*(\\1)+", "$1");
|
||||
String consoleUrl = alerterProperties != null ? alerterProperties.getConsoleUrl() : null;
|
||||
return NoticeTemplateRenderer.renderContent(noticeTemplate, alert, consoleUrl, bundle);
|
||||
}
|
||||
|
||||
protected String escapeJsonStr(String jsonStr){
|
||||
|
||||
+13
@@ -98,6 +98,7 @@ public class EmailAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
||||
Properties props = sender.getJavaMailProperties();
|
||||
props.put("mail.smtp.ssl.enable", emailNoticeSenderConfig.isEmailSsl());
|
||||
props.put("mail.smtp.starttls.enable", emailNoticeSenderConfig.isEmailStarttls());
|
||||
applySslCertVerify(props, emailNoticeSenderConfig.isEmailSslCertVerify());
|
||||
fromUsername = emailNoticeSenderConfig.getEmailUsername();
|
||||
useDatabase = true;
|
||||
}
|
||||
@@ -111,6 +112,7 @@ public class EmailAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
||||
Properties props = sender.getJavaMailProperties();
|
||||
props.put("mail.smtp.ssl.enable", sslEnable);
|
||||
props.put("mail.smtp.starttls.enable", starttlsEnable);
|
||||
applySslCertVerify(props, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Type not found {}", e.getMessage());
|
||||
@@ -133,6 +135,17 @@ public class EmailAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
||||
}
|
||||
}
|
||||
|
||||
// the sender is a singleton, so both branches must set the props to avoid stale state
|
||||
private void applySslCertVerify(Properties props, boolean verify) {
|
||||
if (verify) {
|
||||
props.remove("mail.smtp.ssl.trust");
|
||||
props.remove("mail.smtp.ssl.checkserveridentity");
|
||||
} else {
|
||||
props.put("mail.smtp.ssl.trust", "*");
|
||||
props.put("mail.smtp.ssl.checkserveridentity", "false");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte type() {
|
||||
return 1;
|
||||
|
||||
+25
-15
@@ -171,7 +171,7 @@ public class AlarmGroupReduce implements DisposableBean {
|
||||
.factory());
|
||||
}
|
||||
|
||||
private void runCheckAndSendGroups() {
|
||||
void runCheckAndSendGroups() {
|
||||
beforeCheckAndSendGroupsRun();
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -179,7 +179,6 @@ public class AlarmGroupReduce implements DisposableBean {
|
||||
if (shouldSendGroup(cache, now)) {
|
||||
sendGroupAlert(cache);
|
||||
cache.setLastSendTime(now);
|
||||
cache.getAlertFingerprints().clear();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
@@ -262,22 +261,19 @@ public class AlarmGroupReduce implements DisposableBean {
|
||||
return newCache;
|
||||
});
|
||||
String fingerprint = alert.getFingerprint();
|
||||
// Check if this is a duplicate alert
|
||||
// Preserve the original startAt when updating an alert that is still tracked
|
||||
SingleAlert existingAlert = cache.getAlertFingerprints().get(fingerprint);
|
||||
if (existingAlert != null) {
|
||||
// Update existing alert timestamp
|
||||
alert.setStartAt(existingAlert.getStartAt());
|
||||
cache.getAlertFingerprints().put(fingerprint, alert);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new alert
|
||||
// Add or update the alert. The cache retains every currently-active alert of the
|
||||
// group, so the group status is always computed over the full member set rather
|
||||
// than only the alerts received within the current send window.
|
||||
cache.getAlertFingerprints().put(fingerprint, alert);
|
||||
|
||||
|
||||
if (shouldSendGroupImmediately(cache)) {
|
||||
sendGroupAlert(cache);
|
||||
cache.setLastSendTime(System.currentTimeMillis());
|
||||
cache.getAlertFingerprints().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,21 +284,27 @@ public class AlarmGroupReduce implements DisposableBean {
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
String status = determineGroupStatus(cache.getAlertFingerprints().values());
|
||||
|
||||
|
||||
boolean hasResolvedAlert = cache.getAlertFingerprints().values().stream()
|
||||
.anyMatch(alert -> CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus()));
|
||||
|
||||
// For firing alerts, check repeat interval
|
||||
if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) {
|
||||
AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName());
|
||||
long repeatInterval = ruleConfig.getRepeatInterval() != null
|
||||
? ruleConfig.getRepeatInterval() * MS_PER_SECOND : DEFAULT_REPEAT_INTERVAL;
|
||||
|
||||
// Skip if within repeat interval
|
||||
if (cache.getLastRepeatTime() > 0
|
||||
|
||||
// Skip if within repeat interval. The throttle only suppresses repeated firing
|
||||
// notifications; it must never swallow a pending resolved transition, so we still
|
||||
// send when the batch carries a member that has just recovered.
|
||||
if (!hasResolvedAlert
|
||||
&& cache.getLastRepeatTime() > 0
|
||||
&& now - cache.getLastRepeatTime() < repeatInterval) {
|
||||
return;
|
||||
}
|
||||
cache.setLastRepeatTime(now);
|
||||
}
|
||||
|
||||
|
||||
GroupAlert groupAlert = GroupAlert.builder()
|
||||
.groupKey(cache.getGroupKey())
|
||||
.groupLabels(cache.getGroupLabels())
|
||||
@@ -313,6 +315,14 @@ public class AlarmGroupReduce implements DisposableBean {
|
||||
.build();
|
||||
|
||||
alarmInhibitReduce.inhibitAlarm(groupAlert);
|
||||
|
||||
// The resolved members have now been emitted, so drop them from the group. Firing
|
||||
// members are retained until they recover, keeping the group firing while any member
|
||||
// is still active instead of flushing the whole cache after every send.
|
||||
if (hasResolvedAlert) {
|
||||
cache.getAlertFingerprints().values().removeIf(
|
||||
alert -> CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldSendGroup(GroupAlertCache cache, long now) {
|
||||
|
||||
+12
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
@@ -40,7 +41,17 @@ public interface AlertService {
|
||||
* @return single alerts
|
||||
*/
|
||||
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
|
||||
|
||||
|
||||
/**
|
||||
* export single alerts matching the filters to an Excel sheet
|
||||
* @param status status
|
||||
* @param search search
|
||||
* @param sort sort
|
||||
* @param order order
|
||||
* @param response servlet response the Excel sheet is written to
|
||||
*/
|
||||
void exportSingleAlerts(String status, String search, String sort, String order, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* Dynamic conditional query
|
||||
* @param status Alarm Status
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
|
||||
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
|
||||
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Applies the resource budget shared by alert previews and periodic evaluation.
|
||||
*
|
||||
* <p>The query endpoint uses warehouse credentials and an alert definition runs repeatedly,
|
||||
* so accepting a read-only query is not sufficient on its own. This wrapper limits the input,
|
||||
* the lookback selected by PromQL or Greptime range syntax, and the rows returned to the alert
|
||||
* evaluator. The warehouse client supplies the execution timeout; these checks bound the work
|
||||
* requested and the data retained by the caller.
|
||||
*/
|
||||
final class AlertQueryBudgetExecutor implements QueryExecutor {
|
||||
|
||||
static final int MAX_QUERY_LENGTH = 8_192;
|
||||
|
||||
static final int MAX_RESULT_ROWS = 1_000;
|
||||
|
||||
private static final BigInteger MAX_RANGE_MILLIS = BigInteger.valueOf(86_400_000L);
|
||||
|
||||
private static final Pattern PROMQL_RANGE = Pattern.compile(
|
||||
"\\[\\s*([0-9]+(?:ms|[smhdwy])(?:\\s*[0-9]+(?:ms|[smhdwy]))*)\\s*(?::[^]]*)?]",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern SQL_RANGE = Pattern.compile(
|
||||
"\\bRANGE\\s*['\"]\\s*([^'\"]+)\\s*['\"]",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern DURATION_PART = Pattern.compile("([0-9]+)(ms|[smhdwy])",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final QueryExecutor delegate;
|
||||
|
||||
AlertQueryBudgetExecutor(QueryExecutor delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
static void validateInput(String query) {
|
||||
if (query.length() > MAX_QUERY_LENGTH) {
|
||||
throw new AlertExpressionException("Alert query exceeds the 8192 character limit.");
|
||||
}
|
||||
validateRanges(PROMQL_RANGE.matcher(query));
|
||||
validateRanges(SQL_RANGE.matcher(query));
|
||||
}
|
||||
|
||||
private static void validateRanges(Matcher ranges) {
|
||||
while (ranges.find()) {
|
||||
String duration = ranges.group(1).replaceAll("\\s+", "");
|
||||
if (durationMillis(duration).compareTo(MAX_RANGE_MILLIS) > 0) {
|
||||
throw new AlertExpressionException("Alert query range exceeds the one day limit.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BigInteger durationMillis(String duration) {
|
||||
Matcher parts = DURATION_PART.matcher(duration);
|
||||
BigInteger total = BigInteger.ZERO;
|
||||
int end = 0;
|
||||
while (parts.find()) {
|
||||
if (parts.start() != end) {
|
||||
return BigInteger.ZERO;
|
||||
}
|
||||
BigInteger value = new BigInteger(parts.group(1));
|
||||
total = total.add(value.multiply(unitMillis(parts.group(2))));
|
||||
end = parts.end();
|
||||
}
|
||||
return end == duration.length() ? total : BigInteger.ZERO;
|
||||
}
|
||||
|
||||
private static BigInteger unitMillis(String unit) {
|
||||
return switch (unit.toLowerCase(Locale.ROOT)) {
|
||||
case "ms" -> BigInteger.ONE;
|
||||
case "s" -> BigInteger.valueOf(1_000L);
|
||||
case "m" -> BigInteger.valueOf(60_000L);
|
||||
case "h" -> BigInteger.valueOf(3_600_000L);
|
||||
case "d" -> BigInteger.valueOf(86_400_000L);
|
||||
case "w" -> BigInteger.valueOf(604_800_000L);
|
||||
case "y" -> BigInteger.valueOf(31_536_000_000L);
|
||||
default -> BigInteger.ZERO;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> execute(String query) {
|
||||
validateInput(query);
|
||||
List<Map<String, Object>> rows = delegate.execute(query);
|
||||
if (rows != null && rows.size() > MAX_RESULT_ROWS) {
|
||||
throw new AlertExpressionException("Alert query returned more than 1000 rows.");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatasourceQueryData query(DatasourceQuery datasourceQuery) {
|
||||
return delegate.query(datasourceQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDatasource() {
|
||||
return delegate.getDatasource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String queryLanguage) {
|
||||
return delegate.support(queryLanguage);
|
||||
}
|
||||
}
|
||||
+69
-4
@@ -17,21 +17,33 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import cn.afterturn.easypoi.excel.ExcelExportUtil;
|
||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.dao.GroupAlertDao;
|
||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
import org.apache.hertzbeat.alert.dto.SingleAlertExportDTO;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.service.AlertService;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -47,7 +59,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Slf4j
|
||||
public class AlertServiceImpl implements AlertService {
|
||||
|
||||
|
||||
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Autowired
|
||||
private GroupAlertDao groupAlertDao;
|
||||
|
||||
@@ -59,7 +73,13 @@ public class AlertServiceImpl implements AlertService {
|
||||
|
||||
@Override
|
||||
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
|
||||
Specification<SingleAlert> specification = (root, query, criteriaBuilder) -> {
|
||||
Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort));
|
||||
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
|
||||
return singleAlertDao.findAll(buildSingleAlertSpecification(status, search), pageRequest);
|
||||
}
|
||||
|
||||
private Specification<SingleAlert> buildSingleAlertSpecification(String status, String search) {
|
||||
return (root, query, criteriaBuilder) -> {
|
||||
List<Predicate> andList = new ArrayList<>();
|
||||
if (status != null) {
|
||||
Predicate predicate = criteriaBuilder.equal(root.get("status"), status);
|
||||
@@ -88,9 +108,54 @@ public class AlertServiceImpl implements AlertService {
|
||||
return query.where(andPredicate, orPredicate).getRestriction();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportSingleAlerts(String status, String search, String sort, String order, HttpServletResponse response) {
|
||||
Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort));
|
||||
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
|
||||
return singleAlertDao.findAll(specification, pageRequest);
|
||||
List<SingleAlert> alerts = singleAlertDao.findAll(buildSingleAlertSpecification(status, search), sortExp);
|
||||
// easypoi mutates the list in place, so it must be mutable (not Stream.toList()).
|
||||
List<SingleAlertExportDTO> rows = alerts.stream().map(this::toExportRow).collect(Collectors.toList());
|
||||
try (Workbook workbook = ExcelExportUtil.exportExcel(
|
||||
new ExportParams("Alert Records", "Alerts", ExcelType.XSSF), SingleAlertExportDTO.class, rows)) {
|
||||
String fileName = "hertzbeat_alerts_" + System.currentTimeMillis() + ".xlsx";
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
|
||||
workbook.write(response.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
log.error("export alerts to excel error: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to export alerts", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SingleAlertExportDTO toExportRow(SingleAlert alert) {
|
||||
SingleAlertExportDTO row = new SingleAlertExportDTO();
|
||||
row.setStatus(alert.getStatus());
|
||||
row.setContent(alert.getContent());
|
||||
row.setLabels(mapToString(alert.getLabels()));
|
||||
row.setAnnotations(mapToString(alert.getAnnotations()));
|
||||
row.setFingerprint(alert.getFingerprint());
|
||||
row.setTriggerTimes(alert.getTriggerTimes());
|
||||
row.setStartAt(formatEpochMilli(alert.getStartAt()));
|
||||
row.setActiveAt(formatEpochMilli(alert.getActiveAt()));
|
||||
row.setEndAt(formatEpochMilli(alert.getEndAt()));
|
||||
return row;
|
||||
}
|
||||
|
||||
private String mapToString(Map<String, String> map) {
|
||||
if (map == null || map.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return map.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + "=" + entry.getValue())
|
||||
.collect(Collectors.joining("; "));
|
||||
}
|
||||
|
||||
private String formatEpochMilli(Long epochMilli) {
|
||||
if (epochMilli == null) {
|
||||
return "";
|
||||
}
|
||||
return EXPORT_TIME_FORMATTER.format(Instant.ofEpochMilli(epochMilli).atZone(ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+22
-19
@@ -78,27 +78,30 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// Extract alert info
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
sendSms(receiver.getPhone(), buildTemplateParam(alert));
|
||||
}
|
||||
|
||||
// Aliyun rejects the whole request when any template variable is null or blank,
|
||||
// so every value must fall back to non-blank text
|
||||
String buildTemplateParam(GroupAlert alert) {
|
||||
Map<String, String> labels = alert.getCommonLabels() == null ? Map.of() : alert.getCommonLabels();
|
||||
Map<String, String> annotations = alert.getCommonAnnotations() == null ? Map.of() : alert.getCommonAnnotations();
|
||||
|
||||
Map<String, String> templateParam = new HashMap<>();
|
||||
templateParam.put("instance", firstNonBlank(labels.get("instance"), alert.getGroupKey(), "unknown"));
|
||||
templateParam.put("priority", firstNonBlank(labels.get("priority"), "unknown"));
|
||||
templateParam.put("content", firstNonBlank(annotations.get("summary"), annotations.get("description"),
|
||||
annotations.values().stream().findFirst().orElse(null), "alert triggered"));
|
||||
return JsonUtil.toJson(templateParam);
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Build template parameters
|
||||
Map<String, String> templateParam = new HashMap<>();
|
||||
templateParam.put("instance", instance == null ? alert.getGroupKey() : instance);
|
||||
templateParam.put("priority", priority == null ? "unknown" : priority);
|
||||
templateParam.put("content", content);
|
||||
|
||||
sendSms(receiver.getPhone(), JsonUtil.toJson(templateParam));
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private void sendSms(String phoneNumber, String templateParam) {
|
||||
|
||||
+28
-22
@@ -31,7 +31,6 @@ import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
|
||||
import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
|
||||
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||
import org.apache.hertzbeat.common.support.valid.SqlSecurityException;
|
||||
import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator;
|
||||
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||
import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
|
||||
@@ -58,6 +57,13 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
*/
|
||||
private static final List<String> DEFAULT_ALLOWED_TABLES = List.of(WarehouseConstants.LOG_TABLE_NAME);
|
||||
|
||||
/**
|
||||
* The policy for an alert expression is read only within the configured database. Metric
|
||||
* tables are created per metric on demand, so an exact table whitelist would reject every
|
||||
* legitimate metric query.
|
||||
*/
|
||||
private static final SqlSecurityValidator EXPRESSION_SQL_VALIDATOR = SqlSecurityValidator.selectOnly();
|
||||
|
||||
protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
|
||||
|
||||
@Setter
|
||||
@@ -82,6 +88,7 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
if (!StringUtils.hasText(expr)) {
|
||||
throw new IllegalArgumentException("Empty expression");
|
||||
}
|
||||
AlertQueryBudgetExecutor.validateInput(expr);
|
||||
if (executors == null || executors.isEmpty()) {
|
||||
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
|
||||
}
|
||||
@@ -93,7 +100,7 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
// replace all white space
|
||||
expr = expr.replaceAll("\\s+", " ");
|
||||
try {
|
||||
return evaluate(expr, executor);
|
||||
return evaluate(expr, guardSql(new AlertQueryBudgetExecutor(executor), EXPRESSION_SQL_VALIDATOR));
|
||||
} catch (AlertExpressionException ae) {
|
||||
log.error("Calculate query parse error, datasource: {}, expr: {}, msg: {}", datasource, expr, ae.getMessage(), ae);
|
||||
throw ae;
|
||||
@@ -108,6 +115,7 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
if (!StringUtils.hasText(expr)) {
|
||||
throw new IllegalArgumentException("Empty expression");
|
||||
}
|
||||
AlertQueryBudgetExecutor.validateInput(expr);
|
||||
if (executors == null || executors.isEmpty()) {
|
||||
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
|
||||
}
|
||||
@@ -119,13 +127,11 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
// replace all white space
|
||||
expr = expr.replaceAll("\\s+", " ");
|
||||
|
||||
// SQL security validation for SQL-based datasources
|
||||
if (isSqlDatasource(datasource)) {
|
||||
validateSqlSecurity(expr);
|
||||
}
|
||||
|
||||
try {
|
||||
return executor.execute(expr);
|
||||
return guardSql(new AlertQueryBudgetExecutor(executor), sqlSecurityValidator).execute(expr);
|
||||
} catch (AlertExpressionException ae) {
|
||||
// A statement the policy rejected, whose message names the part it broke.
|
||||
throw ae;
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
|
||||
throw new AlertExpressionException(e.getMessage());
|
||||
@@ -133,22 +139,22 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the datasource is SQL-based
|
||||
* Wraps an executor that speaks sql so that nothing runs on it unvalidated.
|
||||
*
|
||||
* <p>The decision is made from the executor rather than from the datasource string the
|
||||
* caller passed, because the executor is what actually holds the database credentials.
|
||||
* A datasource that does not speak sql is handed back untouched: a promql endpoint takes
|
||||
* a query string, not a statement, and running it through a sql parser would only reject
|
||||
* valid promql.
|
||||
* @param executor Executor chosen for this datasource
|
||||
* @param validator Policy to enforce, read only for expressions and whitelisting for raw log queries
|
||||
* @return The executor, guarded when it speaks sql
|
||||
*/
|
||||
private boolean isSqlDatasource(String datasource) {
|
||||
return datasource != null && datasource.equalsIgnoreCase(WarehouseConstants.SQL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SQL statement for security
|
||||
*/
|
||||
private void validateSqlSecurity(String sql) {
|
||||
try {
|
||||
sqlSecurityValidator.validate(sql);
|
||||
} catch (SqlSecurityException e) {
|
||||
log.warn("SQL security validation failed: {}", e.getMessage());
|
||||
throw new AlertExpressionException("SQL security validation failed: " + e.getMessage());
|
||||
private QueryExecutor guardSql(QueryExecutor executor, SqlSecurityValidator validator) {
|
||||
if (!executor.support(WarehouseConstants.SQL)) {
|
||||
return executor;
|
||||
}
|
||||
return new SqlValidatingQueryExecutor(executor, validator);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
|
||||
|
||||
+2
-1
@@ -377,7 +377,8 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
|
||||
.status("firing")
|
||||
.build();
|
||||
GroupAlert groupAlert = GroupAlert.builder()
|
||||
.commonLabels(Map.of(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert"))
|
||||
.commonLabels(Map.of(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert",
|
||||
CommonConstants.LABEL_INSTANCE, "127.0.0.1"))
|
||||
.commonAnnotations(annotations)
|
||||
.alerts(List.of(singleAlert1, singleAlert2))
|
||||
.status("firing")
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery;
|
||||
import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
|
||||
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||
import org.apache.hertzbeat.common.support.valid.SqlSecurityException;
|
||||
import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator;
|
||||
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A sql executor that validates before it runs anything.
|
||||
*
|
||||
* <p>An alert expression reaches a query executor by several routes: the {@code sql("...")}
|
||||
* and {@code promql("...")} spellings both carry an arbitrary string, a bare select is
|
||||
* parsed by the expression grammar itself, and each of them is evaluated for the preview
|
||||
* endpoint and for the periodic evaluation loop alike. All of them end at
|
||||
* {@link QueryExecutor#execute(String)}, which runs the string with the server side database
|
||||
* credentials.
|
||||
*
|
||||
* <p>Guarding that one method rather than each route is what makes the check complete: the
|
||||
* spelling an expression happens to use does not decide whether the statement is checked,
|
||||
* the database it lands on does. It also means a route added later is covered without anyone
|
||||
* remembering to add a call.
|
||||
*/
|
||||
public class SqlValidatingQueryExecutor implements QueryExecutor {
|
||||
|
||||
private final QueryExecutor delegate;
|
||||
|
||||
private final SqlSecurityValidator validator;
|
||||
|
||||
public SqlValidatingQueryExecutor(QueryExecutor delegate, SqlSecurityValidator validator) {
|
||||
this.delegate = delegate;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> execute(String query) {
|
||||
try {
|
||||
validator.validate(query);
|
||||
} catch (SqlSecurityException e) {
|
||||
// AlertExpressionException rather than a generic failure: it is the type
|
||||
// DataSourceServiceImpl rethrows untouched and the preview endpoint turns into a
|
||||
// 400, so the author of the rule sees which part of the policy the statement broke
|
||||
throw new AlertExpressionException("SQL security validation failed: " + e.getMessage());
|
||||
}
|
||||
return delegate.execute(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatasourceQueryData query(DatasourceQuery datasourceQuery) {
|
||||
return delegate.query(datasourceQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDatasource() {
|
||||
return delegate.getDatasource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String queryLanguage) {
|
||||
return delegate.support(queryLanguage);
|
||||
}
|
||||
}
|
||||
+61
-31
@@ -17,25 +17,30 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.hertzbeat.common.support.SseEmitterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* alert sse manager test
|
||||
* Test case for {@link AlertSseManager}.
|
||||
*
|
||||
* <p>Note: how a subscription is bounded and cleaned up is covered by
|
||||
* {@code SseEmitterRegistryTest}; what is left here is what makes this stream the alert one.
|
||||
*/
|
||||
public class AlertSseManagerTest {
|
||||
class AlertSseManagerTest {
|
||||
|
||||
private AlertSseManager alertSseManager;
|
||||
|
||||
@@ -44,26 +49,51 @@ public class AlertSseManagerTest {
|
||||
alertSseManager = new AlertSseManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* The ui subscribes by event name, so an alert delivered under any other name reaches
|
||||
* nobody even though the connection is up.
|
||||
*/
|
||||
@Test
|
||||
void testCompleteThrowsException() throws Exception {
|
||||
SseEmitter emitter = alertSseManager.createEmitter(1L);
|
||||
assertNotNull(emitter);
|
||||
void testAlertsAreDeliveredUnderTheAlertEventName() throws Exception {
|
||||
alertSseManager.createEmitter(1L);
|
||||
final SseEmitter subscriber = mock(SseEmitter.class);
|
||||
emitters().put(1L, subscriber);
|
||||
|
||||
Map<Long, SseEmitter> emitters = new HashMap<>();
|
||||
SseEmitter spyEmitter = mock(SseEmitter.class);
|
||||
|
||||
doThrow(new IllegalStateException("Simulated output stream error")).when(spyEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
doThrow(new RuntimeException("Complete failed")).when(spyEmitter).complete();
|
||||
|
||||
emitters.put(1L, spyEmitter);
|
||||
alertSseManager.broadcast("{\"id\":1}");
|
||||
|
||||
Field emittersField = AlertSseManager.class.getDeclaredField("emitters");
|
||||
emittersField.setAccessible(true);
|
||||
emittersField.set(alertSseManager, emitters);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> alertSseManager.broadcast("{\"id\":1,\"content\":\"Test alert\"}"));
|
||||
Map<Long, SseEmitter> currentEmitters = (Map<Long, SseEmitter>) emittersField.get(alertSseManager);
|
||||
assertFalse(currentEmitters.containsKey(1L), "Emitter should still exist because complete() threw exception");
|
||||
final ArgumentCaptor<SseEmitter.SseEventBuilder> event =
|
||||
ArgumentCaptor.forClass(SseEmitter.SseEventBuilder.class);
|
||||
verify(subscriber).send(event.capture());
|
||||
final String rendered = event.getValue().build().stream()
|
||||
.map(part -> String.valueOf(part.getData()))
|
||||
.collect(Collectors.joining());
|
||||
assertTrue(rendered.contains("event:ALERT_EVENT"), "alerts must be delivered as ALERT_EVENT, was " + rendered);
|
||||
assertTrue(rendered.contains("{\"id\":1}"), "the alert payload must be delivered as is, was " + rendered);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* The manager has to hand its subscriptions to a registry rather than hold them itself,
|
||||
* otherwise none of the bounds that registry enforces apply to this stream.
|
||||
*/
|
||||
@Test
|
||||
void testSubscriptionsAreBoundedByTheRegistry() {
|
||||
alertSseManager.setMaxEmitters(1);
|
||||
|
||||
assertNotNull(alertSseManager.createEmitter(1L));
|
||||
final ResponseStatusException thrown =
|
||||
assertThrows(ResponseStatusException.class, () -> alertSseManager.createEmitter(2L));
|
||||
|
||||
assertEquals(HttpStatus.SERVICE_UNAVAILABLE, thrown.getStatusCode());
|
||||
assertEquals(1, alertSseManager.subscriptionCount());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<Long, SseEmitter> emitters() throws Exception {
|
||||
final Field registryField = AlertSseManager.class.getDeclaredField("registry");
|
||||
registryField.setAccessible(true);
|
||||
final Object registry = registryField.get(alertSseManager);
|
||||
final Field emittersField = SseEmitterRegistry.class.getDeclaredField("emitters");
|
||||
emittersField.setAccessible(true);
|
||||
return (Map<Long, SseEmitter>) emittersField.get(registry);
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||
import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
@@ -64,6 +65,9 @@ class NoticeConfigControllerTest {
|
||||
@Mock
|
||||
private NoticeConfigServiceImpl noticeConfigService;
|
||||
|
||||
@Mock
|
||||
private AlerterProperties alerterProperties;
|
||||
|
||||
@InjectMocks
|
||||
private NoticeConfigController noticeConfigController;
|
||||
|
||||
@@ -510,4 +514,42 @@ class NoticeConfigControllerTest {
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewNoticeTemplate() throws Exception {
|
||||
NoticeTemplate noticeTemplate = new NoticeTemplate();
|
||||
noticeTemplate.setId(5L);
|
||||
noticeTemplate.setName("preview-test");
|
||||
noticeTemplate.setType((byte) 5);
|
||||
noticeTemplate.setContent("""
|
||||
[${title}] status=${status}
|
||||
<#list alerts as alert>
|
||||
${alert.labels.alertname} - ${alert.content}
|
||||
</#list>""");
|
||||
when(alerterProperties.getConsoleUrl()).thenReturn("http://localhost:1157");
|
||||
|
||||
this.mockMvc.perform(post("/api/notice/template/preview")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonUtil.toJson(noticeTemplate)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data").value(org.hamcrest.Matchers.containsString("HighCPUUsage")))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewNoticeTemplateWithInvalidContent() throws Exception {
|
||||
NoticeTemplate noticeTemplate = new NoticeTemplate();
|
||||
noticeTemplate.setId(5L);
|
||||
noticeTemplate.setName("preview-test-invalid");
|
||||
noticeTemplate.setType((byte) 5);
|
||||
noticeTemplate.setContent("${undefinedVariable}");
|
||||
|
||||
this.mockMvc.perform(post("/api/notice/template/preview")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(JsonUtil.toJson(noticeTemplate)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
|
||||
.andReturn();
|
||||
}
|
||||
}
|
||||
|
||||
+46
-1
@@ -864,10 +864,55 @@ class AlertExpressionEvalVisitorTest {
|
||||
assertEquals(0, result.get(0).get("__value__"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSqlCallRunsPlainRead() {
|
||||
when(mockExecutor.execute("select value from cpu where host = 'server1'"))
|
||||
.thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0))));
|
||||
|
||||
final List<Map<String, Object>> result =
|
||||
evaluate("sql(\"select value from cpu where host = 'server1'\") > 70");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(80.0, result.get(0).get("__value__"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subqueries and nested aggregation are supported in alert expressions, and with every
|
||||
* metric table already readable, rejecting them would cost features without denying an
|
||||
* attacker anything. The policy stops at read only on purpose.
|
||||
*/
|
||||
@Test
|
||||
void testSqlCallKeepsSubqueriesWorking() {
|
||||
final String sql = "select value from cpu where host = (select host from hosts limit 1)";
|
||||
when(mockExecutor.execute(sql)).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0))));
|
||||
|
||||
final List<Map<String, Object>> result = evaluate("sql(\"" + sql + "\") > 70");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(80.0, result.get(0).get("__value__"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The visitor hands both spellings to the executor as written. Whether a statement is
|
||||
* allowed to run is decided by the executor it lands on, see
|
||||
* {@code DataSourceServiceTest}, so promql keeps working for text that is not valid sql
|
||||
* at all.
|
||||
*/
|
||||
@Test
|
||||
void testBothCallSpellingsReachTheExecutorAsWritten() {
|
||||
when(mockExecutor.execute("rate(http_requests_total[5m])"))
|
||||
.thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0))));
|
||||
|
||||
final List<Map<String, Object>> result = evaluate("promql(\"rate(http_requests_total[5m])\") > 70");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(80.0, result.get(0).get("__value__"));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> evaluate(String expression) {
|
||||
AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expression));
|
||||
CommonTokenStream tokens = new CommonTokenStream(lexer);
|
||||
AlertExpressionParser parser = new AlertExpressionParser(tokens);
|
||||
return new AlertExpressionEvalVisitor(mockExecutor, tokens).visit(parser.expression());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.notice.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
@@ -124,6 +126,40 @@ class EmailAlertNotifyHandlerImplTest {
|
||||
verify(mailSender).send(any(MimeMessage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSkipSslCertVerifyTrustsAllHosts() throws Exception {
|
||||
Properties props = stubMailConfig(false);
|
||||
emailAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
assertEquals("*", props.get("mail.smtp.ssl.trust"));
|
||||
assertEquals("false", props.get("mail.smtp.ssl.checkserveridentity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnableSslCertVerifyClearsStaleTrustProps() throws Exception {
|
||||
Properties props = stubMailConfig(true);
|
||||
props.put("mail.smtp.ssl.trust", "*");
|
||||
props.put("mail.smtp.ssl.checkserveridentity", "false");
|
||||
emailAlertNotifyHandler.send(receiver, template, groupAlert);
|
||||
assertNull(props.get("mail.smtp.ssl.trust"));
|
||||
assertNull(props.get("mail.smtp.ssl.checkserveridentity"));
|
||||
}
|
||||
|
||||
private Properties stubMailConfig(boolean sslCertVerify) {
|
||||
MailServerConfig config = new MailServerConfig();
|
||||
config.setEmailHost("smtp.example.com");
|
||||
config.setEmailPort(465);
|
||||
config.setEmailUsername("sender@example.com");
|
||||
config.setEmailPassword("password");
|
||||
config.setEnable(true);
|
||||
config.setEmailSslCertVerify(sslCertVerify);
|
||||
when(generalConfigDao.findByType(any()))
|
||||
.thenReturn(GeneralConfig.builder().content(JsonUtil.toJson(config)).build());
|
||||
Properties props = new Properties();
|
||||
when(mailSender.getJavaMailProperties()).thenReturn(props);
|
||||
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
|
||||
return props;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyAlertFailure() {
|
||||
when(mailSender.createMimeMessage()).thenThrow(new RuntimeException("Test Error"));
|
||||
|
||||
+85
@@ -24,12 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -38,10 +40,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
|
||||
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@@ -146,6 +150,87 @@ class AlarmGroupReduceTest {
|
||||
assertEquals(1, maxConcurrent.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression for issue #4160 (Bug 1): while one member of a group is still firing, the
|
||||
* recovery of another member must not flip the whole group to resolved. The group has to
|
||||
* stay firing until every member has actually cleared.
|
||||
*/
|
||||
@Test
|
||||
void whenOneMemberRecoversButAnotherStillFiring_groupMustNotResolve() {
|
||||
AlertGroupConverge rule = groupRule();
|
||||
alarmGroupReduce.refreshGroupDefines(Collections.singletonList(rule));
|
||||
|
||||
alarmGroupReduce.processGroupAlert(alert("cpu", "firing", "host1"));
|
||||
alarmGroupReduce.processGroupAlert(alert("mem", "firing", "host1"));
|
||||
// First group send: both members firing.
|
||||
alarmGroupReduce.runCheckAndSendGroups();
|
||||
|
||||
// CPU recovers, memory is still firing.
|
||||
alarmGroupReduce.processGroupAlert(alert("cpu", "resolved", "host1"));
|
||||
alarmGroupReduce.runCheckAndSendGroups();
|
||||
|
||||
ArgumentCaptor<GroupAlert> captor = ArgumentCaptor.forClass(GroupAlert.class);
|
||||
verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(captor.capture());
|
||||
List<GroupAlert> groups = captor.getAllValues();
|
||||
|
||||
// Memory never recovered, so no group push may ever carry a resolved group status.
|
||||
assertTrue(groups.stream().noneMatch(g -> "resolved".equals(g.getStatus())),
|
||||
"group wrongly resolved while a member alert was still firing");
|
||||
// The CPU recovery is still communicated, inside a group that stays firing.
|
||||
assertTrue(groups.stream().anyMatch(g -> "firing".equals(g.getStatus())
|
||||
&& g.getAlerts().stream().anyMatch(
|
||||
a -> "cpu".equals(a.getFingerprint()) && "resolved".equals(a.getStatus()))),
|
||||
"CPU recovery was not communicated within the still-firing group");
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression for issue #4160 (Bug 2): a resolved transition that happens while the group is
|
||||
* firing and inside the firing repeat-interval window must still be emitted. The firing
|
||||
* throttle may only suppress repeated firing notifications, never a pending recovery.
|
||||
*/
|
||||
@Test
|
||||
void whenMemberRecoversInsideRepeatInterval_recoveryMustStillBeEmitted() {
|
||||
AlertGroupConverge rule = groupRule();
|
||||
alarmGroupReduce.refreshGroupDefines(Collections.singletonList(rule));
|
||||
|
||||
alarmGroupReduce.processGroupAlert(alert("cpu", "firing", "host1"));
|
||||
alarmGroupReduce.processGroupAlert(alert("mem", "firing", "host1"));
|
||||
// First send arms the firing repeat-interval throttle.
|
||||
alarmGroupReduce.runCheckAndSendGroups();
|
||||
|
||||
// CPU keeps firing, memory recovers within the repeat interval.
|
||||
alarmGroupReduce.processGroupAlert(alert("cpu", "firing", "host1"));
|
||||
alarmGroupReduce.processGroupAlert(alert("mem", "resolved", "host1"));
|
||||
alarmGroupReduce.runCheckAndSendGroups();
|
||||
|
||||
ArgumentCaptor<GroupAlert> captor = ArgumentCaptor.forClass(GroupAlert.class);
|
||||
verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(captor.capture());
|
||||
List<GroupAlert> groups = captor.getAllValues();
|
||||
|
||||
assertTrue(groups.stream().anyMatch(g -> g.getAlerts().stream().anyMatch(
|
||||
a -> "mem".equals(a.getFingerprint()) && "resolved".equals(a.getStatus()))),
|
||||
"memory recovery was silently dropped by the firing repeat-interval throttle");
|
||||
}
|
||||
|
||||
private AlertGroupConverge groupRule() {
|
||||
AlertGroupConverge rule = new AlertGroupConverge();
|
||||
rule.setName("test-rule");
|
||||
rule.setGroupLabels(Collections.singletonList("instance"));
|
||||
rule.setGroupWait(0L);
|
||||
rule.setGroupInterval(0L);
|
||||
rule.setRepeatInterval(3600L);
|
||||
return rule;
|
||||
}
|
||||
|
||||
private SingleAlert alert(String fingerprint, String status, String instance) {
|
||||
return SingleAlert.builder()
|
||||
.fingerprint(fingerprint)
|
||||
.status(status)
|
||||
.labels(createLabels("instance", instance))
|
||||
.annotations(new HashMap<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, String> createLabels(String... keyValues) {
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
for (int i = 0; i < keyValues.length; i += 2) {
|
||||
|
||||
+107
@@ -787,4 +787,111 @@ class DataSourceServiceTest {
|
||||
() -> dataSourceService.query("sql", "SELEC * FORM hertzbeat_logs"));
|
||||
verify(mockExecutor, never()).execute(anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* The expression grammar offers three ways to reach the executor, and the datasource the
|
||||
* caller names is what picks that executor. So a statement written with the
|
||||
* {@code promql("...")} spelling still lands on the sql executor, with the server side
|
||||
* database credentials behind it, whenever the caller names the sql datasource: whether a
|
||||
* statement may run cannot be decided from the spelling.
|
||||
*/
|
||||
@Test
|
||||
void calculateRejectsWritesWhateverSpellingTheyArrivedIn() {
|
||||
final QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(mockExecutor.support("sql")).thenReturn(true);
|
||||
dataSourceService.setExecutors(List.of(mockExecutor));
|
||||
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.calculate("sql", "sql(\"drop table cpu\") > 0"));
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.calculate("sql", "promql(\"drop table cpu\") > 0"));
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.calculate("sql", "sql(\"select 1; drop table cpu\") > 0"));
|
||||
verify(mockExecutor, never()).execute(anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Only a datasource that speaks sql is guarded. A promql endpoint takes a query string
|
||||
* rather than a statement, so running it through a sql parser would reject valid promql
|
||||
* without denying an attacker anything.
|
||||
*/
|
||||
@Test
|
||||
void calculateLeavesPromqlDatasourcesAlone() {
|
||||
final QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(mockExecutor.support("promql")).thenReturn(true);
|
||||
when(mockExecutor.execute(anyString())).thenReturn(List.of(new HashMap<>(Map.of("__value__", 100.0))));
|
||||
dataSourceService.setExecutors(List.of(mockExecutor));
|
||||
|
||||
final List<Map<String, Object>> result = dataSourceService.calculate(
|
||||
"promql", "promql(\"rate(http_requests_total[5m])\") > 70");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
verify(mockExecutor).execute("rate(http_requests_total[5m])");
|
||||
}
|
||||
|
||||
/**
|
||||
* A read still has to run, including the GreptimeDB range query syntax that the sql
|
||||
* parser cannot read.
|
||||
*/
|
||||
@Test
|
||||
void calculateStillRunsReadsOnSqlDatasources() {
|
||||
final QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(mockExecutor.support("sql")).thenReturn(true);
|
||||
when(mockExecutor.execute(anyString())).thenReturn(List.of(new HashMap<>(Map.of("__value__", 100.0))));
|
||||
dataSourceService.setExecutors(List.of(mockExecutor));
|
||||
|
||||
final String rangeQuery = "select avg(value) RANGE '10s' from cpu ALIGN '5s'";
|
||||
final List<Map<String, Object>> result =
|
||||
dataSourceService.calculate("sql", "sql(\"" + rangeQuery + "\") > 70");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
verify(mockExecutor).execute(rangeQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsQueriesThatExceedTheInputBudget() {
|
||||
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(mockExecutor.support("promql")).thenReturn(true);
|
||||
dataSourceService.setExecutors(List.of(mockExecutor));
|
||||
|
||||
String oversized = "m".repeat(8193);
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.query("promql", oversized));
|
||||
verify(mockExecutor, never()).execute(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPromqlAndSqlRangesBeyondOneDay() {
|
||||
QueryExecutor promqlExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(promqlExecutor.support("promql")).thenReturn(true);
|
||||
dataSourceService.setExecutors(List.of(promqlExecutor));
|
||||
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.query("promql", "rate(http_requests_total[2d])"));
|
||||
verify(promqlExecutor, never()).execute(anyString());
|
||||
|
||||
QueryExecutor sqlExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(sqlExecutor.support("sql")).thenReturn(true);
|
||||
dataSourceService.setExecutors(List.of(sqlExecutor));
|
||||
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.calculate(
|
||||
"sql", "sql(\"select avg(value) RANGE '2d' from cpu ALIGN '5m'\") > 1"));
|
||||
verify(sqlExecutor, never()).execute(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsResultSetsBeyondTheAlertBudget() {
|
||||
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
|
||||
when(mockExecutor.support("promql")).thenReturn(true);
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (int index = 0; index < 1001; index++) {
|
||||
rows.add(Map.of("__value__", index));
|
||||
}
|
||||
when(mockExecutor.execute("metric_name")).thenReturn(rows);
|
||||
dataSourceService.setExecutors(List.of(mockExecutor));
|
||||
|
||||
assertThrows(AlertExpressionException.class,
|
||||
() -> dataSourceService.query("promql", "metric_name"));
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Test case for {@link AlibabaSmsClientImpl}: Aliyun rejects a template variable
|
||||
* whose value is null or blank, so the built param JSON must never contain one.
|
||||
*/
|
||||
class AlibabaSmsClientImplTest {
|
||||
|
||||
private final AlibabaSmsClientImpl client = new AlibabaSmsClientImpl(null);
|
||||
|
||||
private Map<String, String> params(GroupAlert alert) {
|
||||
return JsonUtil.fromJson(client.buildTemplateParam(alert), new TypeReference<>() { });
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlertShapedLikeSendTestMsgHasNoNullVariables() {
|
||||
GroupAlert alert = GroupAlert.builder()
|
||||
.commonLabels(Map.of("alertname", "CPU Usage Alert"))
|
||||
.commonAnnotations(Map.of("suggest", "Please check the CPU usage of the server"))
|
||||
.build();
|
||||
|
||||
Map<String, String> params = params(alert);
|
||||
assertEquals(3, params.size());
|
||||
params.forEach((k, v) -> assertFalse(v == null || v.isBlank(), k + " must not be null/blank"));
|
||||
assertEquals("unknown", params.get("instance"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void alertWithoutLabelsAndAnnotationsHasNoNullVariables() {
|
||||
Map<String, String> params = params(GroupAlert.builder().build());
|
||||
params.forEach((k, v) -> assertFalse(v == null || v.isBlank(), k + " must not be null/blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void realAlertValuesPassThrough() {
|
||||
GroupAlert alert = GroupAlert.builder()
|
||||
.commonLabels(Map.of("instance", "192.168.1.10:3306", "priority", "critical"))
|
||||
.commonAnnotations(Map.of("summary", "mysql down"))
|
||||
.build();
|
||||
|
||||
Map<String, String> params = params(alert);
|
||||
assertEquals("192.168.1.10:3306", params.get("instance"));
|
||||
assertEquals("critical", params.get("priority"));
|
||||
assertEquals("mysql down", params.get("content"));
|
||||
assertTrue(params.values().stream().noneMatch(String::isBlank));
|
||||
}
|
||||
}
|
||||
+53
-5
@@ -19,8 +19,11 @@ package org.apache.hertzbeat.collector.collect.ftp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.net.ftp.FTPClient;
|
||||
@@ -32,7 +35,11 @@ import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.sshd.client.SshClient;
|
||||
import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
|
||||
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
||||
import org.apache.sshd.client.session.ClientSession;
|
||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||
import org.apache.sshd.common.digest.BuiltinDigests;
|
||||
import org.apache.sshd.sftp.client.SftpClient;
|
||||
import org.apache.sshd.sftp.client.SftpClientFactory;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -46,6 +53,8 @@ public class FtpCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final String ANONYMOUS = "anonymous";
|
||||
private static final String PASSWORD = "password";
|
||||
private static final int MAX_INSECURE_WARNING_ENDPOINTS = 1024;
|
||||
private static final Set<String> INSECURE_WARNING_ENDPOINTS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* preCheck params
|
||||
@@ -56,10 +65,8 @@ public class FtpCollectImpl extends AbstractCollect {
|
||||
throw new IllegalArgumentException("Ftp collect must has ftp params.");
|
||||
}
|
||||
FtpProtocol ftpProtocol = metrics.getFtp();
|
||||
Assert.hasText(ftpProtocol.getHost(), "Ftp Protocol host is required.");
|
||||
Assert.hasText(ftpProtocol.getPort(), "Ftp Protocol port is required.");
|
||||
Assert.hasText(ftpProtocol.getDirection(), "Ftp Protocol direction is required.");
|
||||
Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is required.");
|
||||
String validationError = ftpProtocol.validationError();
|
||||
Assert.isNull(validationError, validationError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,6 +209,7 @@ public class FtpCollectImpl extends AbstractCollect {
|
||||
SshClient client = null;
|
||||
try {
|
||||
client = SshClient.setUpDefaultClient();
|
||||
client.setServerKeyVerifier(createServerKeyVerifier(ftpProtocol));
|
||||
session = connect(client, ftpProtocol);
|
||||
sftpClient = SftpClientFactory.instance().createSftpClient(session);
|
||||
Map<String, String> valueMap = collectValue(sftpClient, ftpProtocol);
|
||||
@@ -229,4 +237,44 @@ public class FtpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ServerKeyVerifier createServerKeyVerifier(FtpProtocol ftpProtocol) {
|
||||
if (Boolean.parseBoolean(ftpProtocol.getInsecureSkipVerify())) {
|
||||
logInsecureVerification(ftpProtocol);
|
||||
return AcceptAllServerKeyVerifier.INSTANCE;
|
||||
}
|
||||
Assert.hasText(ftpProtocol.getHostKeyFingerprint(),
|
||||
"Sftp Protocol host key fingerprint is required. "
|
||||
+ "Obtain it through a trusted channel; see the FTP monitor guide.");
|
||||
Assert.isTrue(ftpProtocol.hasValidHostKeyFingerprints(),
|
||||
"Sftp Protocol host key fingerprints must use the SHA256:base64 format.");
|
||||
List<String> expectedFingerprints = ftpProtocol.parseHostKeyFingerprints();
|
||||
Assert.notEmpty(expectedFingerprints,
|
||||
"Sftp Protocol host key fingerprint list must not be empty.");
|
||||
return (clientSession, remoteAddress, serverKey) -> {
|
||||
boolean matches = serverKey != null && expectedFingerprints.stream()
|
||||
.anyMatch(expectedFingerprint -> Boolean.TRUE.equals(
|
||||
KeyUtils.checkFingerPrint(
|
||||
expectedFingerprint,
|
||||
BuiltinDigests.sha256,
|
||||
serverKey).getKey()));
|
||||
if (!matches) {
|
||||
log.warn("[SFTPClient] server host key did not match for {}:{}",
|
||||
ftpProtocol.getHost(), ftpProtocol.getPort());
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
}
|
||||
|
||||
private static void logInsecureVerification(FtpProtocol ftpProtocol) {
|
||||
String endpoint = Objects.toString(ftpProtocol.getHost(), "<unknown>")
|
||||
+ ':' + Objects.toString(ftpProtocol.getPort(), "<unknown>");
|
||||
if (INSECURE_WARNING_ENDPOINTS.size() < MAX_INSECURE_WARNING_ENDPOINTS
|
||||
&& INSECURE_WARNING_ENDPOINTS.add(endpoint)) {
|
||||
log.warn("[SFTPClient] host key verification is disabled for {}; "
|
||||
+ "configure trusted host key fingerprints and re-enable verification", endpoint);
|
||||
} else {
|
||||
log.debug("[SFTPClient] host key verification remains disabled for {}", endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -901,7 +901,7 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
if (StringUtils.hasText(header.getValue())) {
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
requestBuilder.addHeader(header.getKey(), TimeExpressionUtil.calculate(header.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -936,18 +936,19 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
|
||||
// uri encode
|
||||
String url = TimeExpressionUtil.calculate(httpProtocol.getUrl());
|
||||
String uri;
|
||||
if (enableUrlEncoding) {
|
||||
// if the url contains parameters directly
|
||||
if (httpProtocol.getUrl().contains("?")) {
|
||||
String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?"));
|
||||
String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1);
|
||||
if (url.contains("?")) {
|
||||
String path = url.substring(0, url.indexOf("?"));
|
||||
String query = url.substring(url.indexOf("?") + 1);
|
||||
uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8");
|
||||
} else {
|
||||
uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8");
|
||||
uri = UriUtils.encodePath(url, "UTF-8");
|
||||
}
|
||||
} else {
|
||||
uri = httpProtocol.getUrl();
|
||||
uri = url;
|
||||
}
|
||||
|
||||
// append query params
|
||||
|
||||
+1
@@ -92,6 +92,7 @@ public class SslCertificateCollectImpl extends AbstractCollect {
|
||||
if (!verifySsl){
|
||||
SSLContext ignoreSslContext = createIgnoreVerifySslContext();
|
||||
urlConnection.setSSLSocketFactory(ignoreSslContext.getSocketFactory());
|
||||
urlConnection.setHostnameVerifier((hostname, session) -> true);
|
||||
}
|
||||
|
||||
urlConnection.connect();
|
||||
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.push;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.NetworkConstants;
|
||||
import org.apache.hertzbeat.common.constants.SignConstants;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.entity.push.PushMetricsDto;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
|
||||
/**
|
||||
* push style collect
|
||||
*/
|
||||
@Slf4j
|
||||
public class PushCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final Map<Long, Long> timeMap = new ConcurrentHashMap<>();
|
||||
|
||||
// ms
|
||||
private static final Integer DEFAULT_TIMEOUT = 3000;
|
||||
|
||||
private static final Integer SUCCESS_CODE = 200;
|
||||
|
||||
// It's hard to determine how long ago the first data collection was, because there's no way to know when the last collection occurred.
|
||||
// This makes it difficult to avoid re-collecting data after a restart. The default is 30 seconds
|
||||
private static final Integer FIRST_COLLECT_INTERVAL = 30000;
|
||||
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
if (metrics == null || metrics.getPush() == null) {
|
||||
throw new IllegalArgumentException("Push collect must has Push params");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(CollectRep.MetricsData.Builder builder,
|
||||
Metrics metrics) {
|
||||
long curTime = System.currentTimeMillis();
|
||||
long monitorId = builder.getId();
|
||||
PushProtocol pushProtocol = metrics.getPush();
|
||||
|
||||
Long time = timeMap.getOrDefault(monitorId, curTime - FIRST_COLLECT_INTERVAL);
|
||||
timeMap.put(monitorId, curTime);
|
||||
|
||||
HttpContext httpContext = createHttpContext(pushProtocol);
|
||||
HttpUriRequest request = createHttpRequest(pushProtocol, monitorId, time);
|
||||
|
||||
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != SUCCESS_CODE) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
|
||||
return;
|
||||
}
|
||||
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
|
||||
parseResponse(builder, resp, metrics);
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e);
|
||||
log.error(errorMsg, e);
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportProtocol() {
|
||||
return DispatchConstants.PROTOCOL_PUSH;
|
||||
}
|
||||
|
||||
private HttpContext createHttpContext(PushProtocol pushProtocol) {
|
||||
HttpHost host = new HttpHost(pushProtocol.getHost(), Integer.parseInt(pushProtocol.getPort()));
|
||||
HttpClientContext httpClientContext = new HttpClientContext();
|
||||
httpClientContext.setTargetHost(host);
|
||||
return httpClientContext;
|
||||
}
|
||||
|
||||
private HttpUriRequest createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) {
|
||||
RequestBuilder requestBuilder = RequestBuilder.get();
|
||||
|
||||
|
||||
// uri
|
||||
String uri = CollectUtil.replaceUriSpecialChar(pushProtocol.getUri());
|
||||
if (IpDomainUtil.isHasSchema(pushProtocol.getHost())) {
|
||||
requestBuilder.setUri(pushProtocol.getHost() + ":" + pushProtocol.getPort() + uri);
|
||||
} else {
|
||||
String ipAddressType = IpDomainUtil.checkIpAddressType(pushProtocol.getHost());
|
||||
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
|
||||
? String.format("[%s]:%s", pushProtocol.getHost(), pushProtocol.getPort() + uri)
|
||||
: String.format("%s:%s", pushProtocol.getHost(), pushProtocol.getPort() + uri);
|
||||
|
||||
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
|
||||
}
|
||||
|
||||
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
|
||||
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
|
||||
|
||||
requestBuilder.addParameter("id", String.valueOf(monitorId));
|
||||
requestBuilder.addParameter("time", String.valueOf(startTime));
|
||||
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
|
||||
//requestBuilder.setUri(pushProtocol.getUri());
|
||||
|
||||
if (DEFAULT_TIMEOUT > 0) {
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(DEFAULT_TIMEOUT)
|
||||
.setSocketTimeout(DEFAULT_TIMEOUT)
|
||||
.setRedirectsEnabled(true)
|
||||
.build();
|
||||
requestBuilder.setConfig(requestConfig);
|
||||
}
|
||||
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
||||
private void parseResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metric) {
|
||||
Message<PushMetricsDto> msg = JsonUtil.fromJson(resp, new TypeReference<>() {
|
||||
});
|
||||
if (msg == null) {
|
||||
throw new NullPointerException("parse result is null");
|
||||
}
|
||||
PushMetricsDto pushMetricsDto = msg.getData();
|
||||
if (pushMetricsDto == null || pushMetricsDto.getMetricsList() == null) {
|
||||
throw new NullPointerException("parse result is null");
|
||||
}
|
||||
for (PushMetricsDto.Metrics pushMetrics : pushMetricsDto.getMetricsList()) {
|
||||
for (Map<String, String> metrics : pushMetrics.getMetrics()) {
|
||||
List<String> metricColumn = new ArrayList<>();
|
||||
for (Metrics.Field field : metric.getFields()) {
|
||||
metricColumn.add(metrics.get(field.getField()));
|
||||
}
|
||||
CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder()
|
||||
.addAllColumns(metricColumn).build();
|
||||
builder.addValueRow(valueRow);
|
||||
}
|
||||
}
|
||||
builder.setTime(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
+21
-6
@@ -73,11 +73,26 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final String UNIQUE_IDENTITY = "identity";
|
||||
|
||||
private final ClientResources defaultClientResources;
|
||||
private final GlobalConnectionCache connectionCache = GlobalConnectionCache.getInstance();
|
||||
|
||||
public RedisCommonCollectImpl() {
|
||||
defaultClientResources = DefaultClientResources.create();
|
||||
|
||||
/**
|
||||
* Holds the lettuce client resources, created on first use.
|
||||
* <p>
|
||||
* Loading {@link DefaultClientResources} runs a static initializer that resolves netty's DNS
|
||||
* address resolver group. That resolver is unavailable in a native image on Windows, where it
|
||||
* fails with a NullPointerException from sun.net.dns.ResolverConfigurationImpl
|
||||
* (see oracle/graal#11280 and oracle/graal#4304). Because every collector is instantiated
|
||||
* eagerly through the ServiceLoader at startup, doing this in the constructor took the whole
|
||||
* collector process down before it could serve anything. Deferring it keeps startup working;
|
||||
* on the platforms where the resolver is broken only Redis collection fails, and it fails with
|
||||
* a clear error at collect time.
|
||||
*/
|
||||
private static final class ClientResourcesHolder {
|
||||
private static final ClientResources INSTANCE = DefaultClientResources.create();
|
||||
}
|
||||
|
||||
private static ClientResources clientResources() {
|
||||
return ClientResourcesHolder.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -292,7 +307,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @return redis cluster client
|
||||
*/
|
||||
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol, String host, String port) {
|
||||
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
|
||||
return RedisClusterClient.create(clientResources(), redisUri(redisProtocol, host, port));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +317,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @return redis single client
|
||||
*/
|
||||
private RedisClient buildSingleClient(RedisProtocol redisProtocol, String host, String port) {
|
||||
return RedisClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
|
||||
return RedisClient.create(clientResources(), redisUri(redisProtocol, host, port));
|
||||
}
|
||||
|
||||
private RedisURI redisUri(RedisProtocol redisProtocol, String host, String port) {
|
||||
|
||||
+104
@@ -18,15 +18,25 @@
|
||||
package org.apache.hertzbeat.collector.collect.ftp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.net.ftp.FTPClient;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
|
||||
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
||||
import org.apache.sshd.client.session.ClientSession;
|
||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||
import org.apache.sshd.common.digest.BuiltinDigests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -143,5 +153,99 @@ class FtpCollectImplTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverKeyVerifierSupportsHostKeyRotationWindow() throws Exception {
|
||||
var currentKey = generateEcPublicKey();
|
||||
var nextKey = generateEcPublicKey();
|
||||
var unrelatedKey = generateEcPublicKey();
|
||||
FtpProtocol ftpProtocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.sha256, currentKey)
|
||||
+ System.lineSeparator()
|
||||
+ KeyUtils.getFingerPrint(BuiltinDigests.sha256, nextKey))
|
||||
.build();
|
||||
|
||||
ServerKeyVerifier verifier = FtpCollectImpl.createServerKeyVerifier(ftpProtocol);
|
||||
ClientSession session = Mockito.mock(ClientSession.class);
|
||||
InetSocketAddress address = InetSocketAddress.createUnresolved("sftp.example.com", 22);
|
||||
|
||||
assertTrue(verifier.verifyServerKey(session, address, currentKey));
|
||||
assertTrue(verifier.verifyServerKey(session, address, nextKey));
|
||||
assertFalse(verifier.verifyServerKey(session, address, unrelatedKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverKeyVerifierRejectsNonSha256Fingerprints() {
|
||||
var serverKey = generateEcPublicKey();
|
||||
FtpProtocol ftpProtocol = FtpProtocol.builder()
|
||||
.hostKeyFingerprint(KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> FtpCollectImpl.createServerKeyVerifier(ftpProtocol));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverKeyVerifierAllowsExplicitVerificationOptOut() {
|
||||
FtpProtocol ftpProtocol = FtpProtocol.builder()
|
||||
.insecureSkipVerify("true")
|
||||
.build();
|
||||
|
||||
assertSame(
|
||||
AcceptAllServerKeyVerifier.INSTANCE,
|
||||
FtpCollectImpl.createServerKeyVerifier(ftpProtocol));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preCheckRejectsMalformedVerificationOptOut() {
|
||||
FtpProtocol ftpProtocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.hostKeyFingerprint(KeyUtils.getFingerPrint(
|
||||
BuiltinDigests.sha256,
|
||||
generateEcPublicKey()))
|
||||
.insecureSkipVerify("enabled")
|
||||
.build();
|
||||
Metrics metrics = new Metrics();
|
||||
metrics.setFtp(ftpProtocol);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> ftpCollectImpl.preCheck(metrics));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preCheckFailsClosedForSftpWithoutHostKeyPolicy() {
|
||||
FtpProtocol ftpProtocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.build();
|
||||
Metrics metrics = new Metrics();
|
||||
metrics.setFtp(ftpProtocol);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> ftpCollectImpl.preCheck(metrics));
|
||||
|
||||
assertTrue(exception.getMessage().contains("host key fingerprint is required"));
|
||||
}
|
||||
|
||||
private static java.security.PublicKey generateEcPublicKey() {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||
keyPairGenerator.initialize(256);
|
||||
return keyPairGenerator.generateKeyPair().getPublic();
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -32,9 +32,12 @@ import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -674,4 +677,42 @@ class HttpCollectImplTest {
|
||||
assertEquals("G1 Eden Space", firstRow.getColumns(1));
|
||||
capturedRows.forEach(t -> assertEquals(2, t.getColumnsList().size()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectResolvesTimeExpressionsInUrlAndHeaders() throws Exception {
|
||||
AtomicReference<String> requestUri = new AtomicReference<>();
|
||||
AtomicReference<String> yearHeader = new AtomicReference<>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/", exchange -> {
|
||||
requestUri.set(exchange.getRequestURI().toString());
|
||||
yearHeader.set(exchange.getRequestHeaders().getFirst("X-Year"));
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
|
||||
try {
|
||||
HttpProtocol http = HttpProtocol.builder()
|
||||
.method("GET")
|
||||
.host("127.0.0.1")
|
||||
.port(String.valueOf(server.getAddress().getPort()))
|
||||
.url("/metrics?year=${@year}")
|
||||
.headers(Map.of("X-Year", "${@year}"))
|
||||
.parseType(DispatchConstants.PARSE_DEFAULT)
|
||||
.build();
|
||||
Metrics metrics = Metrics.builder()
|
||||
.http(http)
|
||||
.aliasFields(Lists.newArrayList("responseTime"))
|
||||
.build();
|
||||
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
|
||||
|
||||
httpCollectImpl.collect(builder, metrics);
|
||||
|
||||
String year = String.valueOf(LocalDateTime.now().getYear());
|
||||
assertEquals("/metrics?year=" + year, requestUri.get());
|
||||
assertEquals(year, yearHeader.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
-7
@@ -17,23 +17,101 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.http;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import com.sun.net.httpserver.HttpsConfigurator;
|
||||
import com.sun.net.httpserver.HttpsServer;
|
||||
import java.io.FileInputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.util.List;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test case for {@link SslCertificateCollectImpl}
|
||||
* Test case for {@link SslCertificateCollectImpl}: real TLS handshake against a local
|
||||
* HTTPS server using a self-signed cert whose CN/SAN does not match the target address.
|
||||
*/
|
||||
class SslCertificateCollectImplTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
private static HttpsServer server;
|
||||
private static Path keystore;
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() throws Exception {
|
||||
keystore = genSelfSignedKeystore();
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
try (FileInputStream in = new FileInputStream(keystore.toFile())) {
|
||||
ks.load(in, "changeit".toCharArray());
|
||||
}
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(ks, "changeit".toCharArray());
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(kmf.getKeyManagers(), null, null);
|
||||
server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.setHttpsConfigurator(new HttpsConfigurator(ctx));
|
||||
server.createContext("/", exchange -> {
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() throws Exception {
|
||||
if (server != null) {
|
||||
server.stop(0);
|
||||
}
|
||||
if (keystore != null) {
|
||||
Files.deleteIfExists(keystore);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path genSelfSignedKeystore() throws Exception {
|
||||
Path ks = Files.createTempFile("ssl-collect-test", ".p12");
|
||||
Files.delete(ks);
|
||||
String keytool = Path.of(System.getProperty("java.home"), "bin", "keytool").toString();
|
||||
int exit = new ProcessBuilder(keytool, "-genkeypair", "-alias", "test",
|
||||
"-keyalg", "RSA", "-keysize", "2048", "-storetype", "PKCS12",
|
||||
"-keystore", ks.toString(), "-storepass", "changeit",
|
||||
"-dname", "CN=test", "-ext", "SAN=dns:not-this-host", "-validity", "1")
|
||||
.inheritIO().start().waitFor();
|
||||
Assertions.assertEquals(0, exit, "keytool failed to generate test keystore");
|
||||
return ks;
|
||||
}
|
||||
|
||||
private CollectRep.MetricsData.Builder collect(boolean verify) {
|
||||
HttpProtocol http = new HttpProtocol();
|
||||
http.setHost("127.0.0.1");
|
||||
http.setPort(String.valueOf(server.getAddress().getPort()));
|
||||
http.setSsl(String.valueOf(verify));
|
||||
Metrics metrics = Metrics.builder()
|
||||
.http(http)
|
||||
.aliasFields(List.of("subject", "expired", "end_timestamp"))
|
||||
.build();
|
||||
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
|
||||
new SslCertificateCollectImpl().collect(builder, metrics);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInstance() {
|
||||
void verifyOnFailsForUntrustedCert() {
|
||||
CollectRep.MetricsData.Builder builder = collect(true);
|
||||
Assertions.assertEquals(CollectRep.Code.UN_CONNECTABLE, builder.getCode(), builder.getMsg());
|
||||
Assertions.assertEquals(0, builder.getValuesCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void collect() {
|
||||
void verifyOffCollectsUntrustedMismatchedCert() {
|
||||
CollectRep.MetricsData.Builder builder = collect(false);
|
||||
Assertions.assertTrue(builder.getValuesCount() > 0, "expected cert rows, got: " + builder.getMsg());
|
||||
Assertions.assertEquals("CN=test", builder.getValues(0).getColumns(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.push;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test case for {@link PushCollectImpl}
|
||||
*/
|
||||
public class PushCollectImplTest {
|
||||
private PushCollectImpl pushCollect;
|
||||
private PushProtocol push;
|
||||
private CollectRep.MetricsData.Builder builder;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
pushCollect = new PushCollectImpl();
|
||||
push = PushProtocol.builder().uri("/metrics").host("example.com").port("60").build();
|
||||
builder = CollectRep.MetricsData.newBuilder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void preCheck() throws Exception {
|
||||
// metrics is null
|
||||
assertThrows(IllegalArgumentException.class, () -> pushCollect.preCheck(null));
|
||||
|
||||
// protocol is null
|
||||
assertThrows(IllegalArgumentException.class, () -> pushCollect.preCheck(new Metrics()));
|
||||
|
||||
// everyting is ok
|
||||
assertDoesNotThrow(() -> {
|
||||
pushCollect.preCheck(Metrics.builder().push(push).build());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void collect() throws Exception {
|
||||
assertDoesNotThrow(() -> {
|
||||
pushCollect.collect(builder, Metrics.builder().push(push).build());
|
||||
assertEquals(CollectRep.Code.FAIL, builder.getCode());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportProtocol() {
|
||||
assertEquals(DispatchConstants.PROTOCOL_PUSH, pushCollect.supportProtocol());
|
||||
}
|
||||
}
|
||||
+22
-12
@@ -158,18 +158,8 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) {
|
||||
MetricsTime metricsTime = entry.getValue();
|
||||
if (metricsTime.getStartTime() < deadline) {
|
||||
// Metrics collection timeout
|
||||
MetricsTime removedMetricsTime = metricsTimeoutMonitorMap.remove(entry.getKey());
|
||||
if (removedMetricsTime == null) {
|
||||
continue;
|
||||
}
|
||||
WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task();
|
||||
Job job = timerJob.getJob();
|
||||
// timeout metrics
|
||||
if (metricsCollector != null) {
|
||||
long duration = System.currentTimeMillis() - removedMetricsTime.getStartTime();
|
||||
metricsCollector.recordCollectMetrics(job, duration, "timeout");
|
||||
}
|
||||
|
||||
CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder()
|
||||
.setId(job.getMonitorId())
|
||||
@@ -184,7 +174,17 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
.setCode(CollectRep.Code.TIMEOUT).setMsg("collect timeout").build();
|
||||
log.error("[Collect Timeout]: \n{}", metricsData);
|
||||
if (metricsData.getPriority() == 0) {
|
||||
// dispatchCollectData removes the map entry as a once-wins gate;
|
||||
// cancel afterwards so cyclicJob() inside it still fires normally.
|
||||
dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData);
|
||||
metricsTime.getTimeout().cancel();
|
||||
} else {
|
||||
// remove the stale entry and cancel the in-flight collect so a
|
||||
// late result does not produce a duplicate dispatch.
|
||||
MetricsTime removed = metricsTimeoutMonitorMap.remove(entry.getKey());
|
||||
if (removed != null) {
|
||||
metricsTime.getTimeout().cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,12 +229,22 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
}
|
||||
MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(monitorKey);
|
||||
|
||||
// job completed metrics
|
||||
if (metricsTime != null && metricsCollector != null) {
|
||||
long duration = System.currentTimeMillis() - metricsTime.getStartTime();
|
||||
String status = metricsData.getCode() == CollectRep.Code.SUCCESS ? "success" : "fail";
|
||||
String status;
|
||||
if (metricsData.getCode() == CollectRep.Code.SUCCESS) {
|
||||
status = "success";
|
||||
} else if (metricsData.getCode() == CollectRep.Code.TIMEOUT) {
|
||||
status = "timeout";
|
||||
} else {
|
||||
status = "fail";
|
||||
}
|
||||
metricsCollector.recordCollectMetrics(job, duration, status);
|
||||
}
|
||||
// if the entry was already removed by the timeout monitor, skip the duplicate result.
|
||||
if (metricsTime == null && !metrics.isHasSubTask() && metrics.getPrometheus() == null) {
|
||||
return;
|
||||
}
|
||||
if (metrics.isHasSubTask()) {
|
||||
boolean isLastTask = metrics.consumeSubTaskResponse(metricsData);
|
||||
if (isLastTask) {
|
||||
|
||||
+4
@@ -183,6 +183,10 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
*/
|
||||
metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
|
||||
validateResponse(metricsData == null ? null : metricsData.stream().findFirst().orElse(null));
|
||||
// if the timeout monitor already cancelled this cycle, skip the late result.
|
||||
if (fastFailed()) {
|
||||
return;
|
||||
}
|
||||
collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
|
||||
return;
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"comment": "Hand-written native hints for the native collector. NOTE: must NOT live under org.apache.hertzbeat/hertzbeat-collector-collector/ - Spring AOT generates reachability-metadata.json at that exact path and silently overwrites it.",
|
||||
"reflection": [
|
||||
{"type": "org.apache.coyote.AbstractProtocol", "allPublicMethods": true},
|
||||
{"type": "org.apache.coyote.http11.AbstractHttp11Protocol", "allPublicMethods": true},
|
||||
{"type": "org.apache.coyote.http11.Http11NioProtocol", "allPublicMethods": true},
|
||||
|
||||
{"type": "io.netty.channel.kqueue.KQueueDatagramChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.kqueue.KQueueSocketChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.kqueue.KQueueEventLoopGroup", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.epoll.EpollDatagramChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.epoll.EpollSocketChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.epoll.EpollEventLoopGroup", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.socket.nio.NioDatagramChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.socket.nio.NioSocketChannel", "allPublicConstructors": true},
|
||||
{"type": "io.netty.channel.nio.NioEventLoopGroup", "allPublicConstructors": true},
|
||||
|
||||
{"type": "java.lang.management.ThreadInfo", "allPublicMethods": true},
|
||||
{"type": "java.lang.management.LockInfo", "allPublicMethods": true},
|
||||
{"type": "java.lang.management.MonitorInfo", "allPublicMethods": true},
|
||||
{"type": "java.lang.StackTraceElement", "allPublicMethods": true},
|
||||
{"type": "java.lang.management.MemoryUsage", "allPublicMethods": true},
|
||||
{"type": "com.sun.management.GcInfo", "allPublicMethods": true}
|
||||
]
|
||||
}
|
||||
-1
@@ -14,7 +14,6 @@ org.apache.hertzbeat.collector.collect.ntp.NtpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.websocket.WebsocketCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.ftp.FtpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.push.PushCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.dns.DnsCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.nginx.NginxCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.memcached.MemcachedCollectImpl
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "org.apache.hertzbeat.collector.Collector__ApplicationContextInitializer",
|
||||
"allDeclaredConstructors": true,
|
||||
"allDeclaredMethods": true
|
||||
},
|
||||
{
|
||||
"name": "org.apache.hertzbeat.collector.Collector__BeanFactoryRegistrations",
|
||||
"allDeclaredConstructors": true,
|
||||
"allDeclaredMethods": true
|
||||
},
|
||||
{
|
||||
"name": "org.apache.hertzbeat.common.entity.dto.ServerInfo",
|
||||
"allDeclaredConstructors": true,
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.kqueue.KQueueDatagramChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.kqueue.KQueueSocketChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.kqueue.KQueueEventLoopGroup",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.epoll.EpollDatagramChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.epoll.EpollSocketChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.epoll.EpollEventLoopGroup",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.socket.nio.NioDatagramChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.socket.nio.NioSocketChannel",
|
||||
"allPublicConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "io.netty.channel.nio.NioEventLoopGroup",
|
||||
"allPublicConstructors": true
|
||||
}
|
||||
]
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
org.apache.hertzbeat.collector.collect.http.HttpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.http.SslCertificateCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect
|
||||
org.apache.hertzbeat.collector.collect.icmp.IcmpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.redis.RedisCommonCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.mongodb.MongodbSingleCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.rocketmq.RocketmqSingleCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.snmp.SnmpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.ssh.SshCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.telnet.TelnetCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.smtp.SmtpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.ntp.NtpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.websocket.WebsocketCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.ftp.FtpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.push.PushCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.dns.DnsCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.nginx.NginxCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.memcached.MemcachedCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.nebulagraph.NebulaGraphCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.pop3.Pop3CollectImpl
|
||||
org.apache.hertzbeat.collector.collect.registry.RegistryImpl
|
||||
org.apache.hertzbeat.collector.collect.redfish.RedfishCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.nebulagraph.NgqlCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.imap.ImapCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.script.ScriptCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.NacosSdCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.DnsSdCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.EurekaSdCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.ConsulSdCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.modbus.ModbusCollectImpl
|
||||
org.apache.hertzbeat.collector.collect.s7.S7CollectImpl
|
||||
org.apache.hertzbeat.collector.collect.sd.ZookeeperSdCollectImpl
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.dispatch;
|
||||
|
||||
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
|
||||
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test case for {@link CommonDispatcher}.
|
||||
* Regression coverage for issue #4203 (duplicate dispatch on collection timeout).
|
||||
*/
|
||||
class CommonDispatcherTest {
|
||||
|
||||
private static final long JOB_ID = 1L;
|
||||
|
||||
/**
|
||||
* Minimal {@link Timeout} stub that tracks cancellation state and returns
|
||||
* a mock {@link WheelTimerTask} backed by a real {@link Job}.
|
||||
*/
|
||||
private static class CancellableTimeout implements Timeout {
|
||||
|
||||
private volatile boolean cancelled = false;
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
cancelled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.hertzbeat.common.timer.Timer timer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.apache.hertzbeat.common.timer.TimerTask task() {
|
||||
WheelTimerTask task = mock(WheelTimerTask.class);
|
||||
Job job = Job.builder()
|
||||
.id(JOB_ID)
|
||||
.monitorId(JOB_ID)
|
||||
.tenantId(0L)
|
||||
.app("test-app")
|
||||
.labels(Collections.emptyMap())
|
||||
.annotations(Collections.emptyMap())
|
||||
.metadata(Collections.emptyMap())
|
||||
.build();
|
||||
when(task.getJob()).thenReturn(job);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private CancellableTimeout timeout;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
timeout = new CancellableTimeout();
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutMonitor_priorityZero_dispatchesOnceAndCancels() throws Exception {
|
||||
long expiredStart = System.currentTimeMillis() - 300_000L;
|
||||
Metrics availability = Metrics.builder().name("availability").priority((byte) 0).build();
|
||||
CommonDispatcher.MetricsTime entry =
|
||||
new CommonDispatcher.MetricsTime(expiredStart, availability, timeout);
|
||||
|
||||
AtomicInteger dispatchCount = new AtomicInteger(0);
|
||||
Map<String, CommonDispatcher.MetricsTime> monitorMap = new ConcurrentHashMap<>();
|
||||
monitorMap.put(JOB_ID + "-availability", entry);
|
||||
|
||||
CommonDispatcher dispatcher = buildDispatcher(monitorMap, dispatchCount);
|
||||
invokeMonitorCollectTaskTimeout(dispatcher);
|
||||
|
||||
assertEquals(1, dispatchCount.get(), "dispatchCollectData must be called exactly once");
|
||||
assertTrue(timeout.isCancelled(), "Timeout must be cancelled after dispatch");
|
||||
assertTrue(monitorMap.isEmpty(), "Map must be empty after timeout is handled");
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutMonitor_secondScan_doesNotDoubleDispatch() throws Exception {
|
||||
long expiredStart = System.currentTimeMillis() - 300_000L;
|
||||
Metrics availability = Metrics.builder().name("availability").priority((byte) 0).build();
|
||||
CommonDispatcher.MetricsTime entry =
|
||||
new CommonDispatcher.MetricsTime(expiredStart, availability, timeout);
|
||||
|
||||
AtomicInteger dispatchCount = new AtomicInteger(0);
|
||||
Map<String, CommonDispatcher.MetricsTime> monitorMap = new ConcurrentHashMap<>();
|
||||
monitorMap.put(JOB_ID + "-availability", entry);
|
||||
|
||||
CommonDispatcher dispatcher = buildDispatcher(monitorMap, dispatchCount);
|
||||
|
||||
invokeMonitorCollectTaskTimeout(dispatcher);
|
||||
assertEquals(1, dispatchCount.get(), "First scan must dispatch once");
|
||||
|
||||
invokeMonitorCollectTaskTimeout(dispatcher);
|
||||
assertEquals(1, dispatchCount.get(), "Second scan must not produce a duplicate dispatch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutMonitor_nonZeroPriority_cancelsWithoutDispatching() throws Exception {
|
||||
long expiredStart = System.currentTimeMillis() - 300_000L;
|
||||
Metrics cpu = Metrics.builder().name("cpu").priority((byte) 1).build();
|
||||
CommonDispatcher.MetricsTime entry =
|
||||
new CommonDispatcher.MetricsTime(expiredStart, cpu, timeout);
|
||||
|
||||
AtomicInteger dispatchCount = new AtomicInteger(0);
|
||||
Map<String, CommonDispatcher.MetricsTime> monitorMap = new ConcurrentHashMap<>();
|
||||
monitorMap.put(JOB_ID + "-cpu", entry);
|
||||
|
||||
CommonDispatcher dispatcher = buildDispatcher(monitorMap, dispatchCount);
|
||||
invokeMonitorCollectTaskTimeout(dispatcher);
|
||||
|
||||
assertEquals(0, dispatchCount.get(), "Non-zero priority timeout must not dispatch");
|
||||
assertTrue(timeout.isCancelled(), "Timeout must be cancelled");
|
||||
assertTrue(monitorMap.isEmpty(), "Map must be empty after timeout is handled");
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutMonitor_nonExpiredEntry_isLeftUntouched() throws Exception {
|
||||
long recentStart = System.currentTimeMillis() - 60_000L;
|
||||
Metrics availability = Metrics.builder().name("availability").priority((byte) 0).build();
|
||||
CommonDispatcher.MetricsTime entry =
|
||||
new CommonDispatcher.MetricsTime(recentStart, availability, timeout);
|
||||
|
||||
AtomicInteger dispatchCount = new AtomicInteger(0);
|
||||
Map<String, CommonDispatcher.MetricsTime> monitorMap = new ConcurrentHashMap<>();
|
||||
monitorMap.put(JOB_ID + "-availability", entry);
|
||||
|
||||
CommonDispatcher dispatcher = buildDispatcher(monitorMap, dispatchCount);
|
||||
invokeMonitorCollectTaskTimeout(dispatcher);
|
||||
|
||||
assertEquals(0, dispatchCount.get(), "Non-expired entry must not be dispatched");
|
||||
assertFalse(timeout.isCancelled(), "Non-expired timeout must not be cancelled");
|
||||
assertFalse(monitorMap.isEmpty(), "Non-expired entry must remain in the map");
|
||||
}
|
||||
|
||||
private CommonDispatcher buildDispatcher(
|
||||
Map<String, CommonDispatcher.MetricsTime> monitorMap,
|
||||
AtomicInteger dispatchCount) throws Exception {
|
||||
|
||||
CollectJobService jobService = mock(CollectJobService.class);
|
||||
when(jobService.getCollectorIdentity()).thenReturn("test-collector");
|
||||
WorkerPool workerPool = mock(WorkerPool.class);
|
||||
|
||||
CommonDispatcher dispatcher = new CommonDispatcher(
|
||||
null, null, null, workerPool, jobService, null) {
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchCollectData(Timeout t, Metrics m, CollectRep.MetricsData data) {
|
||||
WheelTimerTask task = (WheelTimerTask) t.task();
|
||||
String key = task.getJob().getId() + "-" + m.getName();
|
||||
if (monitorMap.remove(key) == null) {
|
||||
return;
|
||||
}
|
||||
dispatchCount.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
Field mapField = CommonDispatcher.class.getDeclaredField("metricsTimeoutMonitorMap");
|
||||
mapField.setAccessible(true);
|
||||
mapField.set(dispatcher, monitorMap);
|
||||
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
private void invokeMonitorCollectTaskTimeout(CommonDispatcher dispatcher) throws Exception {
|
||||
Method method = CommonDispatcher.class.getDeclaredMethod("monitorCollectTaskTimeout");
|
||||
method.setAccessible(true);
|
||||
method.invoke(dispatcher);
|
||||
}
|
||||
}
|
||||
+19
@@ -177,6 +177,11 @@ public class CommonHttpClient {
|
||||
static void setBeforeCleanupHookForTest(Runnable hook) {
|
||||
beforeCleanupHook = hook;
|
||||
}
|
||||
|
||||
static boolean awaitConnectionPoolCleanupIdleForTest(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
ScheduledDispatchTask currentCleanupTask = cleanupTask;
|
||||
return currentCleanupTask == null || currentCleanupTask.awaitIdle(timeout, unit);
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
try {
|
||||
@@ -268,10 +273,24 @@ public class CommonHttpClient {
|
||||
shouldSchedule = pendingRuns > 0;
|
||||
if (!shouldSchedule) {
|
||||
running = false;
|
||||
notifyAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
scheduleRun();
|
||||
}
|
||||
|
||||
private synchronized boolean awaitIdle(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
long remainingNanos = deadline - System.nanoTime();
|
||||
while (running || pendingRuns > 0) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
TimeUnit.NANOSECONDS.timedWait(this, remainingNanos);
|
||||
remainingNanos = deadline - System.nanoTime();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -103,10 +103,6 @@ public interface DispatchConstants {
|
||||
* protocol rocketmq
|
||||
*/
|
||||
String PROTOCOL_ROCKETMQ = "rocketmq";
|
||||
/**
|
||||
* protocol push
|
||||
*/
|
||||
String PROTOCOL_PUSH = "push";
|
||||
/**
|
||||
* protocol prometheus
|
||||
*/
|
||||
|
||||
+17
-1
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
@@ -116,7 +117,7 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
// Delay dispatcher lookup to avoid a startup cycle with CommonDispatcher.
|
||||
WheelTimerTask timerJob = new WheelTimerTask(addJob, metricsTaskDispatchSupplier);
|
||||
if (addJob.isCyclic()) {
|
||||
Long nextExecutionTime = getNextExecutionInterval(addJob);
|
||||
long nextExecutionTime = initialCyclicDelay(addJob);
|
||||
Timeout timeout = wheelTimer.newTimeout(timerJob, nextExecutionTime, TimeUnit.SECONDS);
|
||||
cancelPreviousTimeout(currentCyclicTaskMap.put(addJob.getId(), timeout));
|
||||
} else {
|
||||
@@ -205,6 +206,21 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interval jobs get a random first-run phase: a restart re-adds every job at once,
|
||||
* and a shared phase makes them all collect at the same instant forever. Cron jobs
|
||||
* keep their meaningful phase; already-executed or re-added jobs keep theirs.
|
||||
*/
|
||||
long initialCyclicDelay(Job addJob) {
|
||||
long nextExecutionTime = getNextExecutionInterval(addJob);
|
||||
boolean fixedPhase = ScheduleTypeEnum.CRON.getType().equals(addJob.getScheduleType());
|
||||
if (!fixedPhase && addJob.getDispatchTime() <= 0
|
||||
&& !currentCyclicTaskMap.containsKey(addJob.getId()) && nextExecutionTime > 1) {
|
||||
nextExecutionTime = ThreadLocalRandom.current().nextLong(nextExecutionTime) + 1;
|
||||
}
|
||||
return nextExecutionTime;
|
||||
}
|
||||
|
||||
public Long getNextExecutionInterval(Job job) {
|
||||
if (ScheduleTypeEnum.CRON.getType().equals(job.getScheduleType()) && job.getCronExpression() != null && !job.getCronExpression().isEmpty()) {
|
||||
try {
|
||||
|
||||
-4
@@ -20,7 +20,6 @@ package org.apache.hertzbeat.collector.timer;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
@@ -84,9 +83,6 @@ public class WheelTimerTask implements TimerTask {
|
||||
JsonElement jsonElement = GSON.toJsonTree(metric);
|
||||
CollectUtil.replaceSmilingPlaceholder(jsonElement, configmap);
|
||||
metric = GSON.fromJson(jsonElement, Metrics.class);
|
||||
if (job.getApp().equals(DispatchConstants.PROTOCOL_PUSH)) {
|
||||
CollectUtil.replaceFieldsForPushStyleMonitor(metric, configmap);
|
||||
}
|
||||
metricsTmp.add(metric);
|
||||
}
|
||||
job.setMetrics(metricsTmp);
|
||||
|
||||
-7
@@ -437,13 +437,6 @@ public final class CollectUtil {
|
||||
return mapList;
|
||||
}
|
||||
|
||||
public static void replaceFieldsForPushStyleMonitor(Metrics metrics, Map<String, Configmap> configmap) {
|
||||
|
||||
List<Metrics.Field> pushFieldList = JsonUtil.fromJson((String) configmap.get("fields").getValue(), new TypeReference<>() {
|
||||
});
|
||||
metrics.setFields(pushFieldList);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert 16 hexString to byte[]
|
||||
* eg: 302c0201010409636f6d6d756e697479a11c020419e502e7020100020100300e300c06082b060102010102000500
|
||||
|
||||
+2
@@ -96,9 +96,11 @@ class CommonHttpClientVirtualThreadTest {
|
||||
|
||||
CommonHttpClient.dispatchConnectionPoolCleanup();
|
||||
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
|
||||
assertFalse(CommonHttpClient.awaitConnectionPoolCleanupIdleForTest(200, TimeUnit.MILLISECONDS));
|
||||
|
||||
releaseFirst.countDown();
|
||||
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
|
||||
assertTrue(CommonHttpClient.awaitConnectionPoolCleanupIdleForTest(5, TimeUnit.SECONDS));
|
||||
assertEquals(1, maxConcurrent.get());
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -18,8 +18,10 @@
|
||||
package org.apache.hertzbeat.collector.timer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
|
||||
import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch;
|
||||
@@ -299,4 +301,41 @@ public class TimerDispatcherTest {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFirstCyclicDelayJittersWithinInterval() {
|
||||
when(job.isCyclic()).thenReturn(true);
|
||||
when(job.getScheduleType()).thenReturn(null);
|
||||
when(job.getDispatchTime()).thenReturn(0L);
|
||||
when(job.getInterval()).thenReturn(600L);
|
||||
when(job.getId()).thenReturn(4242L);
|
||||
|
||||
Set<Long> delays = new HashSet<>();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
long delay = timerDispatcher.initialCyclicDelay(job);
|
||||
assertTrue(delay >= 1 && delay <= 600, "delay out of interval: " + delay);
|
||||
delays.add(delay);
|
||||
}
|
||||
assertTrue(delays.size() > 1, "no jitter observed across 50 samples");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExecutedJobKeepsRemainingIntervalOnReAdd() {
|
||||
when(job.getScheduleType()).thenReturn(null);
|
||||
when(job.getDispatchTime()).thenReturn(System.currentTimeMillis() - 10_000L);
|
||||
when(job.getInterval()).thenReturn(600L);
|
||||
|
||||
long expected = timerDispatcher.getNextExecutionInterval(job);
|
||||
long actual = timerDispatcher.initialCyclicDelay(job);
|
||||
assertTrue(Math.abs(actual - expected) <= 1, "remaining interval must not be jittered");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCronScheduleKeepsFixedPhase() {
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
|
||||
when(job.getCronExpression()).thenReturn("0 0 3 * * ?");
|
||||
long expected = timerDispatcher.getNextExecutionInterval(job);
|
||||
long actual = timerDispatcher.initialCyclicDelay(job);
|
||||
assertTrue(Math.abs(actual - expected) <= 1, "cron phase must not be jittered");
|
||||
}
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.timer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Configmap;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class WheelTimerTaskCredentialTest {
|
||||
|
||||
private static final String TEST_SECRET = "0123456789abcdef";
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AesUtil.setDefaultSecretKey(AesUtil.DEFAULT_ENCODE_RULES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void decryptsTheMigratedCredentialBeforeProtocolReplacement() {
|
||||
AesUtil.setDefaultSecretKey(TEST_SECRET);
|
||||
String ciphertext = AesUtil.aesEncode("runtime-ollama-key");
|
||||
HttpProtocol.Authorization authorization = new HttpProtocol.Authorization();
|
||||
authorization.setType("Bearer Token");
|
||||
authorization.setBearerTokenToken("^_^apiKey^_^");
|
||||
Metrics metrics = Metrics.builder()
|
||||
.name("version")
|
||||
.interval(60)
|
||||
.http(HttpProtocol.builder().authorization(authorization).build())
|
||||
.build();
|
||||
Job job = Job.builder()
|
||||
.app("ollama")
|
||||
.defaultInterval(60)
|
||||
.configmap(List.of(new Configmap(
|
||||
"apiKey",
|
||||
ciphertext,
|
||||
CommonConstants.PARAM_TYPE_PASSWORD)))
|
||||
.metrics(List.of(metrics))
|
||||
.build();
|
||||
|
||||
new WheelTimerTask(job, timeout -> {
|
||||
});
|
||||
|
||||
assertEquals("runtime-ollama-key",
|
||||
job.getMetrics().get(0).getHttp().getAuthorization().getBearerTokenToken());
|
||||
}
|
||||
}
|
||||
+56
-6
@@ -29,6 +29,7 @@ import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.kafka.clients.CommonClientConfigs;
|
||||
import org.apache.kafka.clients.admin.AdminClient;
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig;
|
||||
import org.apache.kafka.clients.admin.ConsumerGroupDescription;
|
||||
@@ -44,6 +45,7 @@ import org.apache.kafka.clients.admin.ListTopicsResult;
|
||||
import org.apache.kafka.clients.admin.OffsetSpec;
|
||||
import org.apache.kafka.clients.admin.TopicDescription;
|
||||
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
|
||||
import org.apache.kafka.common.config.SaslConfigs;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.TopicPartitionInfo;
|
||||
|
||||
@@ -52,6 +54,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
@@ -68,6 +71,7 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final String LAG_NUM = "lag_num";
|
||||
private static final String PARTITION_OFFSET = "Partition_offset";
|
||||
private static final String SCRAM_LOGIN_MODULE = "org.apache.kafka.common.security.scram.ScramLoginModule";
|
||||
|
||||
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
|
||||
|
||||
@@ -230,6 +234,7 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
// Ensure that host and port are not empty
|
||||
requireHasText(kafkaProtocol.getHost(), "Kafka Protocol host is required.");
|
||||
requireHasText(kafkaProtocol.getPort(), "Kafka Protocol port is required.");
|
||||
require(!kafkaProtocol.isInvalid(), "Kafka Protocol params is invalid.");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -274,9 +279,7 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
}
|
||||
|
||||
protected AdminClient getAdminClient(KafkaProtocol kafkaProtocol) {
|
||||
CacheIdentifier kafkaAdminClientIdentifier = CacheIdentifier.builder()
|
||||
.ip(kafkaProtocol.getHost()).port(kafkaProtocol.getPort())
|
||||
.build();
|
||||
CacheIdentifier kafkaAdminClientIdentifier = getAdminClientIdentifier(kafkaProtocol);
|
||||
Optional<AbstractConnection<?>> kafkaClientCache =
|
||||
connectionCommonCache.getCache(kafkaAdminClientIdentifier, true);
|
||||
if (kafkaClientCache.isPresent()) {
|
||||
@@ -289,9 +292,7 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
connectionCommonCache.removeCache(kafkaAdminClientIdentifier);
|
||||
}
|
||||
// Cached AdminClient was not present or was invalid; create a new AdminClient
|
||||
Properties properties = new Properties();
|
||||
properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
|
||||
kafkaProtocol.getHost() + ":" + kafkaProtocol.getPort());
|
||||
Properties properties = getAdminClientProperties(kafkaProtocol);
|
||||
AdminClient adminClient = KafkaAdminClient.create(properties);
|
||||
if (adminClient == null) {
|
||||
return null;
|
||||
@@ -300,6 +301,55 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
return adminClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache identifier for a Kafka AdminClient.
|
||||
*
|
||||
* @param kafkaProtocol Kafka connection parameters
|
||||
* @return cache identifier including authentication settings
|
||||
*/
|
||||
protected CacheIdentifier getAdminClientIdentifier(KafkaProtocol kafkaProtocol) {
|
||||
boolean hasSaslAuthentication = kafkaProtocol.hasSaslAuthentication();
|
||||
return CacheIdentifier.builder()
|
||||
.ip(kafkaProtocol.getHost())
|
||||
.port(kafkaProtocol.getPort())
|
||||
.username(hasSaslAuthentication ? kafkaProtocol.getUsername() : null)
|
||||
.password(hasSaslAuthentication ? kafkaProtocol.getPassword() : null)
|
||||
.customArg(hasSaslAuthentication
|
||||
? kafkaProtocol.getSecurityProtocol().toUpperCase(Locale.ROOT) + ":"
|
||||
+ kafkaProtocol.getSaslMechanism().toUpperCase(Locale.ROOT)
|
||||
: null)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Kafka AdminClient properties without logging credentials.
|
||||
*
|
||||
* @param kafkaProtocol Kafka connection parameters
|
||||
* @return AdminClient properties
|
||||
*/
|
||||
protected Properties getAdminClientProperties(KafkaProtocol kafkaProtocol) {
|
||||
Properties properties = new Properties();
|
||||
properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
|
||||
kafkaProtocol.getHost() + ":" + kafkaProtocol.getPort());
|
||||
if (kafkaProtocol.hasSaslAuthentication()) {
|
||||
properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG,
|
||||
kafkaProtocol.getSecurityProtocol().toUpperCase(Locale.ROOT));
|
||||
properties.put(SaslConfigs.SASL_MECHANISM,
|
||||
kafkaProtocol.getSaslMechanism().toUpperCase(Locale.ROOT));
|
||||
properties.put(SaslConfigs.SASL_JAAS_CONFIG, buildSaslJaasConfig(kafkaProtocol));
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static String buildSaslJaasConfig(KafkaProtocol kafkaProtocol) {
|
||||
return SCRAM_LOGIN_MODULE + " required username=\"" + escapeJaasValue(kafkaProtocol.getUsername())
|
||||
+ "\" password=\"" + escapeJaasValue(kafkaProtocol.getPassword()) + "\";";
|
||||
}
|
||||
|
||||
private static String escapeJaasValue(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Collect the earliest and latest offsets for each topic
|
||||
|
||||
+86
@@ -18,14 +18,18 @@
|
||||
package org.apache.hertzbeat.collector.collect.kafka;
|
||||
|
||||
import org.apache.hertzbeat.collector.collect.kafka.constants.SupportedCommand;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.kafka.clients.admin.AdminClient;
|
||||
import org.apache.kafka.clients.CommonClientConfigs;
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig;
|
||||
import org.apache.kafka.clients.admin.ListTopicsOptions;
|
||||
import org.apache.kafka.clients.admin.ListTopicsResult;
|
||||
import org.apache.kafka.common.KafkaFuture;
|
||||
import org.apache.kafka.common.config.SaslConfigs;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -34,10 +38,13 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
@@ -135,4 +142,83 @@ public class KafkaCollectTest {
|
||||
void supportProtocol() {
|
||||
assertEquals(DispatchConstants.PROTOCOL_KAFKA, collect.supportProtocol());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminClientPropertiesWithoutAuthentication() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host(HOST)
|
||||
.port(PORT)
|
||||
.build();
|
||||
|
||||
Properties properties = collect.getAdminClientProperties(protocol);
|
||||
|
||||
assertEquals(HOST + ":" + PORT, properties.get(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG));
|
||||
assertFalse(properties.containsKey(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
|
||||
assertFalse(properties.containsKey(SaslConfigs.SASL_MECHANISM));
|
||||
assertFalse(properties.containsKey(SaslConfigs.SASL_JAAS_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminClientPropertiesWithScramAuthentication() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host(HOST)
|
||||
.port(PORT)
|
||||
.securityProtocol("SASL_SSL")
|
||||
.saslMechanism("SCRAM-SHA-512")
|
||||
.username("monitor")
|
||||
.password("secret")
|
||||
.build();
|
||||
|
||||
Properties properties = collect.getAdminClientProperties(protocol);
|
||||
|
||||
assertEquals("SASL_SSL", properties.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
|
||||
assertEquals("SCRAM-SHA-512", properties.get(SaslConfigs.SASL_MECHANISM));
|
||||
assertEquals("org.apache.kafka.common.security.scram.ScramLoginModule required "
|
||||
+ "username=\"monitor\" password=\"secret\";",
|
||||
properties.get(SaslConfigs.SASL_JAAS_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminClientPropertiesEscapeJaasCredentials() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host(HOST)
|
||||
.port(PORT)
|
||||
.securityProtocol("sasl_plaintext")
|
||||
.saslMechanism("scram-sha-256")
|
||||
.username("monitor\"user")
|
||||
.password("secret\\value")
|
||||
.build();
|
||||
|
||||
Properties properties = collect.getAdminClientProperties(protocol);
|
||||
|
||||
assertEquals("org.apache.kafka.common.security.scram.ScramLoginModule required "
|
||||
+ "username=\"monitor\\\"user\" password=\"secret\\\\value\";",
|
||||
properties.get(SaslConfigs.SASL_JAAS_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminClientCacheIdentifierIncludesAuthentication() {
|
||||
KafkaProtocol firstProtocol = KafkaProtocol.builder()
|
||||
.host(HOST)
|
||||
.port(PORT)
|
||||
.securityProtocol("SASL_PLAINTEXT")
|
||||
.saslMechanism("SCRAM-SHA-256")
|
||||
.username("monitor")
|
||||
.password("first-secret")
|
||||
.build();
|
||||
KafkaProtocol secondProtocol = KafkaProtocol.builder()
|
||||
.host(HOST)
|
||||
.port(PORT)
|
||||
.securityProtocol("SASL_PLAINTEXT")
|
||||
.saslMechanism("SCRAM-SHA-256")
|
||||
.username("monitor")
|
||||
.password("second-secret")
|
||||
.build();
|
||||
|
||||
CacheIdentifier firstIdentifier = collect.getAdminClientIdentifier(firstProtocol);
|
||||
CacheIdentifier secondIdentifier = collect.getAdminClientIdentifier(secondProtocol);
|
||||
|
||||
assertNotEquals(firstIdentifier, secondIdentifier);
|
||||
assertFalse(firstIdentifier.toString().contains("first-secret"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -69,6 +69,8 @@ public interface ConfigConstants {
|
||||
String GRAFANA = "grafana";
|
||||
|
||||
String LOG = "log";
|
||||
|
||||
String OBSERVABILITY = "observability";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ public interface NetworkConstants {
|
||||
Duration READ_TIMEOUT = Duration.ofSeconds(6);
|
||||
Duration WRITE_TIMEOUT = Duration.ofSeconds(6);
|
||||
Duration CONNECT_TIMEOUT = Duration.ofSeconds(6);
|
||||
Duration GREPTIME_QUERY_READ_TIMEOUT = Duration.ofSeconds(5);
|
||||
Duration GREPTIME_QUERY_READ_TIMEOUT = Duration.ofSeconds(15);
|
||||
Duration GREPTIME_QUERY_CONNECT_TIMEOUT = Duration.ofSeconds(2);
|
||||
Duration GREPTIME_WRITE_READ_TIMEOUT = Duration.ofSeconds(3);
|
||||
Duration GREPTIME_WRITE_CONNECT_TIMEOUT = Duration.ofSeconds(2);
|
||||
|
||||
+2
@@ -56,5 +56,7 @@ public class MailServerConfig {
|
||||
|
||||
private boolean emailStarttls = false;
|
||||
|
||||
private boolean emailSslCertVerify = true;
|
||||
|
||||
private boolean enable = true;
|
||||
}
|
||||
|
||||
-5
@@ -54,7 +54,6 @@ import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.NtpProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.Pop3Protocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol;
|
||||
@@ -212,10 +211,6 @@ public class Metrics {
|
||||
* Monitoring configuration information using the public rocketmq protocol
|
||||
*/
|
||||
private RocketmqProtocol rocketmq;
|
||||
/**
|
||||
* Monitoring configuration information using push style
|
||||
*/
|
||||
private PushProtocol push;
|
||||
/**
|
||||
* Monitoring configuration information using the public prometheus protocol
|
||||
*/
|
||||
|
||||
+78
-6
@@ -20,6 +20,9 @@ package org.apache.hertzbeat.common.entity.job.protocol;
|
||||
import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
|
||||
import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -35,6 +38,10 @@ import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class FtpProtocol implements CommonRequestProtocol, Protocol {
|
||||
|
||||
private static final Pattern SHA256_FINGERPRINT_PATTERN =
|
||||
Pattern.compile("SHA256:[A-Za-z0-9+/]{43}=?");
|
||||
private static final String UNRESOLVED_SSL_PLACEHOLDER = "^_^ssl^_^";
|
||||
/**
|
||||
* Peer host ip or domain name
|
||||
*/
|
||||
@@ -71,19 +78,84 @@ public class FtpProtocol implements CommonRequestProtocol, Protocol {
|
||||
*/
|
||||
private String ssl = "false";
|
||||
|
||||
/**
|
||||
* Expected SFTP server host key fingerprints, separated by commas or line
|
||||
* breaks, for example SHA256:base64.
|
||||
*/
|
||||
private String hostKeyFingerprint;
|
||||
|
||||
/**
|
||||
* Whether SFTP host key verification is explicitly disabled.
|
||||
*/
|
||||
private String insecureSkipVerify;
|
||||
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
if (!validateIpDomain(host) || !validPort(port) || StringUtils.isBlank(direction) || StringUtils.isBlank(timeout)) {
|
||||
return true;
|
||||
return validationError() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the complete FTP/SFTP protocol contract used by collectors.
|
||||
*
|
||||
* @return a safe operator-facing error, or {@code null} when valid
|
||||
*/
|
||||
public String validationError() {
|
||||
if (!validateIpDomain(host)) {
|
||||
return "Ftp Protocol host is invalid.";
|
||||
}
|
||||
if (!CommonUtil.isNumeric(timeout)) {
|
||||
return true;
|
||||
if (!validPort(port)) {
|
||||
return "Ftp Protocol port is invalid.";
|
||||
}
|
||||
if (StringUtils.isBlank(direction)) {
|
||||
return "Ftp Protocol direction is required.";
|
||||
}
|
||||
if (StringUtils.isBlank(timeout) || !CommonUtil.isNumeric(timeout)) {
|
||||
return "Ftp Protocol timeout must be numeric.";
|
||||
}
|
||||
if (UNRESOLVED_SSL_PLACEHOLDER.equals(ssl)) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(ssl)
|
||||
&& !"true".equalsIgnoreCase(ssl)
|
||||
&& !"false".equalsIgnoreCase(ssl)) {
|
||||
return true;
|
||||
return "Ftp Protocol SFTP option must be true or false.";
|
||||
}
|
||||
return "true".equalsIgnoreCase(ssl) && StringUtils.isAnyBlank(username, password);
|
||||
if (!"true".equalsIgnoreCase(ssl)) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(insecureSkipVerify)
|
||||
&& !"true".equalsIgnoreCase(insecureSkipVerify)
|
||||
&& !"false".equalsIgnoreCase(insecureSkipVerify)) {
|
||||
return "Sftp Protocol skip-verification option must be true or false.";
|
||||
}
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
return "Sftp Protocol username and password are required.";
|
||||
}
|
||||
if ("true".equalsIgnoreCase(insecureSkipVerify)) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(hostKeyFingerprint)) {
|
||||
return "Sftp Protocol host key fingerprint is required unless verification is explicitly skipped.";
|
||||
}
|
||||
if (!hasValidHostKeyFingerprints()) {
|
||||
return "Sftp Protocol host key fingerprints must use the SHA256:base64 format.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasValidHostKeyFingerprints() {
|
||||
List<String> fingerprints = parseHostKeyFingerprints();
|
||||
return !fingerprints.isEmpty()
|
||||
&& fingerprints.stream().allMatch(value -> SHA256_FINGERPRINT_PATTERN.matcher(value).matches());
|
||||
}
|
||||
|
||||
public List<String> parseHostKeyFingerprints() {
|
||||
if (StringUtils.isBlank(hostKeyFingerprint)) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(hostKeyFingerprint.split("[,;\\r\\n]+"))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
+50
-1
@@ -24,9 +24,13 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Kafka protocol
|
||||
*/
|
||||
@@ -36,6 +40,11 @@ import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
@NoArgsConstructor
|
||||
public class KafkaProtocol implements CommonRequestProtocol, Protocol {
|
||||
|
||||
private static final String SASL_PLAINTEXT = "SASL_PLAINTEXT";
|
||||
private static final String SASL_SSL = "SASL_SSL";
|
||||
private static final Set<String> SECURITY_PROTOCOLS = Set.of("PLAINTEXT", SASL_PLAINTEXT, SASL_SSL);
|
||||
private static final Set<String> SASL_MECHANISMS = Set.of("SCRAM-SHA-256", "SCRAM-SHA-512");
|
||||
|
||||
/**
|
||||
* IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
|
||||
*/
|
||||
@@ -61,6 +70,36 @@ public class KafkaProtocol implements CommonRequestProtocol, Protocol {
|
||||
*/
|
||||
private String monitorInternalTopic = "false";
|
||||
|
||||
/**
|
||||
* Kafka security protocol
|
||||
*/
|
||||
private String securityProtocol;
|
||||
|
||||
/**
|
||||
* SASL mechanism
|
||||
*/
|
||||
private String saslMechanism;
|
||||
|
||||
/**
|
||||
* SASL username
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* SASL password
|
||||
*/
|
||||
@ToString.Exclude
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Determine whether SASL authentication is enabled.
|
||||
*
|
||||
* @return true if the security protocol uses SASL
|
||||
*/
|
||||
public boolean hasSaslAuthentication() {
|
||||
return SASL_PLAINTEXT.equalsIgnoreCase(securityProtocol) || SASL_SSL.equalsIgnoreCase(securityProtocol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
if (!validateIpDomain(host) || !validPort(port)) {
|
||||
@@ -74,6 +113,16 @@ public class KafkaProtocol implements CommonRequestProtocol, Protocol {
|
||||
&& !"false".equalsIgnoreCase(monitorInternalTopic)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (StringUtils.isNotBlank(securityProtocol)
|
||||
&& !SECURITY_PROTOCOLS.contains(securityProtocol.toUpperCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
if (!hasSaslAuthentication()) {
|
||||
return StringUtils.isNotBlank(saslMechanism)
|
||||
|| StringUtils.isNotBlank(username)
|
||||
|| StringUtils.isNotBlank(password);
|
||||
}
|
||||
return StringUtils.isAnyBlank(saslMechanism, username, password)
|
||||
|| !SASL_MECHANISMS.contains(saslMechanism.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.job.protocol;
|
||||
|
||||
import static org.apache.hertzbeat.common.util.IpDomainUtil.isHasSchema;
|
||||
import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
|
||||
import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.common.entity.dto.Field;
|
||||
|
||||
/**
|
||||
* push protocol definition
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PushProtocol implements CommonRequestProtocol, Protocol {
|
||||
private String host;
|
||||
private String port;
|
||||
private String uri = "/api/push";
|
||||
private List<Field> fields;
|
||||
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
if ((!validateIpDomain(host) && !isHasSchema(host)) || !validPort(port)) {
|
||||
return true;
|
||||
}
|
||||
if (Integer.parseInt(port) <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (StringUtils.isBlank(uri) || !uri.startsWith("/") || StringUtils.containsWhitespace(uri)) {
|
||||
return true;
|
||||
}
|
||||
if (fields == null || fields.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (Field field : fields) {
|
||||
if (field == null
|
||||
|| StringUtils.isBlank(field.getName())
|
||||
|| field.getType() == null
|
||||
|| (field.getType() != 0 && field.getType() != 1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -49,7 +49,7 @@ public class RegistryProtocol implements CommonRequestProtocol, Protocol {
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
|
||||
return validateIpDomain(host) && validPort(port)
|
||||
&& StringUtils.isAnyBlank(host, String.valueOf(port), discoveryClientTypeName);
|
||||
return !validateIpDomain(host) || !validPort(port)
|
||||
|| StringUtils.isBlank(discoveryClientTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
+113
@@ -20,10 +20,14 @@ package org.apache.hertzbeat.common.entity.job.protocol;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FtpProtocolTest {
|
||||
|
||||
private static final String VALID_SHA256_FINGERPRINT =
|
||||
"SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
@Test
|
||||
void isInvalidValidAnonymousFtp() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
@@ -36,6 +40,34 @@ class FtpProtocolTest {
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidPlainFtpWhenSftpOnlyOptionRemainsUnresolved() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("ftp.example.com")
|
||||
.port("21")
|
||||
.direction("/")
|
||||
.timeout("3000")
|
||||
.ssl("false")
|
||||
.insecureSkipVerify("^_^insecureSkipVerify^_^")
|
||||
.build();
|
||||
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidLegacyFtpWhenSslOptionRemainsUnresolved() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("ftp.example.com")
|
||||
.port("21")
|
||||
.direction("/")
|
||||
.timeout("3000")
|
||||
.ssl("^_^ssl^_^")
|
||||
.insecureSkipVerify("^_^insecureSkipVerify^_^")
|
||||
.build();
|
||||
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidValidSftp() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
@@ -46,6 +78,7 @@ class FtpProtocolTest {
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
@@ -85,6 +118,67 @@ class FtpProtocolTest {
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidSftpWithoutHostIdentityConfiguration() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidSftpWhenSkipVerificationOptionRemainsUnresolved() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
|
||||
.insecureSkipVerify("^_^insecureSkipVerify^_^")
|
||||
.build();
|
||||
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidSftpWithExplicitVerificationOptOut() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.insecureSkipVerify("true")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidSftpWithMalformedHostKeyFingerprint() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.hostKeyFingerprint("SHA256:not-a-valid-fingerprint")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidInvalidTimeout() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
@@ -95,4 +189,23 @@ class FtpProtocolTest {
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializationDoesNotExposeComputedValidationProperties() {
|
||||
FtpProtocol protocol = FtpProtocol.builder()
|
||||
.host("sftp.example.com")
|
||||
.port("22")
|
||||
.direction("/data")
|
||||
.timeout("3000")
|
||||
.ssl("true")
|
||||
.username("admin")
|
||||
.password("secret")
|
||||
.hostKeyFingerprint(VALID_SHA256_FINGERPRINT)
|
||||
.build();
|
||||
|
||||
String json = JsonUtil.toJson(protocol);
|
||||
|
||||
assertFalse(json.contains("validationError"));
|
||||
assertFalse(json.contains("parsedHostKeyFingerprints"));
|
||||
}
|
||||
}
|
||||
|
||||
+86
-1
@@ -175,4 +175,89 @@ class KafkaProtocolTest {
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidValidScramSha256Authentication() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.securityProtocol("SASL_PLAINTEXT")
|
||||
.saslMechanism("SCRAM-SHA-256")
|
||||
.username("monitor")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
assertTrue(protocol.hasSaslAuthentication());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidValidScramSha512Authentication() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9093")
|
||||
.securityProtocol("SASL_SSL")
|
||||
.saslMechanism("SCRAM-SHA-512")
|
||||
.username("monitor")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
assertTrue(protocol.hasSaslAuthentication());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidMissingScramCredentials() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.securityProtocol("SASL_PLAINTEXT")
|
||||
.saslMechanism("SCRAM-SHA-256")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidUnsupportedSaslMechanism() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.securityProtocol("SASL_PLAINTEXT")
|
||||
.saslMechanism("PLAIN")
|
||||
.username("monitor")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidUnsupportedSecurityProtocol() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.securityProtocol("SSL")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidCredentialsWithoutSasl() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.securityProtocol("PLAINTEXT")
|
||||
.username("monitor")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringDoesNotExposePassword() {
|
||||
KafkaProtocol protocol = KafkaProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("9092")
|
||||
.password("secret")
|
||||
.build();
|
||||
assertTrue(protocol.toString().contains("KafkaProtocol"));
|
||||
assertFalse(protocol.toString().contains("secret"));
|
||||
}
|
||||
}
|
||||
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.job.protocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.dto.Field;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PushProtocolTest {
|
||||
|
||||
@Test
|
||||
void isInvalidValidProtocol() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("cpuUsage").type((byte) 0).build()))
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidValidProtocolWithSchemaHost() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("http://127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 1).build()))
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidInvalidHost() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 1).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidZeroPort() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("0")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 1).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidBlankUri() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 1).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidUriWithoutLeadingSlash() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("api/push")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 1).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidBlankFields() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of())
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidFieldWithoutName() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("").type((byte) 1).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidFieldWithoutType() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("status").type(null).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidFieldWithUnsupportedType() {
|
||||
PushProtocol protocol = PushProtocol.builder()
|
||||
.host("127.0.0.1")
|
||||
.port("1157")
|
||||
.uri("/api/push")
|
||||
.fields(List.of(Field.builder().name("status").type((byte) 2).build()))
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
}
|
||||
+155
-32
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.common.entity.job.protocol;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -28,26 +25,152 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class RegistryProtocolTest {
|
||||
|
||||
@Test
|
||||
void isInvalid() {
|
||||
void isInvalidValidProtocol() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
RegistryProtocol protocol1 = new RegistryProtocol();
|
||||
protocol1.setPort("8080");
|
||||
protocol1.setHost("127.0.0.1");
|
||||
assertTrue(protocol1.isInvalid());
|
||||
@Test
|
||||
void isInvalidValidProtocolWithDomain() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("nacos.example.com")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
RegistryProtocol protocol2 = new RegistryProtocol();
|
||||
protocol2.setPort("8080");
|
||||
protocol2.setHost("www.baidu.com");
|
||||
assertTrue(protocol2.isInvalid());
|
||||
@Test
|
||||
void isInvalidValidProtocolWithLocalhost() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("localhost")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Consul")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
RegistryProtocol protocol3 = new RegistryProtocol();
|
||||
protocol3.setPort("8080");
|
||||
protocol3.setHost("www.baidu.com.");
|
||||
assertFalse(protocol3.isInvalid());
|
||||
@Test
|
||||
void isInvalidValidProtocolWithIpv6() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("::1")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Consul")
|
||||
.build();
|
||||
assertFalse(protocol.isInvalid());
|
||||
}
|
||||
|
||||
RegistryProtocol protocol4 = new RegistryProtocol();
|
||||
protocol3.setPort("80800");
|
||||
protocol3.setHost("10.45.56.344");
|
||||
assertFalse(protocol4.isInvalid());
|
||||
@Test
|
||||
void isInvalidNullHost() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host(null)
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidBlankHost() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host(" ")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidMalformedHost() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("???")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidHostWithTrailingDot() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("www.baidu.com.")
|
||||
.port("8080")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidNullPort() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port(null)
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidBlankPort() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidOutOfRangePort() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("99999")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidNonNumericPort() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("abc")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidOutOfRangePortWithDomainLikeHost() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("10.45.56.344")
|
||||
.port("80800")
|
||||
.discoveryClientTypeName("Nacos")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidNullDiscoveryClientTypeName() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName(null)
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInvalidBlankDiscoveryClientTypeName() {
|
||||
RegistryProtocol protocol = RegistryProtocol.builder()
|
||||
.host("192.168.1.1")
|
||||
.port("8848")
|
||||
.discoveryClientTypeName(" ")
|
||||
.build();
|
||||
assertTrue(protocol.isInvalid());
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,12 +19,14 @@ package org.apache.hertzbeat.common.entity.ai;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
@@ -47,7 +49,9 @@ import java.util.List;
|
||||
@Builder
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "hzb_ai_conversation")
|
||||
@Table(name = "hzb_ai_conversation", indexes = {
|
||||
@Index(name = "idx_ai_conversation_creator", columnList = "creator")
|
||||
})
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatConversation {
|
||||
@@ -81,5 +85,6 @@ public class ChatConversation {
|
||||
@OneToMany(mappedBy = "conversation")
|
||||
private List<ChatMessage> messages;
|
||||
|
||||
@JsonIgnore
|
||||
private String securityData;
|
||||
}
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "hzb_sop_schedule", indexes = {
|
||||
@Index(name = "idx_schedule_conversation_id", columnList = "conversation_id"),
|
||||
@Index(name = "idx_schedule_creator_conversation", columnList = "creator, conversation_id"),
|
||||
@Index(name = "idx_schedule_enabled_next", columnList = "enabled, next_run_time")
|
||||
})
|
||||
@AllArgsConstructor
|
||||
|
||||
+2
-2
@@ -69,8 +69,8 @@ public class AlertDefine {
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Alarm Threshold Expr", example = "usage>90", accessMode = READ_WRITE)
|
||||
@Size(max = 2048)
|
||||
@Column(length = 2048)
|
||||
@Size(max = 65535)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String expr;
|
||||
|
||||
@Schema(title = "Execution Period/ Window Size (seconds) - For periodic rules/ For log realtime", example = "300")
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* Holds the live server sent event subscriptions of one stream and owns their lifecycle.
|
||||
*
|
||||
* <p>What: hands an emitter to each subscriber, keeps the live ones, broadcasts an event to
|
||||
* all of them and drops the ones that have gone away.
|
||||
*
|
||||
* <p>How: hold one instance per stream and name it after that stream, so refusals and
|
||||
* broadcast failures stay tellable apart in the log. The instance is not a bean: a stream
|
||||
* owns its registry the way it owns its subscribers.
|
||||
*
|
||||
* <p>Note: an open subscription occupies a request thread for as long as it lives, so both
|
||||
* how long one may live and how many may exist at once are bounded here. Each stream used to
|
||||
* carry its own copy of this lifecycle and the copies drifted - one released the slot of a
|
||||
* subscription that died on an error and the other did not, so on that stream a failed
|
||||
* subscriber held its slot until the process restarted.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SseEmitterRegistry {
|
||||
|
||||
/**
|
||||
* How long a subscription may stay open before the client has to reconnect.
|
||||
*
|
||||
* <p>An unbounded timeout means a subscription never expires on its own, so a client that
|
||||
* goes away without closing cleanly holds its request thread until the container notices.
|
||||
* A finite timeout bounds that. It is only safe because the ui reconnects when the stream
|
||||
* ends: it reads through `fetch` rather than `EventSource`, so it has to reconnect itself,
|
||||
* and it does.
|
||||
*/
|
||||
private static final long EMITTER_TIMEOUT_MILLIS = 30 * 60 * 1000L;
|
||||
|
||||
/** Name of the stream these subscriptions belong to, used to make log lines tellable apart. */
|
||||
private final String streamName;
|
||||
|
||||
/**
|
||||
* Cap on concurrently held subscriptions. Each one occupies a request thread, so without
|
||||
* a ceiling enough parallel subscriptions exhaust the container's thread pool and take
|
||||
* the whole application down with them.
|
||||
*/
|
||||
@Setter
|
||||
private int maxEmitters = 1000;
|
||||
|
||||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Slots taken by held subscriptions, reserved before the subscription is registered.
|
||||
*
|
||||
* <p>Reading `emitters.size()` and then putting into it would let every request of a
|
||||
* parallel burst pass the same check and register anyway, which is exactly the burst the
|
||||
* ceiling exists to survive. Claiming a slot by compare-and-set makes the ceiling hold no
|
||||
* matter how many requests arrive at once. The counter can briefly run ahead of the map,
|
||||
* between the claim and the put, which only ever refuses one subscription too early.
|
||||
*/
|
||||
private final AtomicInteger heldSlots = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* @param streamName Name of the stream, used in log lines only
|
||||
*/
|
||||
public SseEmitterRegistry(String streamName) {
|
||||
this.streamName = streamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a subscription for the given client.
|
||||
*
|
||||
* <p>When: from the request thread serving a subscribe request. The returned emitter is
|
||||
* what the controller hands back to spring, which keeps the request open around it.
|
||||
*
|
||||
* @param clientId Identifier of the subscriber, unique per subscription, not null
|
||||
* @return The emitter the caller returns from its controller method
|
||||
* @throws ResponseStatusException With {@code 503} when the stream already holds as many
|
||||
* subscriptions as it may
|
||||
* @throws NullPointerException When {@code clientId} is null, which is checked before a
|
||||
* slot is taken so that a caller that gets this wrong cannot spend the ceiling
|
||||
*/
|
||||
public SseEmitter createEmitter(Long clientId) {
|
||||
Objects.requireNonNull(clientId, "clientId of an sse subscription must not be null");
|
||||
claimSlot();
|
||||
boolean registered = false;
|
||||
try {
|
||||
final SseEmitter emitter = new SseEmitter(EMITTER_TIMEOUT_MILLIS);
|
||||
emitter.onCompletion(() -> removeEmitter(clientId));
|
||||
emitter.onTimeout(() -> removeEmitter(clientId));
|
||||
// Without this a subscription that dies on an error keeps its slot: a stream that
|
||||
// broadcasts rarely notices nothing, and the slot is held until the process restarts
|
||||
emitter.onError(ex -> removeEmitter(clientId));
|
||||
// The id is a fresh snowflake per subscription, so this never replaces a live emitter
|
||||
emitters.put(clientId, emitter);
|
||||
registered = true;
|
||||
return emitter;
|
||||
} finally {
|
||||
if (!registered) {
|
||||
// The slot is only ever given back by removing the subscription from the map,
|
||||
// so one that never got in there would hold its slot until the process restarts
|
||||
heldSlots.decrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends one event to every live subscription and drops the ones the send fails for.
|
||||
*
|
||||
* <p>Note: a failing send is how a client that went away is noticed, so the failure is
|
||||
* expected rather than exceptional and only unforeseen ones are logged at error level.
|
||||
*
|
||||
* @param eventName Name of the sse event, which is what the ui subscribes by
|
||||
* @param data Payload to deliver, already serialised
|
||||
*/
|
||||
public void broadcast(String eventName, String data) {
|
||||
emitters.forEach((clientId, emitter) -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.id(String.valueOf(System.currentTimeMillis()))
|
||||
.name(eventName)
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to broadcast {} data to client: {}", streamName, e.getMessage());
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return How many subscriptions this stream currently holds
|
||||
*/
|
||||
public int subscriptionCount() {
|
||||
return heldSlots.get();
|
||||
}
|
||||
|
||||
private void claimSlot() {
|
||||
while (true) {
|
||||
final int held = heldSlots.get();
|
||||
if (held >= maxEmitters) {
|
||||
log.warn("Refused {} subscription, already holding {} of at most {}", streamName, held, maxEmitters);
|
||||
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Too many " + streamName + " subscriptions");
|
||||
}
|
||||
if (heldSlots.compareAndSet(held, held + 1)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||
} catch (Throwable e) {
|
||||
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
// Execute clear
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A subscription can be dropped more than once for the same client: a send failure cleans
|
||||
* it up and completing it fires the completion callback on top of that. The slot is only
|
||||
* released for the removal that actually took the emitter out of the map, so the count
|
||||
* cannot drift below what is held and quietly reopen the ceiling.
|
||||
*/
|
||||
private void removeEmitter(Long clientId) {
|
||||
if (emitters.remove(clientId) != null) {
|
||||
heldSlots.decrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
+394
-9
@@ -21,32 +21,81 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.Statements;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.insert.Insert;
|
||||
import net.sf.jsqlparser.statement.merge.Merge;
|
||||
import net.sf.jsqlparser.statement.select.LateralSubSelect;
|
||||
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SetOperationList;
|
||||
import net.sf.jsqlparser.statement.select.WithItem;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import net.sf.jsqlparser.util.TablesNamesFinder;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* SQL Security Validator using JSqlParser 5.1+.
|
||||
* Security Policy:
|
||||
* 1. Only SELECT statements are allowed.
|
||||
* 2. All referenced tables must be in the whitelist.
|
||||
* 3. Subqueries, UNION, CTE, LATERAL are blocked.
|
||||
*
|
||||
* <p>Two modes, see {@link #SqlSecurityValidator(Collection)} and {@link #selectOnly()}:
|
||||
* <ul>
|
||||
* <li>whitelisting: only SELECT, every referenced table must be whitelisted, and
|
||||
* subqueries, UNION, CTE and LATERAL are blocked because they are the ways a statement
|
||||
* can reach a table the whitelist never mentions.</li>
|
||||
* <li>read only: only SELECT, any table, nested reads kept.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both modes reject a string that carries more than one statement, and neither lets a
|
||||
* write through at any depth of the statement.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlSecurityValidator {
|
||||
|
||||
private static final String SELECT_KEYWORD = "SELECT";
|
||||
|
||||
private static final String WITH_KEYWORD = "WITH";
|
||||
|
||||
/**
|
||||
* Words that no read contains, matched as whole words outside literals and comments.
|
||||
*
|
||||
* <p>This is the check that holds when the parser cannot read the dialect and there is no
|
||||
* tree to walk, so it has to catch a write wherever it sits, including nested in a cte or
|
||||
* a subquery. It is deliberately coarse; the tree walk is the precise one.
|
||||
*
|
||||
* <p>Statements that can only stand alone, {@code TRUNCATE} and {@code CALL} among them,
|
||||
* are absent: the leading keyword already rejects those, and several of them double as
|
||||
* ordinary functions, {@code TRUNCATE(value, 2)} and {@code REPLACE(msg, 'a', 'b')} being
|
||||
* the ones a metric query really does use. An identifier that collides with a word listed
|
||||
* here can still be quoted, which the scan skips over.
|
||||
*/
|
||||
private static final Set<String> WRITE_KEYWORDS = Set.of(
|
||||
"DELETE", "INSERT", "UPDATE", "MERGE", "INTO", "DROP", "ALTER",
|
||||
"CREATE", "GRANT", "REVOKE", "COPY", "RENAME", "ATTACH", "DETACH");
|
||||
|
||||
private static final String DURATION = "[0-9]+(?:ms|[smhdwy])(?:\\s*[0-9]+(?:ms|[smhdwy]))*";
|
||||
|
||||
private static final Pattern GREPTIME_RANGE_CLAUSE = Pattern.compile(
|
||||
"(?i)\\bRANGE\\s*'\\s*" + DURATION + "\\s*'");
|
||||
|
||||
private static final Pattern GREPTIME_ALIGN_CLAUSE = Pattern.compile(
|
||||
"(?i)\\bALIGN\\s*'\\s*" + DURATION + "\\s*'");
|
||||
|
||||
private static final Pattern GREPTIME_FILL_CLAUSE = Pattern.compile(
|
||||
"(?i)\\bFILL\\s+(?:LINEAR|PREV|NEXT|NULL)\\b");
|
||||
|
||||
private final Set<String> allowedTables;
|
||||
|
||||
private final ValidationPolicy validationPolicy;
|
||||
|
||||
public SqlSecurityValidator(Collection<String> allowedTables) {
|
||||
if (CollectionUtils.isEmpty(allowedTables)) {
|
||||
this.allowedTables = new HashSet<>();
|
||||
@@ -55,18 +104,137 @@ public class SqlSecurityValidator {
|
||||
.map(this::normalizeIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
this.validationPolicy = ValidationPolicy.WHITELISTED_SELECT;
|
||||
}
|
||||
|
||||
private SqlSecurityValidator() {
|
||||
this.allowedTables = new HashSet<>();
|
||||
this.validationPolicy = ValidationPolicy.READ_ONLY;
|
||||
}
|
||||
|
||||
/**
|
||||
* A validator whose whole policy is "this statement may only read".
|
||||
*
|
||||
* <p>Use it where there is no table list to validate against: metric tables are created
|
||||
* on demand, one per metric, so the whitelisting constructor would reject every
|
||||
* legitimate metric query.
|
||||
*
|
||||
* <p>It deliberately keeps subqueries, unions and ctes, unlike the whitelisting mode.
|
||||
* Those structures are blocked there because they are the ways a statement can reach a
|
||||
* table the whitelist never mentions; with every table already readable they buy no
|
||||
* protection, while alert expressions do use subqueries and nested aggregation.
|
||||
* @return A validator that accepts only statements proven to be read-only
|
||||
*/
|
||||
public static SqlSecurityValidator selectOnly() {
|
||||
return new SqlSecurityValidator();
|
||||
}
|
||||
|
||||
public void validate(String sql) throws SqlSecurityException {
|
||||
if (sql == null || sql.trim().isEmpty()) {
|
||||
throw new SqlSecurityException("SQL statement cannot be empty");
|
||||
}
|
||||
switch (validationPolicy) {
|
||||
case WHITELISTED_SELECT -> validateAgainstWhitelist(sql);
|
||||
case READ_ONLY -> validateReadOnly(sql);
|
||||
// not dead: a policy added later that nobody wired up here must refuse the
|
||||
// statement rather than fall through this switch having validated nothing
|
||||
default -> throw new SqlSecurityException("Unknown SQL validation policy.");
|
||||
}
|
||||
}
|
||||
|
||||
Statement statement;
|
||||
/**
|
||||
* Read only mode, which runs against a time series database whose sql dialect JSqlParser
|
||||
* does not fully cover: GreptimeDB range queries such as
|
||||
* {@code SELECT avg(v) RANGE '10s' FROM cpu ALIGN '5s'} are rejected by the parser
|
||||
* although they are ordinary reads.
|
||||
*
|
||||
* <p>So the first properties this mode has to guarantee are established without the parser:
|
||||
* statements are counted by scanning outside string literals and comments, the leading
|
||||
* keyword decides whether the statement reads, and no word that only a write contains may
|
||||
* appear anywhere. All three are dialect independent, and the last one is what covers a
|
||||
* write nested where the scan has no structure to reason about, as in
|
||||
* {@code WITH x AS (DELETE FROM cpu RETURNING *) SELECT * FROM x}.
|
||||
*
|
||||
* <p>The parser must still prove the complete statement tree and enumerate every table.
|
||||
* For the Greptime clauses JSqlParser does not understand, a validation-only copy has the
|
||||
* bounded {@code RANGE}, {@code ALIGN}, and {@code FILL} clauses removed before parsing.
|
||||
* Any other unsupported syntax fails closed instead of relying on a keyword denylist as
|
||||
* the sole proof of read-only behavior.
|
||||
* @param sql Statement to validate
|
||||
* @throws SqlSecurityException If the statement writes, or carries more than one statement
|
||||
*/
|
||||
private void validateReadOnly(String sql) throws SqlSecurityException {
|
||||
final StatementShape shape = scan(sql);
|
||||
if (shape.statementCount() != 1) {
|
||||
throw new SqlSecurityException("Only a single statement is allowed.");
|
||||
}
|
||||
if (shape.writeKeyword() != null) {
|
||||
throw new SqlSecurityException("'" + shape.writeKeyword() + "' is not allowed, only reads are.");
|
||||
}
|
||||
if (!SELECT_KEYWORD.equals(shape.leadingKeyword()) && !WITH_KEYWORD.equals(shape.leadingKeyword())) {
|
||||
throw new SqlSecurityException("Only SELECT statements are allowed.");
|
||||
}
|
||||
final Statement statement = parseReadOnlyStatement(sql);
|
||||
|
||||
if (!(statement instanceof Select)) {
|
||||
throw new SqlSecurityException("Only SELECT statements are allowed.");
|
||||
}
|
||||
final List<String> tables = assertNothingWrites(statement);
|
||||
if (tables.stream().map(this::normalizeIdentifier).anyMatch(table -> table.contains("."))) {
|
||||
throw new SqlSecurityException("Schema-qualified tables are not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
private Statement parseReadOnlyStatement(String sql) throws SqlSecurityException {
|
||||
try {
|
||||
statement = CCJSqlParserUtil.parse(sql);
|
||||
return parseSingleStatement(sql);
|
||||
} catch (JSQLParserException originalFailure) {
|
||||
String parserCompatibleSql = GREPTIME_RANGE_CLAUSE.matcher(sql).replaceAll("");
|
||||
parserCompatibleSql = GREPTIME_ALIGN_CLAUSE.matcher(parserCompatibleSql).replaceAll("");
|
||||
parserCompatibleSql = GREPTIME_FILL_CLAUSE.matcher(parserCompatibleSql).replaceAll("");
|
||||
if (parserCompatibleSql.equals(sql)) {
|
||||
throw new SqlSecurityException("Invalid SQL syntax: " + originalFailure.getMessage(), originalFailure);
|
||||
}
|
||||
try {
|
||||
return parseSingleStatement(parserCompatibleSql);
|
||||
} catch (JSQLParserException normalizedFailure) {
|
||||
log.debug("Failed to parse SQL after removing Greptime range clauses: {}", sql, normalizedFailure);
|
||||
throw new SqlSecurityException(
|
||||
"SQL structure could not be verified as a read: " + normalizedFailure.getMessage(),
|
||||
normalizedFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the whole statement rather than its outermost node, because a write hides at any
|
||||
* depth: {@code SELECT * INTO backup FROM cpu UNION SELECT * FROM cpu} puts the write in a
|
||||
* branch of a set operation, and {@code SELECT * FROM (SELECT * INTO backup FROM cpu) t}
|
||||
* puts it in a subquery, so an outermost node that is a plain select proves nothing.
|
||||
*
|
||||
* <p>Any other failure of the walk is a rejection too. A data modifying cte makes
|
||||
* JSqlParser's own finder cast a {@code ParenthesedDelete} to a {@code ParenthesedSelect},
|
||||
* and a walk that ended in an exception established nothing about the statement.
|
||||
* @param statement Parsed statement to walk
|
||||
* @throws SqlSecurityException If any part of the statement writes, or could not be walked
|
||||
*/
|
||||
private List<String> assertNothingWrites(Statement statement) throws SqlSecurityException {
|
||||
try {
|
||||
return new ReadOnlyStatementFinder().getTableList(statement);
|
||||
} catch (SecurityViolationException e) {
|
||||
throw new SqlSecurityException(e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
log.debug("Failed to walk SQL, so nothing about it is established: {}", statement, e);
|
||||
throw new SqlSecurityException("SQL structure could not be verified as a read.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAgainstWhitelist(String sql) throws SqlSecurityException {
|
||||
final Statement statement;
|
||||
try {
|
||||
statement = parseSingleStatement(sql);
|
||||
} catch (JSQLParserException e) {
|
||||
log.warn("Failed to parse SQL: {}", sql, e);
|
||||
log.debug("Failed to parse SQL: {}", sql, e);
|
||||
throw new SqlSecurityException("Invalid SQL syntax: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@@ -74,14 +242,18 @@ public class SqlSecurityValidator {
|
||||
throw new SqlSecurityException("Only SELECT statements are allowed.");
|
||||
}
|
||||
|
||||
// The whitelist is about which tables a statement may touch, so on its own it lets
|
||||
// "select * into backup from hertzbeat_logs" through: every table it names is allowed
|
||||
assertNothingWrites(statement);
|
||||
|
||||
// Check for CTE at top level
|
||||
if (select.getWithItemsList() != null && !select.getWithItemsList().isEmpty()) {
|
||||
throw new SqlSecurityException("CTE (WITH clause) is not allowed");
|
||||
}
|
||||
|
||||
// Use custom TablesNamesFinder that throws on dangerous structures
|
||||
SecurityTablesNamesFinder finder = new SecurityTablesNamesFinder();
|
||||
List<String> tables;
|
||||
final SecurityTablesNamesFinder finder = new SecurityTablesNamesFinder();
|
||||
final List<String> tables;
|
||||
try {
|
||||
tables = finder.getTableList(statement);
|
||||
} catch (SecurityViolationException e) {
|
||||
@@ -91,6 +263,179 @@ public class SqlSecurityValidator {
|
||||
validateTables(tables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sql Statement to parse
|
||||
* @return The only statement the string carries
|
||||
* @throws JSQLParserException If the string does not parse
|
||||
* @throws SqlSecurityException If the string carries more than one statement
|
||||
*/
|
||||
private Statement parseSingleStatement(String sql) throws JSQLParserException, SqlSecurityException {
|
||||
final Statements statements = CCJSqlParserUtil.parseStatements(sql);
|
||||
// ParseStatements rather than parse: parse() returns only the first statement.
|
||||
// Otherwise, "select 1; drop table x" would validate as a plain select.
|
||||
// The caller would still hand the whole string to the database.
|
||||
if (statements.getStatements().size() != 1) {
|
||||
throw new SqlSecurityException("Only a single statement is allowed.");
|
||||
}
|
||||
return statements.getStatements().get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a statement string looks like from outside any sql dialect.
|
||||
* @param statementCount Statements the string carries, a trailing semicolon not counting as one
|
||||
* @param leadingKeyword First word of the first statement, upper cased, empty when it does not start with a word
|
||||
* @param writeKeyword First word from {@link #WRITE_KEYWORDS} found anywhere, null when there is none
|
||||
*/
|
||||
private record StatementShape(int statementCount, String leadingKeyword, String writeKeyword) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the statements a string carries and reads the word it opens with, skipping over
|
||||
* string literals, quoted identifiers and comments so that a semicolon inside them is not
|
||||
* mistaken for a statement separator.
|
||||
*
|
||||
* <p>A backslash is not treated as an escape, because assuming it escapes the closing
|
||||
* quote in a dialect where it does not would let {@code 'a\'; DROP TABLE t} hide a second
|
||||
* statement inside what this scan thinks is one literal. Not assuming it costs at worst a
|
||||
* rejection of a statement that uses backslash escapes, which errs the safe way.
|
||||
* @param sql Statement string to scan
|
||||
* @return The shape of the string
|
||||
* @throws SqlSecurityException If a literal or a block comment is left open
|
||||
*/
|
||||
private StatementShape scan(String sql) throws SqlSecurityException {
|
||||
int statementCount = 0;
|
||||
boolean statementHasContent = false;
|
||||
String leadingKeyword = "";
|
||||
String writeKeyword = null;
|
||||
int index = 0;
|
||||
while (index < sql.length()) {
|
||||
final char current = sql.charAt(index);
|
||||
if (current == '-' && index + 1 < sql.length() && sql.charAt(index + 1) == '-') {
|
||||
final int lineEnd = sql.indexOf('\n', index);
|
||||
index = lineEnd < 0 ? sql.length() : lineEnd + 1;
|
||||
} else if (current == '/' && index + 1 < sql.length() && sql.charAt(index + 1) == '*') {
|
||||
final int commentEnd = sql.indexOf("*/", index + 2);
|
||||
if (commentEnd < 0) {
|
||||
throw new SqlSecurityException("Unterminated block comment.");
|
||||
}
|
||||
index = commentEnd + 2;
|
||||
} else if (current == '$') {
|
||||
rejectDollarQuote(sql, index);
|
||||
statementHasContent = true;
|
||||
index++;
|
||||
} else if (current == '\'' || current == '"' || current == '`') {
|
||||
index = skipQuoted(sql, index, current);
|
||||
statementHasContent = true;
|
||||
} else if (current == ';') {
|
||||
if (statementHasContent) {
|
||||
statementCount++;
|
||||
}
|
||||
statementHasContent = false;
|
||||
index++;
|
||||
} else if (Character.isLetter(current) || current == '_') {
|
||||
// Read the whole word and step past it.
|
||||
// Match a write keyword only on its own, never inside an identifier like delete_count.
|
||||
final int wordEnd = wordEnd(sql, index);
|
||||
final String word = sql.substring(index, wordEnd).toUpperCase(Locale.ROOT);
|
||||
if (statementCount == 0 && !statementHasContent) {
|
||||
leadingKeyword = word;
|
||||
}
|
||||
if (writeKeyword == null && WRITE_KEYWORDS.contains(word)) {
|
||||
writeKeyword = word;
|
||||
}
|
||||
statementHasContent = true;
|
||||
index = wordEnd;
|
||||
} else {
|
||||
if (!Character.isWhitespace(current)) {
|
||||
statementHasContent = true;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (statementHasContent) {
|
||||
statementCount++;
|
||||
}
|
||||
return new StatementShape(statementCount, leadingKeyword, writeKeyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sql Statement string being scanned
|
||||
* @param start Index of the opening quote
|
||||
* @param quote Quote character to close on, a doubled one being an escaped quote rather than the close
|
||||
* @return Index just past the closing quote
|
||||
* @throws SqlSecurityException If the quote is never closed
|
||||
*/
|
||||
private int skipQuoted(String sql, int start, char quote) throws SqlSecurityException {
|
||||
int index = start + 1;
|
||||
while (index < sql.length()) {
|
||||
if (sql.charAt(index) == quote) {
|
||||
if (index + 1 < sql.length() && sql.charAt(index + 1) == quote) {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
return index + 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
throw new SqlSecurityException("Unterminated quoted literal.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects a PostgreSQL dollar-quoted literal, {@code $$...$$} or {@code $tag$...$tag$}.
|
||||
*
|
||||
* <p>Comment markers and semicolons inside such a literal are data, so a scan that reads
|
||||
* them as syntax loses the rest of the input: {@code SELECT $$--$$; DROP TABLE cpu} looks
|
||||
* like one statement once the {@code --} is taken for a comment.
|
||||
*
|
||||
* <p>Rejecting rather than skipping, because skipping would be the same assumption in
|
||||
* reverse. A dialect without dollar quoting means the semicolon inside one really does
|
||||
* separate statements, and a scan that skipped the literal would be the thing hiding
|
||||
* them. Rejecting needs no assumption either way: a dialect that has dollar quoting is
|
||||
* refused, and one that does not was going to fail on the syntax regardless. No read of a
|
||||
* metric table spells anything this way.
|
||||
* @param sql Statement string being scanned
|
||||
* @param start Index of the dollar sign
|
||||
* @throws SqlSecurityException If a dollar-quoted literal opens here
|
||||
*/
|
||||
private void rejectDollarQuote(String sql, int start) throws SqlSecurityException {
|
||||
int tagEnd = start + 1;
|
||||
if (tagEnd >= sql.length()) {
|
||||
return;
|
||||
}
|
||||
final char firstTagCharacter = sql.charAt(tagEnd);
|
||||
if (firstTagCharacter != '$'
|
||||
&& !Character.isLetter(firstTagCharacter)
|
||||
&& firstTagCharacter != '_') {
|
||||
// a positional parameter such as $1, or a dollar sign that is just a character
|
||||
return;
|
||||
}
|
||||
while (tagEnd < sql.length()
|
||||
&& (Character.isLetterOrDigit(sql.charAt(tagEnd)) || sql.charAt(tagEnd) == '_')) {
|
||||
tagEnd++;
|
||||
}
|
||||
if (tagEnd >= sql.length() || sql.charAt(tagEnd) != '$') {
|
||||
return;
|
||||
}
|
||||
throw new SqlSecurityException("Dollar-quoted literals are not allowed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sql Statement string being scanned
|
||||
* @param start Index of the first character of a word
|
||||
* @return Index just past the word, digits and underscores counting as part of it so that
|
||||
* {@code delete_count} is one word rather than a {@code delete} followed by a remainder.
|
||||
* A dollar sign ends the word, so that a literal opening right after an identifier is
|
||||
* still seen by {@link #rejectDollarQuote}
|
||||
*/
|
||||
private int wordEnd(String sql, int start) {
|
||||
int index = start;
|
||||
while (index < sql.length()
|
||||
&& (Character.isLetterOrDigit(sql.charAt(index)) || sql.charAt(index) == '_')) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private void validateTables(List<String> tables) throws SqlSecurityException {
|
||||
if (CollectionUtils.isEmpty(tables)) {
|
||||
return;
|
||||
@@ -121,6 +466,46 @@ public class SqlSecurityValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private enum ValidationPolicy {
|
||||
WHITELISTED_SELECT,
|
||||
READ_ONLY
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks a statement and throws as soon as it finds a part of it that writes, at any depth.
|
||||
*/
|
||||
private static class ReadOnlyStatementFinder extends TablesNamesFinder<Void> {
|
||||
|
||||
@Override
|
||||
public Void visit(PlainSelect plainSelect, Object context) {
|
||||
// SELECT ... INTO writes a new table in the dialects that support it, so it is not a read.
|
||||
if (!CollectionUtils.isEmpty(plainSelect.getIntoTables())) {
|
||||
throw new SecurityViolationException("SELECT ... INTO is not allowed.");
|
||||
}
|
||||
return super.visit(plainSelect, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(Delete delete, Object context) {
|
||||
throw new SecurityViolationException("DELETE is not allowed, only reads are.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(Insert insert, Object context) {
|
||||
throw new SecurityViolationException("INSERT is not allowed, only reads are.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(Update update, Object context) {
|
||||
throw new SecurityViolationException("UPDATE is not allowed, only reads are.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(Merge merge, Object context) {
|
||||
throw new SecurityViolationException("MERGE is not allowed, only reads are.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom TablesNamesFinder that throws exceptions on dangerous SQL structures.
|
||||
* Extends TablesNamesFinder with proper generic type to avoid raw type warnings.
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.ai;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests AI conversation serialization.
|
||||
*/
|
||||
class ChatConversationTest {
|
||||
|
||||
@Test
|
||||
void serializationShouldExcludeStoredSecurityData() {
|
||||
ChatConversation conversation = ChatConversation.builder()
|
||||
.id(1L)
|
||||
.title("Owned conversation")
|
||||
.securityData("encrypted-value")
|
||||
.build();
|
||||
|
||||
String json = JsonUtil.toJson(conversation);
|
||||
|
||||
assertNotNull(json);
|
||||
assertFalse(json.contains("securityData"));
|
||||
assertFalse(json.contains("encrypted-value"));
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.alerter;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.LongStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Test case for {@link AlertDefine}
|
||||
*/
|
||||
class AlertDefineTest {
|
||||
|
||||
private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
|
||||
private final Validator validator = factory.getValidator();
|
||||
|
||||
@Test
|
||||
void exprShouldAllowBindingManyMonitors() {
|
||||
String boundMonitors = LongStream.range(0, 76)
|
||||
.mapToObj(id -> "equals(__instance__, \"" + (653868108767488L + id) + "\")")
|
||||
.collect(Collectors.joining(" or "));
|
||||
String expr = "equals(__app__,\"ping\") && equals(__available__,\"down\") && (" + boundMonitors + ")";
|
||||
assertTrue(expr.length() > 2048);
|
||||
|
||||
AlertDefine define = AlertDefine.builder()
|
||||
.name("ping-offline")
|
||||
.type("realtime_metric")
|
||||
.expr(expr)
|
||||
.template("instance {{ $labels.instance }} is offline")
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<AlertDefine>> violations = validator.validate(define);
|
||||
assertTrue(violations.isEmpty(), "binding many monitors should not fail validation: " + violations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oversizedExprShouldReportCharacterLength() {
|
||||
AlertDefine define = AlertDefine.builder()
|
||||
.name("ping-offline")
|
||||
.type("realtime_metric")
|
||||
.expr("x".repeat(65536))
|
||||
.template("template")
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<AlertDefine>> violations = validator.validate(define);
|
||||
assertEquals(1, violations.size());
|
||||
assertEquals("expr", violations.iterator().next().getPropertyPath().toString());
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.support;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
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.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* Test case for {@link SseEmitterRegistry}.
|
||||
*
|
||||
* <p>Every open subscription holds a request thread for as long as it lives, so both how long
|
||||
* one may live and how many may exist at once have to be bounded, and every way a
|
||||
* subscription can end has to give its slot back.
|
||||
*/
|
||||
class SseEmitterRegistryTest {
|
||||
|
||||
private SseEmitterRegistry registry;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
registry = new SseEmitterRegistry("test");
|
||||
}
|
||||
|
||||
/**
|
||||
* An unbounded emitter never expires on its own, so a client that goes away without
|
||||
* closing cleanly keeps holding its request thread until the container notices.
|
||||
*/
|
||||
@Test
|
||||
void testSubscriptionsAreGivenFiniteTimeout() {
|
||||
final SseEmitter emitter = registry.createEmitter(1L);
|
||||
|
||||
assertNotNull(emitter.getTimeout());
|
||||
assertTrue(emitter.getTimeout() > 0 && emitter.getTimeout() < Long.MAX_VALUE,
|
||||
"timeout must be finite, was " + emitter.getTimeout());
|
||||
}
|
||||
|
||||
/**
|
||||
* Each open subscription occupies a request thread, so enough of them in parallel
|
||||
* exhaust the container's pool and take the whole application down.
|
||||
*/
|
||||
@Test
|
||||
void testSubscriptionsBeyondLimitAreRefused() {
|
||||
registry.setMaxEmitters(2);
|
||||
|
||||
registry.createEmitter(1L);
|
||||
registry.createEmitter(2L);
|
||||
final ResponseStatusException thrown =
|
||||
assertThrows(ResponseStatusException.class, () -> registry.createEmitter(3L));
|
||||
|
||||
assertEquals(HttpStatus.SERVICE_UNAVAILABLE, thrown.getStatusCode());
|
||||
assertEquals(2, registry.subscriptionCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* The cap must not become a permanent lockout: once a dead subscription is cleaned up,
|
||||
* its slot has to be available again.
|
||||
*/
|
||||
@Test
|
||||
void testDroppedSubscriptionFreesItsSlot() throws Exception {
|
||||
registry.setMaxEmitters(1);
|
||||
registry.createEmitter(1L);
|
||||
assertThrows(ResponseStatusException.class, () -> registry.createEmitter(2L));
|
||||
|
||||
// A client that went away makes the next send fail, which is how the registry notices
|
||||
final SseEmitter deadEmitter = mock(SseEmitter.class);
|
||||
doThrow(new IllegalStateException("client gone")).when(deadEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
replaceEmitter(1L, deadEmitter);
|
||||
|
||||
registry.broadcast("TEST_EVENT", "{\"id\":1}");
|
||||
|
||||
assertEquals(0, registry.subscriptionCount());
|
||||
assertNotNull(registry.createEmitter(2L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing else frees the slot of a subscription that dies on an error: a stream that only
|
||||
* broadcasts now and then would let a failed client hold its slot indefinitely and the
|
||||
* ceiling would drift into a permanent lockout.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testErroredSubscriptionFreesItsSlot() {
|
||||
registry.setMaxEmitters(1);
|
||||
|
||||
try (MockedConstruction<SseEmitter> construction = mockConstruction(SseEmitter.class)) {
|
||||
registry.createEmitter(1L);
|
||||
final SseEmitter created = construction.constructed().get(0);
|
||||
final ArgumentCaptor<Consumer<Throwable>> onError = ArgumentCaptor.forClass(Consumer.class);
|
||||
verify(created).onError(onError.capture());
|
||||
|
||||
onError.getValue().accept(new IllegalStateException("client gone"));
|
||||
|
||||
assertEquals(0, registry.subscriptionCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completing a dropped subscription is a courtesy, so a container that refuses it must
|
||||
* not keep the subscription registered and its slot taken.
|
||||
*/
|
||||
@Test
|
||||
void testSubscriptionIsDroppedEvenWhenCompletingItFails() throws Exception {
|
||||
registry.createEmitter(1L);
|
||||
final SseEmitter unusableEmitter = mock(SseEmitter.class);
|
||||
doThrow(new IllegalStateException("client gone"))
|
||||
.when(unusableEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
doThrow(new RuntimeException("complete failed")).when(unusableEmitter).complete();
|
||||
replaceEmitter(1L, unusableEmitter);
|
||||
|
||||
assertDoesNotThrow(() -> registry.broadcast("TEST_EVENT", "{\"id\":1}"));
|
||||
|
||||
assertFalse(emitters().containsKey(1L), "a subscription that cannot be completed still has to be dropped");
|
||||
assertEquals(0, registry.subscriptionCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* A slot is taken before the subscription exists, so anything that goes wrong between the
|
||||
* two has to give it back: nothing ends up in the map to be removed later, and the slot
|
||||
* would be held until the process restarts.
|
||||
*/
|
||||
@Test
|
||||
void testSlotIsGivenBackWhenRegisteringFails() {
|
||||
registry.setMaxEmitters(1);
|
||||
|
||||
try (MockedConstruction<SseEmitter> failing = mockConstruction(SseEmitter.class, (mock, context) -> {
|
||||
throw new IllegalStateException("cannot open a subscription");
|
||||
})) {
|
||||
// The failure is raised through mockito, so what reaches the caller is whatever it
|
||||
// wraps the initializer's exception in; only that it fails matters here
|
||||
assertThrows(RuntimeException.class, () -> registry.createEmitter(1L));
|
||||
}
|
||||
|
||||
assertEquals(0, registry.subscriptionCount());
|
||||
assertNotNull(registry.createEmitter(2L), "a failed registration must not spend the ceiling");
|
||||
}
|
||||
|
||||
/**
|
||||
* A caller that has no id for its subscriber cannot be registered at all, so it must be
|
||||
* turned away before it takes a slot rather than after.
|
||||
*/
|
||||
@Test
|
||||
void testSubscriptionWithoutClientIdTakesNoSlot() {
|
||||
assertThrows(NullPointerException.class, () -> registry.createEmitter(null));
|
||||
|
||||
assertEquals(0, registry.subscriptionCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* The ceiling has to hold for subscriptions that arrive together, which is the only case
|
||||
* that matters: a burst is what exhausts the thread pool, and a burst is exactly what a
|
||||
* check of the current count followed by a separate registration lets straight through.
|
||||
*/
|
||||
@Test
|
||||
void testCeilingHoldsWhenSubscriptionsArriveTogether() throws Exception {
|
||||
final int maxEmitters = 10;
|
||||
final int racers = 200;
|
||||
registry.setMaxEmitters(maxEmitters);
|
||||
final CyclicBarrier allReady = new CyclicBarrier(racers);
|
||||
final AtomicInteger accepted = new AtomicInteger();
|
||||
final ExecutorService pool = Executors.newFixedThreadPool(racers);
|
||||
|
||||
try {
|
||||
for (int i = 0; i < racers; i++) {
|
||||
final long clientId = i;
|
||||
pool.submit(() -> {
|
||||
try {
|
||||
// Every thread is released at the same instant, so they all reach the
|
||||
// ceiling check together
|
||||
allReady.await(10, TimeUnit.SECONDS);
|
||||
registry.createEmitter(clientId);
|
||||
accepted.incrementAndGet();
|
||||
} catch (ResponseStatusException refused) {
|
||||
// Expected for everyone who arrives after the ceiling is reached
|
||||
} catch (Exception e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.shutdown();
|
||||
assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS), "subscriptions did not settle");
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
|
||||
assertEquals(maxEmitters, accepted.get(), "more subscriptions were accepted than the ceiling allows");
|
||||
assertEquals(maxEmitters, registry.subscriptionCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stands in for a client that went away: the registry only learns about it when a send
|
||||
* fails, which needs an emitter that fails on demand.
|
||||
*/
|
||||
private void replaceEmitter(Long clientId, SseEmitter emitter) throws Exception {
|
||||
emitters().put(clientId, emitter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<Long, SseEmitter> emitters() throws Exception {
|
||||
final Field emittersField = SseEmitterRegistry.class.getDeclaredField("emitters");
|
||||
emittersField.setAccessible(true);
|
||||
return (Map<Long, SseEmitter>) emittersField.get(registry);
|
||||
}
|
||||
}
|
||||
+232
@@ -264,4 +264,236 @@ class SqlSecurityValidatorTest {
|
||||
assertThrows(SqlSecurityException.class, () -> validator.validate(
|
||||
"SELECT * FROM hertzbeat_logs WHERE id = 1 + (SELECT id FROM secret_table)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* `CCJSqlParserUtil.parse` returns the first statement and silently discards the rest,
|
||||
* so a stacked statement used to validate as a plain select while the caller still
|
||||
* handed the whole string to the database.
|
||||
*/
|
||||
@Test
|
||||
void testStackedStatementIsRejected() {
|
||||
assertThrows(SqlSecurityException.class, () -> validator.validate(
|
||||
"SELECT * FROM hertzbeat_logs; DROP TABLE hertzbeat_logs"));
|
||||
assertThrows(SqlSecurityException.class, () -> SqlSecurityValidator.selectOnly().validate(
|
||||
"SELECT 1; DROP TABLE cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> SqlSecurityValidator.selectOnly().validate(
|
||||
"SELECT $$--$$; DROP TABLE cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> SqlSecurityValidator.selectOnly().validate(
|
||||
"SELECT $body$--$body$; DROP TABLE cpu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTrailingSemicolonIsStillAcceptedAsOneStatement() {
|
||||
assertDoesNotThrow(() -> validator.validate("SELECT * FROM hertzbeat_logs ; "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectOnlyRejectsWrites() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("DROP TABLE cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("DELETE FROM cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("INSERT INTO cpu VALUES (1)"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("UPDATE cpu SET value = 1"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("TRUNCATE TABLE cpu"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric tables are created per metric on demand, so this mode constrains what a
|
||||
* statement may do, not which table it may touch.
|
||||
*/
|
||||
@Test
|
||||
void testSelectOnlyAcceptsAnyTableAndNestedReads() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT value FROM any_metric_table"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate(
|
||||
"SELECT value FROM cpu WHERE host = (SELECT host FROM hosts LIMIT 1)"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT a FROM t1 UNION ALL SELECT b FROM t2"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The only sql executor today talks to GreptimeDB, whose range query syntax JSqlParser
|
||||
* cannot parse. These are ordinary reads and used to run, so read only mode has to keep
|
||||
* accepting them rather than turn a richer dialect into a rule that no longer fires.
|
||||
*/
|
||||
@Test
|
||||
void testSelectOnlyAcceptsDialectTheParserDoesNotUnderstand() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate(
|
||||
"SELECT ts, avg(value) RANGE '10s' FROM cpu ALIGN '5s' FILL LINEAR"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu ALIGN '5s'"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepting what the parser cannot read must not become a way through: the statement
|
||||
* count and the leading keyword are established without the parser, so they still hold
|
||||
* for a string it never understood.
|
||||
*/
|
||||
@Test
|
||||
void testUnparsableStatementMustStillBeOneRead() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT avg(value) RANGE '10s' FROM cpu ALIGN '5s'; DROP TABLE cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("DROP TABLE cpu ALIGN '5s'"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("TQL EVAL (0, 10, '5s') sum(cpu)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* A semicolon that is data or commentary is not a statement separator, and a statement
|
||||
* scan that cannot tell the difference would reject ordinary queries.
|
||||
*/
|
||||
@Test
|
||||
void testSemicolonInsideLiteralOrCommentDoesNotSplitTheStatement() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu WHERE msg = 'a; DROP TABLE cpu'"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu WHERE msg = 'it''s; fine'"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu -- ; DROP TABLE cpu"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu /* ; DROP TABLE cpu */ LIMIT 1"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT value FROM \"cpu;usage\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnclosedLiteralOrCommentIsRejected() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT * FROM cpu WHERE msg = 'open"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT * FROM cpu /* open"));
|
||||
}
|
||||
|
||||
/**
|
||||
* A dollar-quoted literal is refused outright rather than skipped over, so that neither
|
||||
* answer to "does this dialect have dollar quoting" can hide a statement.
|
||||
*
|
||||
* <p>Skipping would lose a stacked statement to a dialect that has it, since the comment
|
||||
* marker in {@code SELECT $$--$$; DROP TABLE cpu} is data rather than a comment. Skipping
|
||||
* would equally lose one to a dialect that does not, since the semicolon in
|
||||
* {@code SELECT 1 $$;DROP TABLE cpu$$} really does separate statements there.
|
||||
*/
|
||||
@Test
|
||||
void testDollarQuotedLiteralIsRejectedRatherThanSkipped() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT $$--$$; DROP TABLE cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT 1 $$;DROP TABLE cpu$$"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT 1 $t$;DROP TABLE cpu$t$"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT $$a; -- DROP TABLE cpu$$"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT $body$a; -- DROP TABLE cpu$body$"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT $$open"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT $body$open"));
|
||||
// a literal opening right after an identifier is still a literal
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate("SELECT a$$b;c$$ FROM cpu"));
|
||||
}
|
||||
|
||||
/**
|
||||
* A dollar sign that opens nothing is an ordinary character, so reads keep working.
|
||||
*/
|
||||
@Test
|
||||
void testLoneDollarSignIsNotTreatedAsLiteral() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT $1 FROM cpu"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT a$b FROM cpu"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu WHERE cost = '$5; x'"));
|
||||
}
|
||||
|
||||
/**
|
||||
* `SELECT ... INTO` parses as a select but writes a table in the dialects that support it.
|
||||
*/
|
||||
@Test
|
||||
void testSelectOnlyRejectsSelectInto() {
|
||||
assertThrows(SqlSecurityException.class, () -> SqlSecurityValidator.selectOnly()
|
||||
.validate("SELECT * INTO backup FROM cpu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectOnlyAcceptsCte() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate("WITH x AS (SELECT 1 AS v) SELECT * FROM x"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate(
|
||||
"WITH x AS (SELECT avg(v) RANGE '10s' FROM cpu ALIGN '5s') SELECT * FROM x"));
|
||||
}
|
||||
|
||||
/**
|
||||
* An outermost node that is a plain select proves nothing about the rest of the tree: a
|
||||
* write hides in a cte, in a branch of a set operation, or in a subquery, and JSqlParser
|
||||
* reports the outermost node of all three as a select.
|
||||
*/
|
||||
@Test
|
||||
void testSelectOnlyRejectsWritesNestedInsideReads() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"WITH x AS (DELETE FROM cpu RETURNING *) SELECT * FROM x"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"WITH x AS (INSERT INTO cpu VALUES (1) RETURNING *) SELECT * FROM x"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"WITH x AS (SELECT id FROM t) DELETE FROM cpu WHERE id IN (SELECT id FROM x)"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT * INTO backup FROM cpu UNION SELECT * FROM cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT * FROM cpu UNION SELECT * INTO backup FROM cpu"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT * FROM (SELECT * INTO backup FROM cpu) t"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The nested writes above have to stay rejected when the parser cannot read the dialect
|
||||
* and there is no tree to walk, which is the case the whole read only mode exists for.
|
||||
*/
|
||||
@Test
|
||||
void testNestedWritesStayRejectedWithoutTheParser() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT * FROM (DELETE FROM cpu RETURNING *) t ALIGN '5s'"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"WITH x AS (DELETE FROM cpu RETURNING *) SELECT avg(v) RANGE '10s' FROM x ALIGN '5s'"));
|
||||
assertThrows(SqlSecurityException.class, () -> selectOnly.validate(
|
||||
"SELECT * INTO backup FROM cpu ALIGN '5s'"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The word scan matches whole words only, so ordinary reads whose identifiers or functions
|
||||
* merely contain one keep working. An identifier that collides outright can be quoted.
|
||||
*/
|
||||
@Test
|
||||
void testWriteWordScanDoesNotCatchOrdinaryReads() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT delete_count, insert_rate FROM cpu"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT truncate(value, 2) FROM cpu"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT replace(msg, 'a', 'b') FROM logs"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT * FROM cpu WHERE msg = 'drop table cpu'"));
|
||||
assertDoesNotThrow(() -> selectOnly.validate("SELECT \"drop\" FROM cpu"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The whitelist says which tables a statement may touch, so on its own it lets a write
|
||||
* through as long as every table it names is allowed.
|
||||
*/
|
||||
@Test
|
||||
void testWhitelistModeRejectsSelectIntoOnAnAllowedTable() {
|
||||
assertThrows(SqlSecurityException.class, () -> validator.validate(
|
||||
"SELECT * INTO backup FROM hertzbeat_logs"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The whitelisting mode needs the parse tree to enumerate table names, so unlike read
|
||||
* only mode it has nothing to fall back on and keeps rejecting what it cannot parse.
|
||||
*/
|
||||
@Test
|
||||
void testWhitelistModeStillRejectsWhatItCannotParse() {
|
||||
assertThrows(SqlSecurityException.class, () -> validator.validate(
|
||||
"SELECT avg(value) RANGE '10s' FROM hertzbeat_logs ALIGN '5s'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectOnlyCannotEscapeTheConfiguredDatabase() {
|
||||
final SqlSecurityValidator selectOnly = SqlSecurityValidator.selectOnly();
|
||||
|
||||
assertThrows(SqlSecurityException.class,
|
||||
() -> selectOnly.validate("SELECT * FROM information_schema.tables"));
|
||||
assertThrows(SqlSecurityException.class,
|
||||
() -> selectOnly.validate("SELECT * FROM pg_catalog.pg_tables"));
|
||||
assertThrows(SqlSecurityException.class,
|
||||
() -> selectOnly.validate("SELECT * FROM public.cpu"));
|
||||
assertThrows(SqlSecurityException.class,
|
||||
() -> selectOnly.validate("SELECT * FROM \"information_schema\".\"tables\""));
|
||||
assertThrows(SqlSecurityException.class,
|
||||
() -> selectOnly.validate(
|
||||
"SELECT * FROM cpu, information_schema.tables RANGE '1m' ALIGN '1m'"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hertzbeat-log-e2e</artifactId>
|
||||
<artifactId>hertzbeat-observability-e2e</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
@@ -52,7 +52,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-log</artifactId>
|
||||
<artifactId>hertzbeat-observability</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.alert;
|
||||
package org.apache.hertzbeat.observability.alert;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.alert;
|
||||
package org.apache.hertzbeat.observability.alert;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.ingestion;
|
||||
package org.apache.hertzbeat.observability.ingestion;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.observability.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
|
||||
import io.opentelemetry.proto.common.v1.AnyValue;
|
||||
import io.opentelemetry.proto.common.v1.KeyValue;
|
||||
import io.opentelemetry.proto.logs.v1.LogRecord;
|
||||
import io.opentelemetry.proto.logs.v1.ResourceLogs;
|
||||
import io.opentelemetry.proto.logs.v1.ScopeLogs;
|
||||
import io.opentelemetry.proto.resource.v1.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeOtlpSignalStorage;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
/** Proves the warehouse-owned Entity-free OTLP log path against a real GreptimeDB. */
|
||||
@Testcontainers
|
||||
class GreptimeEntityFreeSignalStorageE2eTest {
|
||||
|
||||
private static final int GREPTIME_HTTP_PORT = 4000;
|
||||
private static final int GREPTIME_GRPC_PORT = 4001;
|
||||
private static final String LOG_SCHEMA = "greptime/tables/hertzbeat_logs.sql";
|
||||
private static final String LOG_PIPELINE = "greptime/pipelines/hertzbeat_otlp_log_v1.yaml";
|
||||
private static final String PIPELINE_NAME = "hertzbeat_otlp_log_v1";
|
||||
private static final String TRACE_ID = "0123456789abcdef0123456789abcdef";
|
||||
private static final String SPAN_ID = "0123456789abcdef";
|
||||
private static final String BODY = "entity-free greptime proof";
|
||||
private static final long LOG_TIME_NANOS = 1_710_000_000_123_456_789L;
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Container
|
||||
@SuppressWarnings("resource")
|
||||
private static final GenericContainer<?> GREPTIME = new GenericContainer<>(
|
||||
DockerImageName.parse("greptime/greptimedb:latest"))
|
||||
.withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
|
||||
.withCommand("standalone", "start",
|
||||
"--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
|
||||
"--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
|
||||
.waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
|
||||
.withStartupTimeout(Duration.ofSeconds(120));
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
@Test
|
||||
void warehouseStorageShouldPersistEntityFreeOtlpLogs() throws Exception {
|
||||
executeSql(classpathResource(LOG_SCHEMA).strip().replaceFirst(";\\s*$", ""));
|
||||
uploadPipeline();
|
||||
|
||||
GreptimeOtlpSignalStorage storage = new GreptimeOtlpSignalStorage(
|
||||
new GreptimeProperties(true, GREPTIME.getHost() + ':' + GREPTIME.getMappedPort(GREPTIME_GRPC_PORT),
|
||||
endpoint(), "public", "", ""),
|
||||
new RestTemplate());
|
||||
|
||||
storage.writeProtobuf("logs", request().toByteArray());
|
||||
|
||||
await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
|
||||
String sql = "SELECT COUNT(*) AS count FROM hertzbeat_logs WHERE trace_id = '" + TRACE_ID
|
||||
+ "' AND body = '" + BODY + "'";
|
||||
assertThat(queryCount(sql)).isEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
private void uploadPipeline() throws Exception {
|
||||
String boundary = "----hertzbeat-entity-free-proof";
|
||||
String body = "--" + boundary + "\r\n"
|
||||
+ "Content-Disposition: form-data; name=\"file\"; filename=\"pipeline.yaml\"\r\n"
|
||||
+ "Content-Type: application/x-yaml\r\n\r\n"
|
||||
+ classpathResource(LOG_PIPELINE) + "\r\n"
|
||||
+ "--" + boundary + "--\r\n";
|
||||
HttpResponse<String> response = httpClient.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint() + "/v1/pipelines/" + PIPELINE_NAME))
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).as(response.body()).isBetween(200, 299);
|
||||
}
|
||||
|
||||
private ExportLogsServiceRequest request() {
|
||||
LogRecord record = LogRecord.newBuilder()
|
||||
.setTimeUnixNano(LOG_TIME_NANOS)
|
||||
.setObservedTimeUnixNano(LOG_TIME_NANOS)
|
||||
.setSeverityNumberValue(9)
|
||||
.setSeverityText("INFO")
|
||||
.setBody(AnyValue.newBuilder().setStringValue(BODY).build())
|
||||
.setTraceId(ByteString.copyFrom(hexToBytes(TRACE_ID)))
|
||||
.setSpanId(ByteString.copyFrom(hexToBytes(SPAN_ID)))
|
||||
.build();
|
||||
return ExportLogsServiceRequest.newBuilder()
|
||||
.addResourceLogs(ResourceLogs.newBuilder()
|
||||
.setResource(Resource.newBuilder()
|
||||
.addAttributes(stringAttribute("service.name", "checkout"))
|
||||
.addAttributes(stringAttribute("deployment.environment.name", "test"))
|
||||
.build())
|
||||
.addScopeLogs(ScopeLogs.newBuilder().addLogRecords(record).build())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private int queryCount(String sql) throws Exception {
|
||||
JsonNode rows = OBJECT_MAPPER.readTree(executeSql(sql).body())
|
||||
.path("output").path(0).path("records").path("rows");
|
||||
assertThat(rows.isArray()).isTrue();
|
||||
assertThat(rows).isNotEmpty();
|
||||
return rows.get(0).get(0).asInt();
|
||||
}
|
||||
|
||||
private HttpResponse<String> executeSql(String sql) throws Exception {
|
||||
HttpResponse<String> response = httpClient.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint() + "/v1/sql?db=public"))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(
|
||||
"sql=" + URLEncoder.encode(sql, StandardCharsets.UTF_8), StandardCharsets.UTF_8))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertThat(response.statusCode()).as(response.body()).isBetween(200, 299);
|
||||
return response;
|
||||
}
|
||||
|
||||
private String classpathResource(String path) throws Exception {
|
||||
try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
|
||||
assertThat(input).as(path).isNotNull();
|
||||
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyValue stringAttribute(String key, String value) {
|
||||
return KeyValue.newBuilder().setKey(key)
|
||||
.setValue(AnyValue.newBuilder().setStringValue(value).build()).build();
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String value) {
|
||||
byte[] bytes = new byte[value.length() / 2];
|
||||
for (int index = 0; index < value.length(); index += 2) {
|
||||
bytes[index / 2] = (byte) Integer.parseInt(value.substring(index, index + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static String endpoint() {
|
||||
return "http://" + GREPTIME.getHost() + ':' + GREPTIME.getMappedPort(GREPTIME_HTTP_PORT);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user