// Package healthcheck provides health status reporting for the server. package healthcheck import ( "context" "log/slog" "time" "git.eeqj.de/sneak/neoirc/internal/config" "git.eeqj.de/sneak/neoirc/internal/db" "git.eeqj.de/sneak/neoirc/internal/globals" "git.eeqj.de/sneak/neoirc/internal/logger" "git.eeqj.de/sneak/neoirc/internal/stats" "go.uber.org/fx" ) // Params defines the dependencies for creating a Healthcheck. type Params struct { fx.In Globals *globals.Globals Config *config.Config Logger *logger.Logger Database *db.Database Stats *stats.Tracker } // Healthcheck tracks server uptime and provides health status. type Healthcheck struct { // StartupTime records when the server started. StartupTime time.Time log *slog.Logger params *Params } // New creates a new Healthcheck instance. func New( lifecycle fx.Lifecycle, params Params, ) (*Healthcheck, error) { hcheck := &Healthcheck{ //nolint:exhaustruct // StartupTime set in OnStart params: ¶ms, log: params.Logger.Get(), } lifecycle.Append(fx.Hook{ OnStart: func(_ context.Context) error { hcheck.StartupTime = time.Now() return nil }, OnStop: func(_ context.Context) error { return nil }, }) return hcheck, nil } // Response is the JSON response returned by the health endpoint. type Response struct { Status string `json:"status"` Now string `json:"now"` UptimeSeconds int64 `json:"uptimeSeconds"` UptimeHuman string `json:"uptimeHuman"` Version string `json:"version"` Appname string `json:"appname"` Maintenance bool `json:"maintenanceMode"` // Runtime statistics. Sessions int64 `json:"sessions"` Clients int64 `json:"clients"` QueuedLines int64 `json:"queuedLines"` Channels int64 `json:"channels"` ConnectionsSinceBoot int64 `json:"connectionsSinceBoot"` SessionsSinceBoot int64 `json:"sessionsSinceBoot"` MessagesSinceBoot int64 `json:"messagesSinceBoot"` } // Healthcheck returns the current health status of the server. func (hcheck *Healthcheck) Healthcheck( ctx context.Context, ) *Response { resp := &Response{ Status: "ok", Now: time.Now().UTC().Format(time.RFC3339Nano), UptimeSeconds: int64(hcheck.uptime().Seconds()), UptimeHuman: hcheck.uptime().String(), Appname: hcheck.params.Globals.Appname, Version: hcheck.params.Globals.Version, Maintenance: hcheck.params.Config.MaintenanceMode, Sessions: 0, Clients: 0, QueuedLines: 0, Channels: 0, ConnectionsSinceBoot: hcheck.params.Stats.ConnectionsSinceBoot(), SessionsSinceBoot: hcheck.params.Stats.SessionsSinceBoot(), MessagesSinceBoot: hcheck.params.Stats.MessagesSinceBoot(), } hcheck.populateDBStats(ctx, resp) return resp } // populateDBStats fills in database-derived counters. func (hcheck *Healthcheck) populateDBStats( ctx context.Context, resp *Response, ) { sessions, err := hcheck.params.Database.GetUserCount(ctx) if err != nil { hcheck.log.Error( "healthcheck: session count failed", "error", err, ) } else { resp.Sessions = sessions } clients, err := hcheck.params.Database.GetClientCount(ctx) if err != nil { hcheck.log.Error( "healthcheck: client count failed", "error", err, ) } else { resp.Clients = clients } queued, err := hcheck.params.Database.GetQueueEntryCount(ctx) if err != nil { hcheck.log.Error( "healthcheck: queue entry count failed", "error", err, ) } else { resp.QueuedLines = queued } channels, err := hcheck.params.Database.GetChannelCount(ctx) if err != nil { hcheck.log.Error( "healthcheck: channel count failed", "error", err, ) } else { resp.Channels = channels } } func (hcheck *Healthcheck) uptime() time.Duration { return time.Since(hcheck.StartupTime) }