108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
// Package uptime provides read-only access to arcline-uptime's SQLite database.
|
|
// The portal never writes to the uptime DB — it only queries it.
|
|
package uptime
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Reader is a read-only view of the arcline-uptime database.
|
|
type Reader struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// MonitorStatus is a summary of a single monitor's current state.
|
|
type MonitorStatus struct {
|
|
Name string
|
|
Label string // display label from the portal (not from uptime)
|
|
Up bool
|
|
LastChecked time.Time
|
|
ResponseMS int64
|
|
Uptime24h float64
|
|
Uptime7d float64
|
|
Uptime30d float64
|
|
}
|
|
|
|
// Open opens the uptime database in read-only mode.
|
|
func Open(path string) (*Reader, error) {
|
|
db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=ro", path))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open uptime db: %w", err)
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
// Verify the expected schema exists.
|
|
var n int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='checks'`).Scan(&n); err != nil || n == 0 {
|
|
db.Close()
|
|
return nil, fmt.Errorf("uptime db does not contain a 'checks' table — is the path correct?")
|
|
}
|
|
return &Reader{db: db}, nil
|
|
}
|
|
|
|
func (r *Reader) Close() error { return r.db.Close() }
|
|
|
|
// GetStatus returns the current status for each of the supplied monitor names.
|
|
// Unknown monitors (no check records) are included with Up=false.
|
|
func (r *Reader) GetStatus(monitors []string) ([]MonitorStatus, error) {
|
|
out := make([]MonitorStatus, 0, len(monitors))
|
|
for _, name := range monitors {
|
|
ms := MonitorStatus{Name: name}
|
|
|
|
// Latest check
|
|
var ts int64
|
|
var up, statusCode int
|
|
var responseMS int64
|
|
err := r.db.QueryRow(
|
|
`SELECT checked_at, up, status_code, response_ms FROM checks
|
|
WHERE monitor_name = ? ORDER BY checked_at DESC LIMIT 1`,
|
|
name,
|
|
).Scan(&ts, &up, &statusCode, &responseMS)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return nil, err
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
ms.Up = up != 0
|
|
ms.LastChecked = time.Unix(ts, 0)
|
|
ms.ResponseMS = responseMS
|
|
}
|
|
|
|
// Uptime percentages
|
|
ms.Uptime24h, _ = r.uptimePct(name, time.Now().Add(-24*time.Hour))
|
|
ms.Uptime7d, _ = r.uptimePct(name, time.Now().Add(-7*24*time.Hour))
|
|
ms.Uptime30d, _ = r.uptimePct(name, time.Now().Add(-30*24*time.Hour))
|
|
|
|
out = append(out, ms)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Reader) uptimePct(monitorName string, since time.Time) (float64, error) {
|
|
var total, upCount int64
|
|
err := r.db.QueryRow(
|
|
`SELECT COUNT(*), COALESCE(SUM(up), 0) FROM checks WHERE monitor_name = ? AND checked_at >= ?`,
|
|
monitorName, since.Unix(),
|
|
).Scan(&total, &upCount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if total == 0 {
|
|
return 100.0, nil
|
|
}
|
|
return float64(upCount) / float64(total) * 100.0, nil
|
|
}
|
|
|
|
// Available reports whether the uptime DB can be opened at path.
|
|
// Used at startup to warn if the path is wrong without hard-failing.
|
|
func Available(path string) bool {
|
|
r, err := Open(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
r.Close()
|
|
return true
|
|
}
|