feat(observability): phase 1 - schema, models & event log backend

Add database foundation for observability features:
- event_log table with severity/source filtering and pagination
- standalone_proxies table for user-created reverse proxies
- stale_threshold_days setting (default 7 days)
- Auto-persist warn/error events from event bus to database
- SSE broadcast of persistent events for real-time UI updates
- Frontend types and API functions for downstream UI phases
This commit is contained in:
2026-03-30 10:59:13 +03:00
parent f71c314262
commit c38b7d4c78
23 changed files with 1149 additions and 20 deletions
+70
View File
@@ -2,6 +2,7 @@ package events
import (
"encoding/json"
"log/slog"
"sync"
)
@@ -17,6 +18,9 @@ const (
// EventDeployStatus is emitted when a deploy status changes.
EventDeployStatus EventType = "deploy_status"
// EventLog is emitted when a persistent event is logged.
EventLog EventType = "event_log"
)
// Event is a single event published on the bus.
@@ -50,6 +54,72 @@ type DeployStatusPayload struct {
Error string `json:"error,omitempty"`
}
// EventLogPayload is the payload for EventLog events (persistent event log).
type EventLogPayload struct {
ID int64 `json:"id"`
Source string `json:"source"`
Severity string `json:"severity"`
Message string `json:"message"`
Metadata string `json:"metadata"`
CreatedAt string `json:"created_at"`
}
// PersistFunc is a callback that persists an event log entry.
// It receives source, severity, message, and metadata (JSON string).
// It returns the persisted entry's ID and created_at timestamp.
type PersistFunc func(source, severity, message, metadata string) (int64, string, error)
// RegisterPersistentLogger subscribes to the bus and auto-persists warn/error
// events by calling the provided persist function. It also re-publishes the
// persisted event as an EventLog so SSE clients receive it in real-time.
// Call the returned function to unsubscribe.
func (b *Bus) RegisterPersistentLogger(persist PersistFunc) func() {
sub := b.Subscribe(func(evt Event) bool {
// Only persist deploy log events with warn/error level.
if evt.Type != EventDeployLog {
return false
}
p, ok := evt.Payload.(DeployLogPayload)
if !ok {
return false
}
return p.Level == "warn" || p.Level == "error"
})
go func() {
for evt := range sub {
p, ok := evt.Payload.(DeployLogPayload)
if !ok {
continue
}
metaBytes, _ := json.Marshal(map[string]string{"deploy_id": p.DeployID})
metadata := string(metaBytes)
id, createdAt, err := persist("deploy", p.Level, p.Message, metadata)
if err != nil {
slog.Error("failed to persist event log", "source", "deploy", "level", p.Level, "error", err)
continue
}
// Re-publish as EventLog for SSE clients.
b.Publish(Event{
Type: EventLog,
Payload: EventLogPayload{
ID: id,
Source: "deploy",
Severity: p.Level,
Message: p.Message,
Metadata: metadata,
CreatedAt: createdAt,
},
})
}
}()
return func() {
b.Unsubscribe(sub)
}
}
// Subscriber is a channel that receives events.
type Subscriber chan Event