mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-13 02:36:48 +00:00
6b037a92b4
- [x] Reorder struct fields across codebase for consistency - [x] Add analytics event handlers and tests - [x] Add authentication API key management handlers and tests - [x] Add pre-warming control handlers and tests - [x] Implement S3 storage backend with tests - [x] Implement SMB/CIFS storage backend with tests - [x] Add CDN middleware tests - [x] Integrate analytics tracking into cache manager - [x] Add S3 and SMB storage initialization in app setup - [x] Add CDN caching to proxy handlers - [x] Remove distributed locking (Redis lock manager) - [x] Remove proxy common package and utilities - [x] Remove standalone HTTP server package - [x] Remove logger middleware - [x] Simplify error handling utilities - [x] Update config with S3 and SMB options - [x] Update cache manager signature to include analytics
179 lines
4.5 KiB
Go
179 lines
4.5 KiB
Go
package health
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
json "github.com/goccy/go-json"
|
|
"github.com/lukaszraczylo/gohoarder/internal/version"
|
|
)
|
|
|
|
// Status represents component health status
|
|
type Status string
|
|
|
|
const (
|
|
StatusHealthy Status = "healthy"
|
|
StatusUnhealthy Status = "unhealthy"
|
|
StatusDegraded Status = "degraded"
|
|
)
|
|
|
|
// Check represents a single health check
|
|
type Check struct {
|
|
Fn func(context.Context) (Status, string) `json:"-"`
|
|
Name string `json:"name"`
|
|
Status Status `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Response is the health check response
|
|
type Response struct {
|
|
Data *HealthData `json:"data,omitempty"`
|
|
Metadata *Metadata `json:"metadata,omitempty"`
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
// HealthData contains health check data
|
|
type HealthData struct {
|
|
Components map[string]*Component `json:"components"`
|
|
Status Status `json:"status"`
|
|
Version string `json:"version"`
|
|
Uptime string `json:"uptime"`
|
|
}
|
|
|
|
// Component represents a system component
|
|
type Component struct {
|
|
Status Status `json:"status"`
|
|
Details map[string]interface{} `json:"details,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Metadata contains response metadata
|
|
type Metadata struct {
|
|
RequestID string `json:"request_id"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
// Checker manages health checks
|
|
type Checker struct {
|
|
startTime time.Time
|
|
checks []*Check
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// New creates a new health checker
|
|
func New() *Checker {
|
|
return &Checker{
|
|
checks: make([]*Check, 0),
|
|
startTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
// AddCheck adds a health check
|
|
func (c *Checker) AddCheck(name string, fn func(context.Context) (Status, string)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.checks = append(c.checks, &Check{
|
|
Name: name,
|
|
Fn: fn,
|
|
})
|
|
}
|
|
|
|
// RunChecks runs all health checks
|
|
func (c *Checker) RunChecks(ctx context.Context) *HealthData {
|
|
c.mu.RLock()
|
|
checks := make([]*Check, len(c.checks))
|
|
copy(checks, c.checks)
|
|
c.mu.RUnlock()
|
|
|
|
components := make(map[string]*Component)
|
|
overallStatus := StatusHealthy
|
|
|
|
for _, check := range checks {
|
|
status, errMsg := check.Fn(ctx)
|
|
components[check.Name] = &Component{
|
|
Status: status,
|
|
Error: errMsg,
|
|
}
|
|
|
|
// Determine overall status
|
|
if status == StatusUnhealthy {
|
|
overallStatus = StatusUnhealthy
|
|
} else if status == StatusDegraded && overallStatus == StatusHealthy {
|
|
overallStatus = StatusDegraded
|
|
}
|
|
}
|
|
|
|
return &HealthData{
|
|
Status: overallStatus,
|
|
Version: version.Version,
|
|
Uptime: time.Since(c.startTime).String(),
|
|
Components: components,
|
|
}
|
|
}
|
|
|
|
// HealthHandler returns an HTTP handler for health checks
|
|
func (c *Checker) HealthHandler() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
healthData := c.RunChecks(ctx)
|
|
|
|
response := Response{
|
|
Success: healthData.Status == StatusHealthy,
|
|
Data: healthData,
|
|
Metadata: &Metadata{
|
|
RequestID: r.Header.Get("X-Request-ID"),
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
}
|
|
|
|
statusCode := http.StatusOK
|
|
if healthData.Status == StatusUnhealthy {
|
|
statusCode = http.StatusServiceUnavailable
|
|
} else if healthData.Status == StatusDegraded {
|
|
statusCode = http.StatusOK // 200 but degraded
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
_ = json.NewEncoder(w).Encode(response) // #nosec G104 -- JSON response write
|
|
}
|
|
}
|
|
|
|
// ReadyHandler returns an HTTP handler for readiness checks
|
|
func (c *Checker) ReadyHandler() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
healthData := c.RunChecks(ctx)
|
|
|
|
ready := healthData.Status != StatusUnhealthy
|
|
|
|
response := Response{
|
|
Success: ready,
|
|
Data: &HealthData{
|
|
Status: healthData.Status,
|
|
Components: healthData.Components,
|
|
},
|
|
Metadata: &Metadata{
|
|
RequestID: r.Header.Get("X-Request-ID"),
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
}
|
|
|
|
statusCode := http.StatusOK
|
|
if !ready {
|
|
statusCode = http.StatusServiceUnavailable
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
_ = json.NewEncoder(w).Encode(response) // #nosec G104 -- JSON response write
|
|
}
|
|
}
|