Skip to content

Commit 45ce560

Browse files
feat(health): enhance health check functionality with new metrics and reporting
- Added new metrics for latency, jitter, and load to health check results, providing deeper insights into network performance. - Implemented optional checks for entropy and active-probe, allowing for more flexible health assessments. - Updated JSON reporting to include additional fields for new metrics, improving machine-readable output for CI integration. - Enhanced the web interface to display new metrics and improve user experience during health checks. These changes significantly improve the robustness and usability of the health check system, offering users more comprehensive data and clearer reporting.
1 parent 6c2ea44 commit 45ce560

23 files changed

Lines changed: 1849 additions & 31 deletions

File tree

.github/workflows/ci.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,64 @@ jobs:
3939
./hiddify-health check "$dir" || exit 1
4040
done
4141
42+
protocol-test:
43+
name: Protocol health gate
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Set up Go
49+
uses: actions/setup-go@v5
50+
with:
51+
go-version-file: go.mod
52+
cache: true
53+
54+
- name: Install sing-box
55+
run: |
56+
SB_VER=1.13.13
57+
curl -fsSL -o sb.tgz \
58+
"https://github.com/SagerNet/sing-box/releases/download/v${SB_VER}/sing-box-${SB_VER}-linux-amd64.tar.gz"
59+
tar -xzf sb.tgz
60+
sudo install "sing-box-${SB_VER}-linux-amd64/sing-box" /usr/local/bin/sing-box
61+
sing-box version
62+
63+
- name: Install xray
64+
run: |
65+
XR_VER=26.3.27
66+
curl -fsSL -o xray.zip \
67+
"https://github.com/XTLS/Xray-core/releases/download/v${XR_VER}/Xray-linux-64.zip"
68+
unzip -o xray.zip xray
69+
sudo install xray /usr/local/bin/xray
70+
xray version | head -1
71+
72+
- name: Build
73+
run: go build -o hiddify-health ./cmd
74+
75+
- name: Run all protocol tests (JSON gate)
76+
env:
77+
SINGBOX_BIN: /usr/local/bin/sing-box
78+
XRAY_CLIENT_PATH: /usr/local/bin/xray
79+
XRAY_SERVER_PATH: /usr/local/bin/xray
80+
run: |
81+
# Warn-only checks (quic/probe/entropy/load/tls-fp) do not fail the run.
82+
# Exit non-zero only on a real connectivity failure.
83+
./hiddify-health run-all examples/ --json | tee result.json
84+
# Fail the job if the report records any failures.
85+
python3 -c "import json,sys; d=json.load(open('result.json')); sys.exit(1 if d['failed'] else 0)"
86+
87+
- name: Generate HTML report
88+
if: always()
89+
run: ./hiddify-health report --html report.html
90+
91+
- name: Upload report artifact
92+
if: always()
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: health-report
96+
path: |
97+
report.html
98+
result.json
99+
42100
cross-compile:
43101
name: Cross-compile
44102
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
build:
12+
name: Build & release binaries
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
include:
17+
- { goos: linux, goarch: amd64 }
18+
- { goos: linux, goarch: arm64 }
19+
- { goos: darwin, goarch: amd64 }
20+
- { goos: darwin, goarch: arm64 }
21+
- { goos: windows, goarch: amd64, ext: .exe }
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Set up Go
26+
uses: actions/setup-go@v5
27+
with:
28+
go-version-file: go.mod
29+
cache: true
30+
31+
- name: Build
32+
env:
33+
GOOS: ${{ matrix.goos }}
34+
GOARCH: ${{ matrix.goarch }}
35+
run: |
36+
VERSION=${GITHUB_REF_NAME}
37+
OUT="hiddify-health-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}"
38+
go build -ldflags "-s -w -X main.Version=${VERSION}" -o "$OUT" ./cmd
39+
echo "ASSET=$OUT" >> "$GITHUB_ENV"
40+
41+
- name: Attach binary to release
42+
uses: softprops/action-gh-release@v2
43+
with:
44+
files: ${{ env.ASSET }}
45+
generate_release_notes: true

cmd/main.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import (
2424
"github.com/hiddify/hiddify_config_health/internal/web"
2525
)
2626

27+
// Version is set at build time via -ldflags "-X main.Version=...".
28+
var Version = "dev"
29+
2730
func main() {
2831
if err := rootCmd().Execute(); err != nil {
2932
fmt.Fprintln(os.Stderr, err)
@@ -39,6 +42,7 @@ var (
3942
flagJSON bool
4043
flagCore string
4144
flagDeploy string
45+
flagPort string
4246
)
4347

4448
// jsonCheck is the CI-friendly serialization of a single health check.
@@ -60,6 +64,16 @@ type jsonResult struct {
6064
Pass bool `json:"pass"`
6165
Censor string `json:"censor,omitempty"`
6266
DurationMs int64 `json:"duration_ms"`
67+
LatencyMs float64 `json:"latency_ms,omitempty"`
68+
JitterMs float64 `json:"jitter_ms,omitempty"`
69+
ProbeVerdict string `json:"probe_verdict,omitempty"`
70+
JA3 string `json:"ja3,omitempty"`
71+
JA4 string `json:"ja4,omitempty"`
72+
TLSMatch string `json:"tls_match,omitempty"`
73+
Entropy float64 `json:"entropy,omitempty"`
74+
LoadBPS float64 `json:"load_bps,omitempty"`
75+
LoadDropped int `json:"load_dropped,omitempty"`
76+
Regressed bool `json:"regressed,omitempty"`
6377
Checks []jsonCheck `json:"checks"`
6478
Error string `json:"error,omitempty"`
6579
}
@@ -86,15 +100,34 @@ func toJSONResult(dir, core string, res *runner.Result) jsonResult {
86100
if c.Err != nil {
87101
jc.Error = c.Err.Error()
88102
}
103+
if c.Name == "ping" && c.PingAvg > 0 {
104+
jr.LatencyMs = float64(c.PingAvg.Microseconds()) / 1000.0
105+
}
106+
if (c.Name == "ping" || c.Name == "jitter") && c.Jitter > 0 {
107+
jr.JitterMs = float64(c.Jitter.Microseconds()) / 1000.0
108+
}
109+
switch c.Name {
110+
case "active-probe":
111+
jr.ProbeVerdict = c.ProbeVerdict
112+
case "tls-fingerprint":
113+
jr.JA3, jr.JA4, jr.TLSMatch = c.JA3, c.JA4, c.TLSMatch
114+
case "entropy":
115+
jr.Entropy = c.EntropyScore
116+
case "load":
117+
jr.LoadBPS, jr.LoadDropped = c.Throughput, c.LoadDropped
118+
case "regression":
119+
jr.Regressed = c.Regressed
120+
}
89121
jr.Checks = append(jr.Checks, jc)
90122
}
91123
return jr
92124
}
93125

94126
func rootCmd() *cobra.Command {
95127
root := &cobra.Command{
96-
Use: "hiddify-health",
97-
Short: "Test VPN/proxy configuration files across multiple cores",
128+
Use: "hiddify-health",
129+
Short: "Test VPN/proxy configuration files across multiple cores",
130+
Version: Version,
98131
SilenceUsage: true,
99132
SilenceErrors: true,
100133
}
@@ -109,6 +142,7 @@ func rootCmd() *cobra.Command {
109142
checkCmd(),
110143
serveCmd(),
111144
historyCmd(),
145+
reportCmd(),
112146
)
113147
return root
114148
}
@@ -137,6 +171,7 @@ func runCmd() *cobra.Command {
137171
}
138172
c.Flags().BoolVar(&flagJSON, "json", false, "emit machine-readable JSON report (for CI)")
139173
c.Flags().StringVar(&flagDeploy, "deploy", "", "deploy server to this SSH URL for all examples (ssh://user:pass@host:22)")
174+
c.Flags().StringVar(&flagPort, "port", "", "pin the server PORT to a fixed value (needed for remote deploy through a firewall)")
140175
return c
141176
}
142177

@@ -153,7 +188,13 @@ func runOne(ctx context.Context, dir string, db *store.DB) ([]jsonResult, bool)
153188
}
154189

155190
core := coreOf(dir)
156-
results, err := runner.RunWithOverrides(ctx, dir, logOut, runner.Overrides{DeployToServer: flagDeploy})
191+
ov := runner.Overrides{DeployToServer: flagDeploy}
192+
if flagPort != "" {
193+
// Pin the server port to a fixed, firewall-opened value (needed for
194+
// remote deploy, where a random high port is usually blocked).
195+
ov.Vars = map[string]string{"PORT": flagPort}
196+
}
197+
results, err := runner.RunWithOverrides(ctx, dir, logOut, ov)
157198
if err != nil && len(results) == 0 {
158199
// Hard failure before any variant produced a result.
159200
if !flagJSON {
@@ -165,6 +206,13 @@ func runOne(ctx context.Context, dir string, db *store.DB) ([]jsonResult, bool)
165206
var jrs []jsonResult
166207
anyFail := false
167208
for _, res := range results {
209+
// Compare against the prior baseline BEFORE saving this run, and append
210+
// the regression verdict as an extra (warn-only) check row.
211+
if db != nil {
212+
if reg, ok := db.RegressionCheck(dir, res.Variant, res.Checks); ok {
213+
res.Checks = append(res.Checks, reg)
214+
}
215+
}
168216
if db != nil {
169217
rec := store.Record{
170218
ExampleDir: dir,
@@ -288,6 +336,7 @@ func runAllCmd() *cobra.Command {
288336
c.Flags().BoolVar(&flagJSON, "json", false, "emit machine-readable JSON report (for CI)")
289337
c.Flags().StringVar(&flagCore, "core", "", "only run examples for this core (e.g. sing-box, xray)")
290338
c.Flags().StringVar(&flagDeploy, "deploy", "", "deploy server to this SSH URL for ALL examples (ssh://user:pass@host:22)")
339+
c.Flags().StringVar(&flagPort, "port", "", "pin the server PORT to a fixed value (needed for remote deploy through a firewall)")
291340
return c
292341
}
293342

0 commit comments

Comments
 (0)