mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-19 15:31:37 +02:00
refactor(api): restructured API for type safety, maintainability and docs generation
- These changes makes the API incombatible with previous versions - Added new types for error handling, success responses, and health checks. - Updated health check logic to utilize the new types for better clarity and structure. - Refactored existing handlers to improve response consistency and error handling. - Updated Makefile to include a new target for generating API types from Swagger. - Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/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"`
|
||||
Interval time.Duration `json:"interval" validate:"omitempty,min=1s"`
|
||||
Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s"`
|
||||
Retries int64 `json:"retries"` // <0: immediate, >=0: threshold
|
||||
|
||||
BaseContext func() context.Context `json:"-"`
|
||||
}
|
||||
|
||||
func DefaultHealthConfig() *HealthCheckConfig {
|
||||
return &HealthCheckConfig{
|
||||
Interval: common.HealthCheckIntervalDefault,
|
||||
Timeout: common.HealthCheckTimeoutDefault,
|
||||
Retries: int64(common.HealthCheckDownNotifyDelayDefault / common.HealthCheckIntervalDefault),
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type JSONRepresentation struct {
|
||||
Name string
|
||||
Config *HealthCheckConfig
|
||||
Status Status
|
||||
Started time.Time
|
||||
Uptime time.Duration
|
||||
Latency time.Duration
|
||||
LastSeen time.Time
|
||||
Detail string
|
||||
URL *url.URL
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
func (jsonRepr *JSONRepresentation) MarshalJSON() ([]byte, error) {
|
||||
var url string
|
||||
if jsonRepr.URL != nil {
|
||||
url = jsonRepr.URL.String()
|
||||
}
|
||||
if url == "http://:0" {
|
||||
url = ""
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
"name": jsonRepr.Name,
|
||||
"config": jsonRepr.Config,
|
||||
"started": jsonRepr.Started.Unix(),
|
||||
"startedStr": strutils.FormatTime(jsonRepr.Started),
|
||||
"status": jsonRepr.Status.String(),
|
||||
"uptime": jsonRepr.Uptime.Seconds(),
|
||||
"uptimeStr": strutils.FormatDuration(jsonRepr.Uptime),
|
||||
"latency": jsonRepr.Latency.Seconds(),
|
||||
"latencyStr": strconv.Itoa(int(jsonRepr.Latency.Milliseconds())) + " ms",
|
||||
"lastSeen": jsonRepr.LastSeen.Unix(),
|
||||
"lastSeenStr": strutils.FormatLastSeen(jsonRepr.LastSeen),
|
||||
"detail": jsonRepr.Detail,
|
||||
"url": url,
|
||||
"extra": jsonRepr.Extra,
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -48,7 +48,7 @@ func (target *AgentCheckHealthTarget) displayURL() *url.URL {
|
||||
}
|
||||
}
|
||||
|
||||
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *health.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
|
||||
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
|
||||
mon := &AgentProxiedMonitor{
|
||||
agent: agent,
|
||||
endpointURL: agentPkg.EndpointHealth + "?" + target.buildQuery(),
|
||||
@@ -57,9 +57,9 @@ func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *health.HealthCh
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *AgentProxiedMonitor) CheckHealth() (result *health.HealthCheckResult, err error) {
|
||||
func (mon *AgentProxiedMonitor) CheckHealth() (result *types.HealthCheckResult, err error) {
|
||||
startTime := time.Now()
|
||||
result = new(health.HealthCheckResult)
|
||||
result = new(types.HealthCheckResult)
|
||||
ctx, cancel := mon.ContextWithTimeout("timeout querying agent")
|
||||
defer cancel()
|
||||
data, status, err := mon.agent.Fetch(ctx, mon.endpointURL)
|
||||
|
||||
@@ -3,18 +3,17 @@ package monitor
|
||||
import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type DockerHealthMonitor struct {
|
||||
*monitor
|
||||
client *docker.SharedClient
|
||||
containerID string
|
||||
fallback health.HealthChecker
|
||||
fallback types.HealthChecker
|
||||
}
|
||||
|
||||
func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias string, config *health.HealthCheckConfig, fallback health.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
|
||||
@@ -24,7 +23,7 @@ func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias stri
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *DockerHealthMonitor) CheckHealth() (result *health.HealthCheckResult, err error) {
|
||||
func (mon *DockerHealthMonitor) CheckHealth() (result *types.HealthCheckResult, err error) {
|
||||
ctx, cancel := mon.ContextWithTimeout("docker health check timed out")
|
||||
defer cancel()
|
||||
cont, err := mon.client.ContainerInspect(ctx, mon.containerID)
|
||||
@@ -34,12 +33,12 @@ func (mon *DockerHealthMonitor) CheckHealth() (result *health.HealthCheckResult,
|
||||
status := cont.State.Status
|
||||
switch status {
|
||||
case "dead", "exited", "paused", "restarting", "removing":
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Healthy: false,
|
||||
Detail: "container is " + status,
|
||||
}, nil
|
||||
case "created":
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Healthy: false,
|
||||
Detail: "container is not started",
|
||||
}, nil
|
||||
@@ -47,7 +46,7 @@ func (mon *DockerHealthMonitor) CheckHealth() (result *health.HealthCheckResult,
|
||||
if cont.State.Health == nil {
|
||||
return mon.fallback.CheckHealth()
|
||||
}
|
||||
result = new(health.HealthCheckResult)
|
||||
result = new(types.HealthCheckResult)
|
||||
result.Healthy = cont.State.Health.Status == container.Healthy
|
||||
if len(cont.State.Health.Log) > 0 {
|
||||
lastLog := cont.State.Health.Log[len(cont.State.Health.Log)-1]
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type FileServerHealthMonitor struct {
|
||||
@@ -12,24 +12,24 @@ type FileServerHealthMonitor struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewFileServerHealthMonitor(config *health.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
|
||||
}
|
||||
|
||||
func (s *FileServerHealthMonitor) CheckHealth() (*health.HealthCheckResult, error) {
|
||||
func (s *FileServerHealthMonitor) CheckHealth() (*types.HealthCheckResult, error) {
|
||||
start := time.Now()
|
||||
_, err := os.Stat(s.path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Detail: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Healthy: true,
|
||||
Latency: time.Since(start),
|
||||
}, nil
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ var pinger = &http.Client{
|
||||
},
|
||||
}
|
||||
|
||||
func NewHTTPHealthMonitor(url *url.URL, config *health.HealthCheckConfig) *HTTPHealthMonitor {
|
||||
func NewHTTPHealthMonitor(url *url.URL, config *types.HealthCheckConfig) *HTTPHealthMonitor {
|
||||
mon := new(HTTPHealthMonitor)
|
||||
mon.monitor = newMonitor(url, config, mon.CheckHealth)
|
||||
if config.UseGet {
|
||||
@@ -37,7 +37,7 @@ func NewHTTPHealthMonitor(url *url.URL, config *health.HealthCheckConfig) *HTTPH
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *HTTPHealthMonitor) CheckHealth() (*health.HealthCheckResult, error) {
|
||||
func (mon *HTTPHealthMonitor) CheckHealth() (*types.HealthCheckResult, error) {
|
||||
ctx, cancel := mon.ContextWithTimeout("ping request timed out")
|
||||
defer cancel()
|
||||
|
||||
@@ -67,19 +67,19 @@ func (mon *HTTPHealthMonitor) CheckHealth() (*health.HealthCheckResult, error) {
|
||||
// treat tls error as healthy
|
||||
var tlsErr *tls.CertificateVerificationError
|
||||
if ok := errors.As(respErr, &tlsErr); !ok {
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Detail: respErr.Error(),
|
||||
}, nil
|
||||
}
|
||||
case resp.StatusCode == http.StatusServiceUnavailable:
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Detail: resp.Status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Healthy: true,
|
||||
}, nil
|
||||
|
||||
@@ -13,22 +13,21 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
"github.com/yusing/go-proxy/internal/utils/atomic"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type (
|
||||
HealthCheckFunc func() (result *health.HealthCheckResult, err error)
|
||||
HealthCheckFunc func() (result *types.HealthCheckResult, err error)
|
||||
monitor struct {
|
||||
service string
|
||||
config *health.HealthCheckConfig
|
||||
config *types.HealthCheckConfig
|
||||
url atomic.Value[*url.URL]
|
||||
|
||||
status atomic.Value[health.Status]
|
||||
lastResult atomic.Value[*health.HealthCheckResult]
|
||||
status atomic.Value[types.HealthStatus]
|
||||
lastResult atomic.Value[*types.HealthCheckResult]
|
||||
|
||||
checkHealth HealthCheckFunc
|
||||
startTime time.Time
|
||||
@@ -45,15 +44,15 @@ type (
|
||||
|
||||
var ErrNegativeInterval = gperr.New("negative interval")
|
||||
|
||||
func NewMonitor(r routes.Route) health.HealthMonCheck {
|
||||
var mon health.HealthMonCheck
|
||||
func NewMonitor(r types.Route) types.HealthMonCheck {
|
||||
var mon types.HealthMonCheck
|
||||
if r.IsAgent() {
|
||||
mon = NewAgentProxiedMonitor(r.GetAgent(), r.HealthCheckConfig(), AgentTargetFromURL(&r.TargetURL().URL))
|
||||
} else {
|
||||
switch r := r.(type) {
|
||||
case routes.HTTPRoute:
|
||||
case types.HTTPRoute:
|
||||
mon = NewHTTPHealthMonitor(&r.TargetURL().URL, r.HealthCheckConfig())
|
||||
case routes.StreamRoute:
|
||||
case types.StreamRoute:
|
||||
mon = NewRawHealthMonitor(&r.TargetURL().URL, r.HealthCheckConfig())
|
||||
default:
|
||||
log.Panic().Msgf("unexpected route type: %T", r)
|
||||
@@ -71,7 +70,7 @@ func NewMonitor(r routes.Route) health.HealthMonCheck {
|
||||
return mon
|
||||
}
|
||||
|
||||
func newMonitor(u *url.URL, config *health.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
|
||||
func newMonitor(u *url.URL, config *types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
|
||||
if config.Retries == 0 {
|
||||
config.Retries = int64(common.HealthCheckDownNotifyDelayDefault / config.Interval)
|
||||
}
|
||||
@@ -85,13 +84,13 @@ func newMonitor(u *url.URL, config *health.HealthCheckConfig, healthCheckFunc He
|
||||
u = &url.URL{}
|
||||
}
|
||||
mon.url.Store(u)
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
|
||||
port := u.Port()
|
||||
mon.isZeroPort = port == "" || port == "0"
|
||||
if mon.isZeroPort {
|
||||
mon.status.Store(health.StatusUnknown)
|
||||
mon.lastResult.Store(&health.HealthCheckResult{Healthy: false, Detail: "no port detected"})
|
||||
mon.status.Store(types.StatusUnknown)
|
||||
mon.lastResult.Store(&types.HealthCheckResult{Healthy: false, Detail: "no port detected"})
|
||||
}
|
||||
return mon
|
||||
}
|
||||
@@ -125,8 +124,8 @@ func (mon *monitor) Start(parent task.Parent) gperr.Error {
|
||||
logger := log.With().Str("name", mon.service).Logger()
|
||||
|
||||
defer func() {
|
||||
if mon.status.Load() != health.StatusError {
|
||||
mon.status.Store(health.StatusUnhealthy)
|
||||
if mon.status.Load() != types.StatusError {
|
||||
mon.status.Store(types.StatusUnhealthy)
|
||||
}
|
||||
mon.task.Finish(nil)
|
||||
}()
|
||||
@@ -154,7 +153,7 @@ func (mon *monitor) Start(parent task.Parent) gperr.Error {
|
||||
failures = 0
|
||||
}
|
||||
if failures >= 5 {
|
||||
mon.status.Store(health.StatusError)
|
||||
mon.status.Store(types.StatusError)
|
||||
mon.task.Finish(err)
|
||||
logger.Error().Msg("healthchecker stopped after 5 trials")
|
||||
return
|
||||
@@ -186,12 +185,12 @@ func (mon *monitor) URL() *url.URL {
|
||||
}
|
||||
|
||||
// Config implements HealthChecker.
|
||||
func (mon *monitor) Config() *health.HealthCheckConfig {
|
||||
func (mon *monitor) Config() *types.HealthCheckConfig {
|
||||
return mon.config
|
||||
}
|
||||
|
||||
// Status implements HealthMonitor.
|
||||
func (mon *monitor) Status() health.Status {
|
||||
func (mon *monitor) Status() types.HealthStatus {
|
||||
return mon.status.Load()
|
||||
}
|
||||
|
||||
@@ -229,7 +228,7 @@ func (mon *monitor) String() string {
|
||||
return mon.Name()
|
||||
}
|
||||
|
||||
var resHealthy = health.HealthCheckResult{Healthy: true}
|
||||
var resHealthy = types.HealthCheckResult{Healthy: true}
|
||||
|
||||
// MarshalJSON implements health.HealthMonitor.
|
||||
func (mon *monitor) MarshalJSON() ([]byte, error) {
|
||||
@@ -238,7 +237,7 @@ func (mon *monitor) MarshalJSON() ([]byte, error) {
|
||||
res = &resHealthy
|
||||
}
|
||||
|
||||
return (&health.JSONRepresentation{
|
||||
return (&types.HealthJSONRepr{
|
||||
Name: mon.service,
|
||||
Config: mon.config,
|
||||
Status: mon.status.Load(),
|
||||
@@ -255,21 +254,21 @@ func (mon *monitor) checkUpdateHealth() error {
|
||||
logger := log.With().Str("name", mon.Name()).Logger()
|
||||
result, err := mon.checkHealth()
|
||||
|
||||
var lastStatus health.Status
|
||||
var lastStatus types.HealthStatus
|
||||
switch {
|
||||
case err != nil:
|
||||
result = &health.HealthCheckResult{Healthy: false, Detail: err.Error()}
|
||||
lastStatus = mon.status.Swap(health.StatusError)
|
||||
result = &types.HealthCheckResult{Healthy: false, Detail: err.Error()}
|
||||
lastStatus = mon.status.Swap(types.StatusError)
|
||||
case result.Healthy:
|
||||
lastStatus = mon.status.Swap(health.StatusHealthy)
|
||||
lastStatus = mon.status.Swap(types.StatusHealthy)
|
||||
UpdateLastSeen(mon.service)
|
||||
default:
|
||||
lastStatus = mon.status.Swap(health.StatusUnhealthy)
|
||||
lastStatus = mon.status.Swap(types.StatusUnhealthy)
|
||||
}
|
||||
mon.lastResult.Store(result)
|
||||
|
||||
// change of status
|
||||
if result.Healthy != (lastStatus == health.StatusHealthy) {
|
||||
if result.Healthy != (lastStatus == types.StatusHealthy) {
|
||||
if result.Healthy {
|
||||
mon.notifyServiceUp(&logger, result)
|
||||
mon.numConsecFailures.Store(0)
|
||||
@@ -293,7 +292,7 @@ func (mon *monitor) checkUpdateHealth() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (mon *monitor) notifyServiceUp(logger *zerolog.Logger, result *health.HealthCheckResult) {
|
||||
func (mon *monitor) notifyServiceUp(logger *zerolog.Logger, result *types.HealthCheckResult) {
|
||||
logger.Info().Msg("service is up")
|
||||
extras := mon.buildNotificationExtras(result)
|
||||
extras.Add("Ping", fmt.Sprintf("%d ms", result.Latency.Milliseconds()))
|
||||
@@ -305,7 +304,7 @@ func (mon *monitor) notifyServiceUp(logger *zerolog.Logger, result *health.Healt
|
||||
})
|
||||
}
|
||||
|
||||
func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *health.HealthCheckResult) {
|
||||
func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *types.HealthCheckResult) {
|
||||
logger.Warn().Msg("service went down")
|
||||
extras := mon.buildNotificationExtras(result)
|
||||
extras.Add("Last Seen", strutils.FormatLastSeen(GetLastSeen(mon.service)))
|
||||
@@ -317,7 +316,7 @@ func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *health.Hea
|
||||
})
|
||||
}
|
||||
|
||||
func (mon *monitor) buildNotificationExtras(result *health.HealthCheckResult) notif.FieldsBody {
|
||||
func (mon *monitor) buildNotificationExtras(result *types.HealthCheckResult) notif.FieldsBody {
|
||||
extras := notif.FieldsBody{
|
||||
{Name: "Service Name", Value: mon.service},
|
||||
{Name: "Time", Value: strutils.FormatTime(time.Now())},
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
// Test notification tracker
|
||||
@@ -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 *health.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,14 +56,14 @@ func createTestMonitor(config *health.HealthCheckConfig, checkFunc HealthCheckFu
|
||||
}
|
||||
|
||||
func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: -1, // Immediate notification
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
})
|
||||
|
||||
// Start with healthy service
|
||||
@@ -72,8 +72,8 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
|
||||
require.True(t, result.Healthy)
|
||||
|
||||
// Set to unhealthy
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// Simulate status change detection
|
||||
@@ -81,7 +81,7 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// With NotifyAfter=0, notification should happen immediately
|
||||
require.Equal(t, health.StatusUnhealthy, mon.Status())
|
||||
require.Equal(t, types.StatusUnhealthy, mon.Status())
|
||||
|
||||
// Check notification counts - should have 1 down notification
|
||||
up, down, last := tracker.getStats()
|
||||
@@ -91,22 +91,22 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 50 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 2, // Notify after 2 consecutive failures
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
})
|
||||
|
||||
// Start healthy
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
|
||||
// Set to unhealthy
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// First failure - should not notify yet
|
||||
@@ -130,22 +130,22 @@ func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 3, // Notify after 3 consecutive failures
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
})
|
||||
|
||||
// Start healthy
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
|
||||
// Set to unhealthy
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// First failure
|
||||
@@ -162,8 +162,8 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
|
||||
require.Equal(t, 0, up)
|
||||
|
||||
// Service recovers before third failure
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
}
|
||||
|
||||
// Health check with recovery
|
||||
@@ -179,22 +179,22 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 2, // Notify after 2 consecutive failures
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
})
|
||||
|
||||
// Start healthy
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
|
||||
// Set to unhealthy
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// First failure
|
||||
@@ -202,8 +202,8 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Recover briefly
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
}
|
||||
|
||||
err = mon.checkUpdateHealth()
|
||||
@@ -215,8 +215,8 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
require.Equal(t, 1, up)
|
||||
|
||||
// Go down again - consecutive counter should start from 0
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// First failure after recovery
|
||||
@@ -240,14 +240,14 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNotification_ContextCancellation(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 1,
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true}, nil
|
||||
})
|
||||
|
||||
// Create a task that we can cancel
|
||||
@@ -255,9 +255,9 @@ func TestNotification_ContextCancellation(t *testing.T) {
|
||||
mon.task = rootTask.Subtask("monitor", true)
|
||||
|
||||
// Start healthy, then go unhealthy
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
// Trigger notification
|
||||
@@ -279,22 +279,22 @@ func TestNotification_ContextCancellation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImmediateUpNotification(t *testing.T) {
|
||||
config := &health.HealthCheckConfig{
|
||||
config := &types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 2, // NotifyAfter should not affect up notifications
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: false}, nil
|
||||
mon, tracker := createTestMonitor(config, func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: false}, nil
|
||||
})
|
||||
|
||||
// Start unhealthy
|
||||
mon.status.Store(health.StatusUnhealthy)
|
||||
mon.status.Store(types.StatusUnhealthy)
|
||||
|
||||
// Set to healthy
|
||||
mon.checkHealth = func() (*health.HealthCheckResult, error) {
|
||||
return &health.HealthCheckResult{Healthy: true, Latency: 50 * time.Millisecond}, nil
|
||||
mon.checkHealth = func() (*types.HealthCheckResult, error) {
|
||||
return &types.HealthCheckResult{Healthy: true, Latency: 50 * time.Millisecond}, nil
|
||||
}
|
||||
|
||||
// Trigger health check
|
||||
@@ -302,7 +302,7 @@ func TestImmediateUpNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Up notification should happen immediately regardless of NotifyAfter setting
|
||||
require.Equal(t, health.StatusHealthy, mon.Status())
|
||||
require.Equal(t, types.StatusHealthy, mon.Status())
|
||||
|
||||
// Should have exactly 1 up notification immediately
|
||||
up, down, last := tracker.getStats()
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -15,7 +15,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func NewRawHealthMonitor(url *url.URL, config *health.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{
|
||||
@@ -25,7 +25,7 @@ func NewRawHealthMonitor(url *url.URL, config *health.HealthCheckConfig) *RawHea
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *RawHealthMonitor) CheckHealth() (*health.HealthCheckResult, error) {
|
||||
func (mon *RawHealthMonitor) CheckHealth() (*types.HealthCheckResult, error) {
|
||||
ctx, cancel := mon.ContextWithTimeout("ping request timed out")
|
||||
defer cancel()
|
||||
|
||||
@@ -36,7 +36,7 @@ func (mon *RawHealthMonitor) CheckHealth() (*health.HealthCheckResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
return &health.HealthCheckResult{
|
||||
return &types.HealthCheckResult{
|
||||
Latency: time.Since(start),
|
||||
Healthy: true,
|
||||
}, nil
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package health
|
||||
|
||||
type Status uint8
|
||||
|
||||
const (
|
||||
StatusUnknown Status = 0
|
||||
StatusHealthy Status = (1 << iota)
|
||||
StatusNapping
|
||||
StatusStarting
|
||||
StatusUnhealthy
|
||||
StatusError
|
||||
|
||||
NumStatuses int = iota - 1
|
||||
|
||||
HealthyMask = StatusHealthy | StatusNapping | StatusStarting
|
||||
IdlingMask = StatusNapping | StatusStarting
|
||||
)
|
||||
|
||||
func NewStatus(s string) Status {
|
||||
switch s {
|
||||
case "healthy":
|
||||
return StatusHealthy
|
||||
case "unhealthy":
|
||||
return StatusUnhealthy
|
||||
case "napping":
|
||||
return StatusNapping
|
||||
case "starting":
|
||||
return StatusStarting
|
||||
case "error":
|
||||
return StatusError
|
||||
default:
|
||||
return StatusUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case StatusHealthy:
|
||||
return "healthy"
|
||||
case StatusUnhealthy:
|
||||
return "unhealthy"
|
||||
case StatusNapping:
|
||||
return "napping"
|
||||
case StatusStarting:
|
||||
return "starting"
|
||||
case StatusError:
|
||||
return "error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) Good() bool {
|
||||
return s&HealthyMask != 0
|
||||
}
|
||||
|
||||
func (s Status) Bad() bool {
|
||||
return s&HealthyMask == 0
|
||||
}
|
||||
|
||||
func (s Status) Idling() bool {
|
||||
return s&IdlingMask != 0
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
type (
|
||||
HealthCheckResult struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Detail string `json:"detail"`
|
||||
Latency time.Duration `json:"latency"`
|
||||
}
|
||||
WithHealthInfo interface {
|
||||
Status() Status
|
||||
Uptime() time.Duration
|
||||
Latency() time.Duration
|
||||
Detail() string
|
||||
}
|
||||
HealthMonitor interface {
|
||||
task.TaskStarter
|
||||
task.TaskFinisher
|
||||
fmt.Stringer
|
||||
WithHealthInfo
|
||||
Name() string
|
||||
json.Marshaler
|
||||
}
|
||||
HealthChecker interface {
|
||||
CheckHealth() (result *HealthCheckResult, err error)
|
||||
URL() *url.URL
|
||||
Config() *HealthCheckConfig
|
||||
UpdateURL(url *url.URL)
|
||||
}
|
||||
HealthMonCheck interface {
|
||||
HealthMonitor
|
||||
HealthChecker
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user