package services import ( "context" "fmt" "time" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) type HealthStatus string const ( HealthStatusHealthy HealthStatus = "healthy" HealthStatusUnhealthy HealthStatus = "unhealthy" HealthStatusDegraded HealthStatus = "degraded" ) type ComponentHealth struct { Status HealthStatus `json:"status"` Message string `json:"message,omitempty"` Latency string `json:"latency,omitempty"` CheckedAt time.Time `json:"checked_at"` } type HealthReport struct { Status HealthStatus `json:"status"` Version string `json:"version"` CheckedAt time.Time `json:"checked_at"` Components map[string]ComponentHealth `json:"components"` } type HealthService struct { db *gorm.DB redis *redis.Client version string } func NewHealthService(db *gorm.DB, redisClient *redis.Client, version string) *HealthService { if version == "" { version = "0.0.1" } return &HealthService{db: db, redis: redisClient, version: version} } func (s *HealthService) CheckHealth(ctx context.Context) *HealthReport { report := &HealthReport{Status: HealthStatusHealthy, Version: s.version, CheckedAt: time.Now(), Components: map[string]ComponentHealth{}} dbHealth := s.checkDatabase(ctx) report.Components["database"] = dbHealth if dbHealth.Status != HealthStatusHealthy { report.Status = HealthStatusUnhealthy } redisHealth := s.checkRedis(ctx) report.Components["redis"] = redisHealth if redisHealth.Status != HealthStatusHealthy && report.Status == HealthStatusHealthy { report.Status = HealthStatusDegraded } return report } func (s *HealthService) SimpleHealthCheck(ctx context.Context) (string, int) { report := s.CheckHealth(ctx) switch report.Status { case HealthStatusHealthy: return "ok", 200 case HealthStatusDegraded: return "degraded", 200 default: return "unhealthy", 503 } } func (s *HealthService) checkDatabase(ctx context.Context) ComponentHealth { start := time.Now() sqlDB, err := s.db.DB() if err != nil { return ComponentHealth{Status: HealthStatusUnhealthy, Message: fmt.Sprintf("failed to get database instance: %v", err), CheckedAt: time.Now()} } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := sqlDB.PingContext(ctx); err != nil { return ComponentHealth{Status: HealthStatusUnhealthy, Message: fmt.Sprintf("database ping failed: %v", err), Latency: time.Since(start).String(), CheckedAt: time.Now()} } var result int if err := s.db.WithContext(ctx).Raw("SELECT 1").Scan(&result).Error; err != nil { return ComponentHealth{Status: HealthStatusUnhealthy, Message: fmt.Sprintf("database query failed: %v", err), Latency: time.Since(start).String(), CheckedAt: time.Now()} } return ComponentHealth{Status: HealthStatusHealthy, Latency: time.Since(start).String(), CheckedAt: time.Now()} } func (s *HealthService) checkRedis(ctx context.Context) ComponentHealth { start := time.Now() if s.redis == nil { return ComponentHealth{Status: HealthStatusUnhealthy, Message: "redis client not initialized", CheckedAt: time.Now()} } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := s.redis.Ping(ctx).Err(); err != nil { return ComponentHealth{Status: HealthStatusUnhealthy, Message: fmt.Sprintf("redis ping failed: %v", err), Latency: time.Since(start).String(), CheckedAt: time.Now()} } return ComponentHealth{Status: HealthStatusHealthy, Latency: time.Since(start).String(), CheckedAt: time.Now()} }