refactor(healthcheck): streamline health check configuration and defaults

- Moved health check constants from common package alongside type definition.
- Updated health check configuration to use struct directly instead of pointers.
- Introduced global default health check config
This commit is contained in:
yusing
2025-12-04 15:19:10 +08:00
parent 100eac1f3c
commit 65ee6d40bd
15 changed files with 66 additions and 51 deletions

View File

@@ -1,9 +1,5 @@
package common package common
import (
"time"
)
// file, folder structure // file, folder structure
const ( const (
@@ -38,10 +34,6 @@ var RequiredDirectories = []string{
const DockerHostFromEnv = "$DOCKER_HOST" const DockerHostFromEnv = "$DOCKER_HOST"
const ( const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
HealthCheckDownNotifyDelayDefault = 15 * time.Second
WakeTimeoutDefault = "3m" WakeTimeoutDefault = "3m"
StopTimeoutDefault = "3m" StopTimeoutDefault = "3m"
StopMethodDefault = "stop" StopMethodDefault = "stop"

View File

@@ -14,6 +14,7 @@ import (
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox" "github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
) )
@@ -25,8 +26,12 @@ type (
Providers Providers `json:"providers"` Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"` MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"` Homepage homepage.Config `json:"homepage"`
Defaults Defaults `json:"defaults"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"` TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
} }
Defaults struct {
HealthCheck types.HealthCheckConfig `json:"healthcheck"`
}
Providers struct { Providers struct {
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"` Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"` Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`

View File

@@ -82,7 +82,7 @@ func BenchmarkEntrypointReal(b *testing.B) {
Scheme: routeTypes.SchemeHTTP, Scheme: routeTypes.SchemeHTTP,
Host: host, Host: host,
Port: route.Port{Proxy: portInt}, Port: route.Port{Proxy: portInt},
HealthCheck: &types.HealthCheckConfig{Disable: true}, HealthCheck: types.HealthCheckConfig{Disable: true},
} }
err = r.Validate() err = r.Validate()
@@ -125,7 +125,7 @@ func BenchmarkEntrypoint(b *testing.B) {
Port: route.Port{ Port: route.Port{
Proxy: 8080, Proxy: 8080,
}, },
HealthCheck: &types.HealthCheckConfig{ HealthCheck: types.HealthCheckConfig{
Disable: true, Disable: true,
}, },
} }

View File

@@ -9,6 +9,7 @@ import (
gphttp "github.com/yusing/godoxy/internal/net/gphttp" gphttp "github.com/yusing/godoxy/internal/net/gphttp"
"github.com/yusing/godoxy/internal/net/gphttp/middleware" "github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/routes" "github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor" "github.com/yusing/godoxy/internal/watcher/health/monitor"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"

View File

@@ -16,6 +16,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/agent"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types" homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
@@ -50,7 +51,7 @@ type (
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"` PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"` Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"`
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"` RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
HealthCheck *types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"` LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"` Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
Homepage *homepage.ItemConfig `json:"homepage"` Homepage *homepage.ItemConfig `json:"homepage"`
@@ -498,7 +499,7 @@ func (r *Route) IdlewatcherConfig() *types.IdlewatcherConfig {
return r.Idlewatcher return r.Idlewatcher
} }
func (r *Route) HealthCheckConfig() *types.HealthCheckConfig { func (r *Route) HealthCheckConfig() types.HealthCheckConfig {
return r.HealthCheck return r.HealthCheck
} }
@@ -772,17 +773,7 @@ func (r *Route) Finalize() {
} }
r.Port.Listening, r.Port.Proxy = lp, pp r.Port.Listening, r.Port.Proxy = lp, pp
r.HealthCheck.ApplyDefaults(config.ActiveConfig.Load().Defaults.HealthCheck)
if r.HealthCheck == nil {
r.HealthCheck = types.DefaultHealthConfig()
}
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
} }
func (r *Route) FinalizeHomepageConfig() { func (r *Route) FinalizeHomepageConfig() {
@@ -795,7 +786,6 @@ func (r *Route) FinalizeHomepageConfig() {
if r.Homepage == nil { if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{ r.Homepage = &homepage.ItemConfig{
Show: true, Show: true,
Name: r.Alias,
} }
} }

View File

@@ -2,6 +2,7 @@ package route
import ( import (
"testing" "testing"
"time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
@@ -41,7 +42,7 @@ func TestRouteValidate(t *testing.T) {
Scheme: route.SchemeHTTP, Scheme: route.SchemeHTTP,
Host: "example.com", Host: "example.com",
Port: route.Port{Proxy: 80}, Port: route.Port{Proxy: 80},
HealthCheck: &types.HealthCheckConfig{ HealthCheck: types.HealthCheckConfig{
Disable: true, Disable: true,
}, },
LoadBalance: &types.LoadBalancerConfig{ LoadBalance: &types.LoadBalancerConfig{
@@ -180,3 +181,14 @@ func TestRouteAgent(t *testing.T) {
expect.NoError(t, err, "Validate should not return error for valid route with agent") expect.NoError(t, err, "Validate should not return error for valid route with agent")
expect.NotNil(t, r.GetAgent(), "GetAgent should return agent") expect.NotNil(t, r.GetAgent(), "GetAgent should return agent")
} }
func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
hc := types.HealthCheckConfig{}
hc.ApplyDefaults(types.HealthCheckConfig{
Interval: 15 * time.Second,
Timeout: 10 * time.Second,
})
expect.Equal(t, hc.Interval, 15*time.Second)
expect.Equal(t, hc.Timeout, 10*time.Second)
}

View File

@@ -3,25 +3,39 @@ package types
import ( import (
"context" "context"
"time" "time"
"github.com/yusing/godoxy/internal/common"
) )
type HealthCheckConfig struct { type HealthCheckConfig struct {
Disable bool `json:"disable,omitempty" aliases:"disabled"` Disable bool `json:"disable,omitempty" aliases:"disabled"`
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
UseGet bool `json:"use_get,omitempty"` UseGet bool `json:"use_get,omitempty"`
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
Interval time.Duration `json:"interval" validate:"omitempty,min=1s" swaggertype:"primitive,integer"` Interval time.Duration `json:"interval" validate:"omitempty,min=1s" swaggertype:"primitive,integer"`
Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s" swaggertype:"primitive,integer"` Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s" swaggertype:"primitive,integer"`
Retries int64 `json:"retries"` // <0: immediate, >=0: threshold Retries int64 `json:"retries"` // <0: immediate, 0: default, >0: threshold
BaseContext func() context.Context `json:"-"` BaseContext func() context.Context `json:"-"`
} // @name HealthCheckConfig } // @name HealthCheckConfig
func DefaultHealthConfig() *HealthCheckConfig { const (
return &HealthCheckConfig{ HealthCheckIntervalDefault = 5 * time.Second
Interval: common.HealthCheckIntervalDefault, HealthCheckTimeoutDefault = 5 * time.Second
Timeout: common.HealthCheckTimeoutDefault, HealthCheckDownNotifyDelayDefault = 15 * time.Second
Retries: int64(common.HealthCheckDownNotifyDelayDefault / common.HealthCheckIntervalDefault), )
func (hc *HealthCheckConfig) ApplyDefaults(defaults HealthCheckConfig) {
if hc.Interval == 0 {
hc.Interval = defaults.Interval
if hc.Interval == 0 {
hc.Interval = HealthCheckIntervalDefault
}
}
if hc.Timeout == 0 {
hc.Timeout = defaults.Timeout
if hc.Timeout == 0 {
hc.Timeout = HealthCheckTimeoutDefault
}
}
if hc.Retries == 0 {
hc.Retries = int64(HealthCheckDownNotifyDelayDefault / hc.Interval)
} }
} }

View File

@@ -29,7 +29,7 @@ type (
Started() <-chan struct{} Started() <-chan struct{}
IdlewatcherConfig() *IdlewatcherConfig IdlewatcherConfig() *IdlewatcherConfig
HealthCheckConfig() *HealthCheckConfig HealthCheckConfig() HealthCheckConfig
LoadBalanceConfig() *LoadBalancerConfig LoadBalanceConfig() *LoadBalancerConfig
HomepageItem() homepage.Item HomepageItem() homepage.Item
DisplayName() string DisplayName() string

View File

@@ -45,7 +45,7 @@ func (target *AgentCheckHealthTarget) displayURL() *url.URL {
} }
} }
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor { func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
mon := &AgentProxiedMonitor{ mon := &AgentProxiedMonitor{
agent: agent, agent: agent,
} }

View File

@@ -25,7 +25,7 @@ type DockerHealthMonitor struct {
const dockerFailuresThreshold = 3 const dockerFailuresThreshold = 3
func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias string, config *types.HealthCheckConfig, fallback types.HealthChecker) *DockerHealthMonitor { func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias string, config types.HealthCheckConfig, fallback types.HealthChecker) *DockerHealthMonitor {
mon := new(DockerHealthMonitor) mon := new(DockerHealthMonitor)
mon.client = client mon.client = client
mon.containerID = containerID mon.containerID = containerID

View File

@@ -12,7 +12,7 @@ type FileServerHealthMonitor struct {
path string path string
} }
func NewFileServerHealthMonitor(config *types.HealthCheckConfig, path string) *FileServerHealthMonitor { func NewFileServerHealthMonitor(config types.HealthCheckConfig, path string) *FileServerHealthMonitor {
mon := &FileServerHealthMonitor{path: path} mon := &FileServerHealthMonitor{path: path}
mon.monitor = newMonitor(nil, config, mon.CheckHealth) mon.monitor = newMonitor(nil, config, mon.CheckHealth)
return mon return mon

View File

@@ -29,7 +29,7 @@ var pinger = &fasthttp.Client{
NoDefaultUserAgentHeader: true, NoDefaultUserAgentHeader: true,
} }
func NewHTTPHealthMonitor(url *url.URL, config *types.HealthCheckConfig) *HTTPHealthMonitor { func NewHTTPHealthMonitor(url *url.URL, config types.HealthCheckConfig) *HTTPHealthMonitor {
mon := new(HTTPHealthMonitor) mon := new(HTTPHealthMonitor)
mon.monitor = newMonitor(url, config, mon.CheckHealth) mon.monitor = newMonitor(url, config, mon.CheckHealth)
if config.UseGet { if config.UseGet {

View File

@@ -10,7 +10,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common" config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
@@ -24,7 +24,7 @@ type (
HealthCheckFunc func() (result types.HealthCheckResult, err error) HealthCheckFunc func() (result types.HealthCheckResult, err error)
monitor struct { monitor struct {
service string service string
config *types.HealthCheckConfig config types.HealthCheckConfig
url synk.Value[*url.URL] url synk.Value[*url.URL]
status synk.Value[types.HealthStatus] status synk.Value[types.HealthStatus]
@@ -73,6 +73,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
return mon return mon
} }
func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
cfg.ApplyDefaults(config.DefaultConfig().Defaults.HealthCheck) cfg.ApplyDefaults(config.DefaultConfig().Defaults.HealthCheck)
mon := &monitor{ mon := &monitor{
config: cfg, config: cfg,
@@ -196,7 +197,7 @@ func (mon *monitor) URL() *url.URL {
// Config implements HealthChecker. // Config implements HealthChecker.
func (mon *monitor) Config() *types.HealthCheckConfig { func (mon *monitor) Config() *types.HealthCheckConfig {
return mon.config return &mon.config
} }
// Status implements HealthMonitor. // Status implements HealthMonitor.
@@ -237,7 +238,7 @@ func (mon *monitor) MarshalJSON() ([]byte, error) {
res := mon.lastResult.Load() res := mon.lastResult.Load()
return (&types.HealthJSONRepr{ return (&types.HealthJSONRepr{
Name: mon.service, Name: mon.service,
Config: mon.config, Config: &mon.config,
Status: mon.status.Load(), Status: mon.status.Load(),
Started: mon.startTime, Started: mon.startTime,
Uptime: mon.Uptime(), Uptime: mon.Uptime(),

View File

@@ -28,7 +28,7 @@ func (t *testNotificationTracker) getStats() (up, down int, last string) {
} }
// Create test monitor with mock health checker - returns both monitor and tracker // Create test monitor with mock health checker - returns both monitor and tracker
func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) { func createTestMonitor(config types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) {
testURL, _ := url.Parse("http://localhost:8080") testURL, _ := url.Parse("http://localhost:8080")
mon := newMonitor(testURL, config, checkFunc) mon := newMonitor(testURL, config, checkFunc)
@@ -56,7 +56,7 @@ func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFun
} }
func TestNotification_ImmediateNotifyAfterZero(t *testing.T) { func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond, Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: -1, // Immediate notification Retries: -1, // Immediate notification
@@ -91,7 +91,7 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
} }
func TestNotification_WithNotifyAfterThreshold(t *testing.T) { func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 50 * time.Millisecond, Interval: 50 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures Retries: 2, // Notify after 2 consecutive failures
@@ -130,7 +130,7 @@ func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
} }
func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) { func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond, Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: 3, // Notify after 3 consecutive failures Retries: 3, // Notify after 3 consecutive failures
@@ -179,7 +179,7 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
} }
func TestNotification_ConsecutiveFailureReset(t *testing.T) { func TestNotification_ConsecutiveFailureReset(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond, Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures Retries: 2, // Notify after 2 consecutive failures
@@ -240,7 +240,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
} }
func TestNotification_ContextCancellation(t *testing.T) { func TestNotification_ContextCancellation(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond, Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: 1, Retries: 1,
@@ -279,7 +279,7 @@ func TestNotification_ContextCancellation(t *testing.T) {
} }
func TestImmediateUpNotification(t *testing.T) { func TestImmediateUpNotification(t *testing.T) {
config := &types.HealthCheckConfig{ config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond, Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond, Timeout: 50 * time.Millisecond,
Retries: 2, // NotifyAfter should not affect up notifications Retries: 2, // NotifyAfter should not affect up notifications

View File

@@ -17,7 +17,7 @@ type (
} }
) )
func NewRawHealthMonitor(url *url.URL, config *types.HealthCheckConfig) *RawHealthMonitor { func NewRawHealthMonitor(url *url.URL, config types.HealthCheckConfig) *RawHealthMonitor {
mon := new(RawHealthMonitor) mon := new(RawHealthMonitor)
mon.monitor = newMonitor(url, config, mon.CheckHealth) mon.monitor = newMonitor(url, config, mon.CheckHealth)
mon.dialer = &net.Dialer{ mon.dialer = &net.Dialer{