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 48627753d6
commit 55a42b81de
15 changed files with 66 additions and 51 deletions

View File

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

View File

@@ -14,6 +14,7 @@ import (
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
)
@@ -25,8 +26,12 @@ type (
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"`
Defaults Defaults `json:"defaults"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
}
Defaults struct {
HealthCheck types.HealthCheckConfig `json:"healthcheck"`
}
Providers struct {
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"`

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog/log"
"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/homepage"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
@@ -49,7 +50,7 @@ type (
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
Rules rules.Rules `json:"rules,omitempty" extension:"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"`
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
Homepage *homepage.ItemConfig `json:"homepage"`
@@ -497,7 +498,7 @@ func (r *Route) IdlewatcherConfig() *types.IdlewatcherConfig {
return r.Idlewatcher
}
func (r *Route) HealthCheckConfig() *types.HealthCheckConfig {
func (r *Route) HealthCheckConfig() types.HealthCheckConfig {
return r.HealthCheck
}
@@ -771,17 +772,7 @@ func (r *Route) Finalize() {
}
r.Port.Listening, r.Port.Proxy = lp, pp
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
}
r.HealthCheck.ApplyDefaults(config.ActiveConfig.Load().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {
@@ -794,7 +785,6 @@ func (r *Route) FinalizeHomepageConfig() {
if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{
Show: true,
Name: r.Alias,
}
}

View File

@@ -2,6 +2,7 @@ package route
import (
"testing"
"time"
"github.com/yusing/godoxy/internal/common"
route "github.com/yusing/godoxy/internal/route/types"
@@ -40,7 +41,7 @@ func TestRouteValidate(t *testing.T) {
Scheme: route.SchemeHTTP,
Host: "example.com",
Port: route.Port{Proxy: 80},
HealthCheck: &types.HealthCheckConfig{
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
LoadBalance: &types.LoadBalancerConfig{
@@ -179,3 +180,14 @@ func TestRouteAgent(t *testing.T) {
expect.NoError(t, err, "Validate should not return error for valid route with 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 (
"context"
"time"
"github.com/yusing/godoxy/internal/common"
)
type HealthCheckConfig struct {
Disable bool `json:"disable,omitempty" aliases:"disabled"`
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
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"`
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:"-"`
} // @name HealthCheckConfig
func DefaultHealthConfig() *HealthCheckConfig {
return &HealthCheckConfig{
Interval: common.HealthCheckIntervalDefault,
Timeout: common.HealthCheckTimeoutDefault,
Retries: int64(common.HealthCheckDownNotifyDelayDefault / common.HealthCheckIntervalDefault),
const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
HealthCheckDownNotifyDelayDefault = 15 * time.Second
)
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{}
IdlewatcherConfig() *IdlewatcherConfig
HealthCheckConfig() *HealthCheckConfig
HealthCheckConfig() HealthCheckConfig
LoadBalanceConfig() *LoadBalancerConfig
HomepageItem() homepage.Item
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{
agent: agent,
}

View File

@@ -26,7 +26,7 @@ type DockerHealthMonitor struct {
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.client = client
mon.containerID = containerID

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import (
"github.com/rs/zerolog"
"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/notif"
"github.com/yusing/godoxy/internal/types"
@@ -24,7 +24,7 @@ type (
HealthCheckFunc func() (result types.HealthCheckResult, err error)
monitor struct {
service string
config *types.HealthCheckConfig
config types.HealthCheckConfig
url synk.Value[*url.URL]
status synk.Value[types.HealthStatus]
@@ -73,6 +73,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
return mon
}
func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
cfg.ApplyDefaults(config.DefaultConfig().Defaults.HealthCheck)
mon := &monitor{
config: cfg,
@@ -196,7 +197,7 @@ func (mon *monitor) URL() *url.URL {
// Config implements HealthChecker.
func (mon *monitor) Config() *types.HealthCheckConfig {
return mon.config
return &mon.config
}
// Status implements HealthMonitor.
@@ -237,7 +238,7 @@ func (mon *monitor) MarshalJSON() ([]byte, error) {
res := mon.lastResult.Load()
return (&types.HealthJSONRepr{
Name: mon.service,
Config: mon.config,
Config: &mon.config,
Status: mon.status.Load(),
Started: mon.startTime,
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
func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) {
func createTestMonitor(config types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) {
testURL, _ := url.Parse("http://localhost:8080")
mon := newMonitor(testURL, config, checkFunc)
@@ -56,7 +56,7 @@ func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFun
}
func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: -1, // Immediate notification
@@ -91,7 +91,7 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
}
func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 50 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures
@@ -130,7 +130,7 @@ func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
}
func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 3, // Notify after 3 consecutive failures
@@ -179,7 +179,7 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
}
func TestNotification_ConsecutiveFailureReset(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures
@@ -240,7 +240,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
}
func TestNotification_ContextCancellation(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 1,
@@ -279,7 +279,7 @@ func TestNotification_ContextCancellation(t *testing.T) {
}
func TestImmediateUpNotification(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
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.monitor = newMonitor(url, config, mon.CheckHealth)
mon.dialer = &net.Dialer{