146 lines
4.4 KiB
Go
146 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"time"
|
|
|
|
"fn-registry/functions/infra"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationsFS embed.FS
|
|
|
|
// openOpsDB opens the operations.db and applies pending migrations.
|
|
func openOpsDB(path string) (*sql.DB, error) {
|
|
db, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open ops db: %w", err)
|
|
}
|
|
if err := infra.ApplyVersionedMigrations(db, migrationsFS, "migrations"); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("apply migrations: %w", err)
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
// upsertState writes the latest check into service_state. When `overall` changed
|
|
// vs previous row, also appends a row to service_transition for audit.
|
|
func upsertState(db *sql.DB, c ServiceCheck) error {
|
|
now := time.Now().Unix()
|
|
|
|
var prevOverall string
|
|
row := db.QueryRow(
|
|
"SELECT overall FROM service_state WHERE app_id = ? AND pc_id = ?",
|
|
c.AppID, c.PCID,
|
|
)
|
|
if err := row.Scan(&prevOverall); err != nil && err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
changeTS := now
|
|
if prevOverall == c.Overall {
|
|
// Preserve last_change_ts.
|
|
var oldChange int64
|
|
_ = db.QueryRow(
|
|
"SELECT last_change_ts FROM service_state WHERE app_id = ? AND pc_id = ?",
|
|
c.AppID, c.PCID,
|
|
).Scan(&oldChange)
|
|
if oldChange > 0 {
|
|
changeTS = oldChange
|
|
}
|
|
}
|
|
|
|
listening := 0
|
|
if c.PortListening {
|
|
listening = 1
|
|
}
|
|
|
|
_, err := db.Exec(`
|
|
INSERT INTO service_state
|
|
(app_id, pc_id, systemd_state, port_listening, http_status, http_latency_ms,
|
|
last_check_ts, last_change_ts, last_error, overall)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(app_id, pc_id) DO UPDATE SET
|
|
systemd_state = excluded.systemd_state,
|
|
port_listening = excluded.port_listening,
|
|
http_status = excluded.http_status,
|
|
http_latency_ms = excluded.http_latency_ms,
|
|
last_check_ts = excluded.last_check_ts,
|
|
last_change_ts = excluded.last_change_ts,
|
|
last_error = excluded.last_error,
|
|
overall = excluded.overall
|
|
`, c.AppID, c.PCID, c.SystemdState, listening, c.HTTPStatus, c.HTTPLatencyMs,
|
|
now, changeTS, c.Error, c.Overall)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert state: %w", err)
|
|
}
|
|
|
|
if prevOverall != "" && prevOverall != c.Overall {
|
|
_, err = db.Exec(`
|
|
INSERT INTO service_transition (ts, app_id, pc_id, from_state, to_state, detail)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, now, c.AppID, c.PCID, prevOverall, c.Overall, c.Error)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// StateRow is one row of /api/services output.
|
|
type StateRow struct {
|
|
AppID string `json:"app_id"`
|
|
AppName string `json:"app_name"`
|
|
PCID string `json:"pc_id"`
|
|
IsSelf bool `json:"is_self"`
|
|
Reachable bool `json:"reachable"`
|
|
Runtime string `json:"runtime"`
|
|
Port int `json:"port"`
|
|
HealthEndpoint string `json:"health_endpoint"`
|
|
SystemdUnit string `json:"systemd_unit"`
|
|
SystemdScope string `json:"systemd_scope"`
|
|
SystemdState string `json:"systemd_state"`
|
|
PortListening bool `json:"port_listening"`
|
|
HTTPStatus int `json:"http_status"`
|
|
HTTPLatencyMs int `json:"http_latency_ms"`
|
|
LastCheckTS int64 `json:"last_check_ts"`
|
|
LastChangeTS int64 `json:"last_change_ts"`
|
|
LastError string `json:"last_error"`
|
|
Overall string `json:"overall"`
|
|
}
|
|
|
|
// loadStates returns the latest snapshot from service_state joined with the
|
|
// declared service metadata from registry.db (already merged in memory by the
|
|
// caller through the targetsCache).
|
|
func loadStates(opsDB *sql.DB, targets []Target, selfPC string) ([]StateRow, error) {
|
|
rows := make([]StateRow, 0, len(targets))
|
|
for _, t := range targets {
|
|
r := StateRow{
|
|
AppID: t.AppID,
|
|
AppName: t.Name,
|
|
PCID: t.PCID,
|
|
IsSelf: t.PCID == selfPC,
|
|
Runtime: t.Runtime,
|
|
Port: t.Port,
|
|
HealthEndpoint: t.HealthEndpoint,
|
|
SystemdUnit: t.SystemdUnit,
|
|
SystemdScope: t.SystemdScope,
|
|
Overall: "unknown",
|
|
}
|
|
var listening int
|
|
err := opsDB.QueryRow(`
|
|
SELECT systemd_state, port_listening, http_status, http_latency_ms,
|
|
last_check_ts, last_change_ts, last_error, overall
|
|
FROM service_state WHERE app_id = ? AND pc_id = ?
|
|
`, t.AppID, t.PCID).Scan(
|
|
&r.SystemdState, &listening, &r.HTTPStatus, &r.HTTPLatencyMs,
|
|
&r.LastCheckTS, &r.LastChangeTS, &r.LastError, &r.Overall,
|
|
)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return nil, err
|
|
}
|
|
r.PortListening = listening != 0
|
|
rows = append(rows, r)
|
|
}
|
|
return rows, nil
|
|
}
|