diff options
| -rw-r--r-- | cmd/relay/main.go | 18 | ||||
| -rw-r--r-- | internal/metrics/dashboard.html | 6 | ||||
| -rw-r--r-- | internal/storage/storage.go | 36 |
3 files changed, 58 insertions, 2 deletions
diff --git a/cmd/relay/main.go b/cmd/relay/main.go index e4fe9bc..8948ebf 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go | |||
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "os" | 9 | "os" |
| 10 | "os/signal" | 10 | "os/signal" |
| 11 | "syscall" | 11 | "syscall" |
| 12 | "time" | ||
| 12 | 13 | ||
| 13 | "connectrpc.com/connect" | 14 | "connectrpc.com/connect" |
| 14 | "golang.org/x/net/http2" | 15 | "golang.org/x/net/http2" |
| @@ -95,6 +96,23 @@ func main() { | |||
| 95 | wsHandler := wshandler.NewHandler(store, subManager) | 96 | wsHandler := wshandler.NewHandler(store, subManager) |
| 96 | if m != nil { | 97 | if m != nil { |
| 97 | wsHandler.SetMetrics(m) | 98 | wsHandler.SetMetrics(m) |
| 99 | |||
| 100 | // Update storage stats periodically | ||
| 101 | go func() { | ||
| 102 | ticker := time.NewTicker(30 * time.Second) | ||
| 103 | defer ticker.Stop() | ||
| 104 | |||
| 105 | // Update immediately on start | ||
| 106 | if stats, err := store.GetStats(context.Background()); err == nil { | ||
| 107 | m.UpdateStorageStats(stats.EventCount, stats.DBSizeBytes) | ||
| 108 | } | ||
| 109 | |||
| 110 | for range ticker.C { | ||
| 111 | if stats, err := store.GetStats(context.Background()); err == nil { | ||
| 112 | m.UpdateStorageStats(stats.EventCount, stats.DBSizeBytes) | ||
| 113 | } | ||
| 114 | } | ||
| 115 | }() | ||
| 98 | } | 116 | } |
| 99 | 117 | ||
| 100 | var grpcDisplay, httpDisplay, wsDisplay string | 118 | var grpcDisplay, httpDisplay, wsDisplay string |
diff --git a/internal/metrics/dashboard.html b/internal/metrics/dashboard.html index 363f1f7..63b42ed 100644 --- a/internal/metrics/dashboard.html +++ b/internal/metrics/dashboard.html | |||
| @@ -295,6 +295,12 @@ | |||
| 295 | document.getElementById('event_deletions').textContent = | 295 | document.getElementById('event_deletions').textContent = |
| 296 | sumMetric(metrics, `${prefix}_relay_event_deletions_total`); | 296 | sumMetric(metrics, `${prefix}_relay_event_deletions_total`); |
| 297 | 297 | ||
| 298 | const durationSum = sumMetric(metrics, `${prefix}_relay_request_duration_seconds_sum`); | ||
| 299 | const durationCount = sumMetric(metrics, `${prefix}_relay_request_duration_seconds_count`); | ||
| 300 | const avgLatencyMs = durationCount > 0 ? (durationSum / durationCount * 1000) : 0; | ||
| 301 | document.getElementById('avg_latency').innerHTML = | ||
| 302 | `${avgLatencyMs.toFixed(1)}<span class="metric-unit">ms</span>`; | ||
| 303 | |||
| 298 | const processStart = sumMetric(metrics, 'process_start_time_seconds'); | 304 | const processStart = sumMetric(metrics, 'process_start_time_seconds'); |
| 299 | if (processStart > 0) { | 305 | if (processStart > 0) { |
| 300 | if (!processStartTime) processStartTime = processStart; | 306 | if (!processStartTime) processStartTime = processStart; |
diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 9ef9956..ca9b264 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go | |||
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | "context" | 4 | "context" |
| 5 | "database/sql" | 5 | "database/sql" |
| 6 | "fmt" | 6 | "fmt" |
| 7 | "os" | ||
| 7 | 8 | ||
| 8 | _ "modernc.org/sqlite" // Pure Go SQLite driver | 9 | _ "modernc.org/sqlite" // Pure Go SQLite driver |
| 9 | ) | 10 | ) |
| @@ -11,7 +12,8 @@ import ( | |||
| 11 | // Storage provides event persistence using SQLite. | 12 | // Storage provides event persistence using SQLite. |
| 12 | // Consumers should define their own interface based on their needs. | 13 | // Consumers should define their own interface based on their needs. |
| 13 | type Storage struct { | 14 | type Storage struct { |
| 14 | db *sql.DB | 15 | db *sql.DB |
| 16 | dbPath string | ||
| 15 | } | 17 | } |
| 16 | 18 | ||
| 17 | // New creates a new Storage instance and initializes the database schema. | 19 | // New creates a new Storage instance and initializes the database schema. |
| @@ -48,7 +50,10 @@ func New(dbPath string) (*Storage, error) { | |||
| 48 | } | 50 | } |
| 49 | } | 51 | } |
| 50 | 52 | ||
| 51 | s := &Storage{db: db} | 53 | s := &Storage{ |
| 54 | db: db, | ||
| 55 | dbPath: dbPath, | ||
| 56 | } | ||
| 52 | 57 | ||
| 53 | // Initialize schema | 58 | // Initialize schema |
| 54 | if err := s.initSchema(context.Background()); err != nil { | 59 | if err := s.initSchema(context.Background()); err != nil { |
| @@ -142,3 +147,30 @@ func (s *Storage) initSchema(ctx context.Context) error { | |||
| 142 | func (s *Storage) DB() *sql.DB { | 147 | func (s *Storage) DB() *sql.DB { |
| 143 | return s.db | 148 | return s.db |
| 144 | } | 149 | } |
| 150 | |||
| 151 | type Stats struct { | ||
| 152 | EventCount int64 | ||
| 153 | DBSizeBytes int64 | ||
| 154 | } | ||
| 155 | |||
| 156 | func (s *Storage) GetStats(ctx context.Context) (*Stats, error) { | ||
| 157 | var eventCount int64 | ||
| 158 | err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM events WHERE deleted = 0").Scan(&eventCount) | ||
| 159 | if err != nil { | ||
| 160 | return nil, fmt.Errorf("failed to count events: %w", err) | ||
| 161 | } | ||
| 162 | |||
| 163 | var dbSize int64 | ||
| 164 | if s.dbPath != ":memory:" { | ||
| 165 | info, err := os.Stat(s.dbPath) | ||
| 166 | if err != nil { | ||
| 167 | return nil, fmt.Errorf("failed to stat database file: %w", err) | ||
| 168 | } | ||
| 169 | dbSize = info.Size() | ||
| 170 | } | ||
| 171 | |||
| 172 | return &Stats{ | ||
| 173 | EventCount: eventCount, | ||
| 174 | DBSizeBytes: dbSize, | ||
| 175 | }, nil | ||
| 176 | } | ||
