Extracted from ardenone-cluster/containers/zai-proxy and ardenone-cluster/containers/zai-proxy-dashboard. - proxy/: OpenAI-compatible ZAI reverse proxy (Go, v1.10.0) - Token counting, rate limiting, Prometheus metrics, canary support - dashboard/: Metrics dashboard backend + React frontend (Go, v1.0.0) - Prometheus collector, SQLite storage, SSE live updates - docs/: Operational notes, research, and plan subdirs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
3 KiB
Go
119 lines
3 KiB
Go
// Package storage implements SQLite-based metric storage.
|
|
package storage
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Schema manages the SQLite database schema.
|
|
type Schema struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewSchema creates a new Schema manager.
|
|
func NewSchema(db *sql.DB) *Schema {
|
|
return &Schema{db: db}
|
|
}
|
|
|
|
// Initialize creates the database schema if it doesn't exist.
|
|
func (s *Schema) Initialize() error {
|
|
// Enable WAL mode for concurrent reads/writes
|
|
if _, err := s.db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
|
return fmt.Errorf("failed to enable WAL mode: %w", err)
|
|
}
|
|
|
|
// High-resolution data (5-second intervals, 24h retention)
|
|
if _, err := s.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS metrics_5s (
|
|
ts INTEGER NOT NULL,
|
|
variant TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
PRIMARY KEY (ts, variant)
|
|
)
|
|
`); err != nil {
|
|
return fmt.Errorf("failed to create metrics_5s table: %w", err)
|
|
}
|
|
|
|
// Downsampled data (1-minute averages, 7-day retention)
|
|
if _, err := s.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS metrics_1m (
|
|
ts INTEGER NOT NULL,
|
|
variant TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
PRIMARY KEY (ts, variant)
|
|
)
|
|
`); err != nil {
|
|
return fmt.Errorf("failed to create metrics_1m table: %w", err)
|
|
}
|
|
|
|
// Indexes for range queries
|
|
if _, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_5s_ts ON metrics_5s(ts)`); err != nil {
|
|
return fmt.Errorf("failed to create idx_5s_ts: %w", err)
|
|
}
|
|
if _, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_1m_ts ON metrics_1m(ts)`); err != nil {
|
|
return fmt.Errorf("failed to create idx_1m_ts: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DDL returns the SQL statements to create the schema.
|
|
func (s *Schema) DDL() []string {
|
|
return []string{
|
|
`PRAGMA journal_mode=WAL`,
|
|
`CREATE TABLE IF NOT EXISTS metrics_5s (
|
|
ts INTEGER NOT NULL,
|
|
variant TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
PRIMARY KEY (ts, variant)
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS metrics_1m (
|
|
ts INTEGER NOT NULL,
|
|
variant TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
PRIMARY KEY (ts, variant)
|
|
)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_5s_ts ON metrics_5s(ts)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_1m_ts ON metrics_1m(ts)`,
|
|
}
|
|
}
|
|
|
|
// Config holds storage configuration.
|
|
type Config struct {
|
|
DBPath string // Path to SQLite database file
|
|
Retention5s time.Duration // Retention for 5s data (default 24h)
|
|
Retention1m time.Duration // Retention for 1m data (default 168h = 7d)
|
|
}
|
|
|
|
// DefaultConfig returns the default storage configuration.
|
|
func DefaultConfig() Config {
|
|
dbPath := os.Getenv("DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "/data/dashboard.db"
|
|
}
|
|
|
|
retention5s := 24 * time.Hour
|
|
if v := os.Getenv("RETENTION_5S"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
retention5s = d
|
|
}
|
|
}
|
|
|
|
retention1m := 168 * time.Hour // 7 days
|
|
if v := os.Getenv("RETENTION_1M"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
retention1m = d
|
|
}
|
|
}
|
|
|
|
return Config{
|
|
DBPath: dbPath,
|
|
Retention5s: retention5s,
|
|
Retention1m: retention1m,
|
|
}
|
|
}
|