lolly/internal/proxy/health_match.go
xfy 00c5319819 refactor(proxy,server): 改进健康检查 goroutine 退出机制
- health: 使用 WaitGroup 确保 run goroutine 完全退出后再继续
- health_match: 忽略未使用参数,预分配 slice 容量
- proxy_coverage_extra_test: 使用 atomic.Int32 修复测试竞态条件
- purge: 忽略 Delete 返回值

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 13:35:03 +08:00

197 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package proxy 提供 HTTP 代理功能。
//
// 该文件实现 HealthMatch 健康检查匹配接口,支持:
// - 默认 2xx 状态码判断
// - 自定义状态码范围匹配
// - 响应体正则匹配
// - 响应头匹配
//
// 主要用途:
//
// 灵活定义后端服务器健康判断逻辑,替代硬编码的 2xx 判断。
//
// 作者xfy
package proxy
import (
"regexp"
"strconv"
"strings"
)
// HealthMatch 定义健康检查匹配接口。
//
// 用于判断健康检查响应是否表示目标健康。
type HealthMatch interface {
// Match 判断健康检查响应是否表示目标健康。
//
// 参数:
// - status: HTTP 状态码
// - body: 响应体内容
// - headers: 响应头key 为小写)
//
// 返回值:
// - true: 目标健康
// - false: 目标不健康
Match(status int, body []byte, headers map[string]string) bool
}
// defaultHealthMatch 默认健康检查匹配器。
//
// 判断逻辑:状态码为 2xx 即健康。
type defaultHealthMatch struct{}
// Match 实现 HealthMatch 接口。
func (m *defaultHealthMatch) Match(status int, _ []byte, _ map[string]string) bool {
return status >= 200 && status < 300
}
// customHealthMatch 自定义健康检查匹配器。
//
// 支持状态码范围、响应体正则、响应头匹配。
type customHealthMatch struct {
statusRanges []statusRange // 状态码范围列表
bodyRegex *regexp.Regexp // 响应体正则(可选)
headerMatches []headerMatch // 响应头匹配列表(可选)
}
// statusRange 表示状态码范围。
type statusRange struct {
min int
max int
}
// headerMatch 表示响应头匹配条件。
type headerMatch struct {
key string
value string
}
// Match 实现 HealthMatch 接口。
func (m *customHealthMatch) Match(status int, body []byte, headers map[string]string) bool {
// 1. 检查状态码
if !m.matchStatus(status) {
return false
}
// 2. 检查响应体正则(如果配置)
if m.bodyRegex != nil && !m.bodyRegex.Match(body) {
return false
}
// 3. 检查响应头(如果配置)
for _, hm := range m.headerMatches {
value, exists := headers[hm.key]
if !exists || value != hm.value {
return false
}
}
return true
}
// matchStatus 检查状态码是否匹配任一范围。
func (m *customHealthMatch) matchStatus(status int) bool {
for _, r := range m.statusRanges {
if status >= r.min && status <= r.max {
return true
}
}
return false
}
// HealthMatchConfig 健康检查匹配配置。
type HealthMatchConfig struct {
// Status 状态码范围列表,如 ["200-299", "301"]
Status []string `yaml:"status"`
// Body 响应体正则表达式
Body string `yaml:"body"`
// Headers 响应头匹配,如 {"Content-Type": "application/json"}
Headers map[string]string `yaml:"headers"`
}
// NewHealthMatch 从配置创建健康检查匹配器。
//
// 如果配置为空或无效返回默认匹配器2xx 判断)。
func NewHealthMatch(cfg *HealthMatchConfig) HealthMatch {
if cfg == nil {
return &defaultHealthMatch{}
}
// 解析状态码范围
ranges := make([]statusRange, 0, len(cfg.Status))
for _, s := range cfg.Status {
r, err := parseStatusRange(s)
if err != nil {
continue // 忽略无效范围
}
ranges = append(ranges, r)
}
// 如果没有有效状态码范围,使用默认 2xx
if len(ranges) == 0 {
ranges = []statusRange{{min: 200, max: 299}}
}
// 解析响应体正则
var bodyRegex *regexp.Regexp
if cfg.Body != "" {
bodyRegex = regexp.MustCompile(cfg.Body) // 配置加载时预编译
}
// 解析响应头匹配
headerMatches := make([]headerMatch, 0, len(cfg.Headers))
for k, v := range cfg.Headers {
headerMatches = append(headerMatches, headerMatch{
key: strings.ToLower(k), // 统一小写
value: v,
})
}
return &customHealthMatch{
statusRanges: ranges,
bodyRegex: bodyRegex,
headerMatches: headerMatches,
}
}
// parseStatusRange 解析状态码范围字符串。
//
// 支持格式:
// - "200" → 单个状态码
// - "200-299" → 范围
func parseStatusRange(s string) (statusRange, error) {
s = strings.TrimSpace(s)
// 尝试解析范围
if strings.Contains(s, "-") {
parts := strings.Split(s, "-")
if len(parts) != 2 {
return statusRange{}, strconv.ErrSyntax
}
min, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
max, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
if err1 != nil || err2 != nil {
return statusRange{}, strconv.ErrSyntax
}
return statusRange{min: min, max: max}, nil
}
// 单个状态码
code, err := strconv.Atoi(s)
if err != nil {
return statusRange{}, err
}
return statusRange{min: code, max: code}, nil
}
// DefaultHealthMatch 返回默认健康检查匹配器。
func DefaultHealthMatch() HealthMatch {
return &defaultHealthMatch{}
}