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:
yusing
2025-08-16 13:04:05 +08:00
parent fce9ce21c9
commit 35a3e3fef6
149 changed files with 13173 additions and 2173 deletions

View File

@@ -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),
}
}

View File

@@ -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,
})
}

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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())},

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
)