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:
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
)
|
||||
|
||||
// listEventLog handles GET /api/events/log.
|
||||
// Supports query parameters: severity, source, since, until, limit, offset.
|
||||
func (s *Server) listEventLog(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
limit, _ := strconv.Atoi(q.Get("limit"))
|
||||
offset, _ := strconv.Atoi(q.Get("offset"))
|
||||
|
||||
filter := store.EventLogFilter{
|
||||
Severity: q.Get("severity"),
|
||||
Source: q.Get("source"),
|
||||
Since: q.Get("since"),
|
||||
Until: q.Get("until"),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
events, err := s.store.ListEvents(filter)
|
||||
if err != nil {
|
||||
slog.Error("failed to list events", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "failed to list events")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, events)
|
||||
}
|
||||
|
||||
// getEventLogStats handles GET /api/events/log/stats.
|
||||
func (s *Server) getEventLogStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := s.store.GetEventStats()
|
||||
if err != nil {
|
||||
slog.Error("failed to get event stats", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "failed to get event stats")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
@@ -125,6 +125,8 @@ func (s *Server) Router() chi.Router {
|
||||
r.Get("/deploys", s.listDeploys)
|
||||
r.Get("/deploys/{id}/logs", s.streamDeployLogs)
|
||||
r.Get("/events", s.streamEvents)
|
||||
r.Get("/events/log", s.listEventLog)
|
||||
r.Get("/events/log/stats", s.getEventLogStats)
|
||||
r.Get("/registries", s.listRegistries)
|
||||
r.Route("/registries/{id}", func(r chi.Router) {
|
||||
r.Get("/tags/*", s.listRegistryTags)
|
||||
|
||||
+21
-12
@@ -24,7 +24,8 @@ type settingsRequest struct {
|
||||
NpmEmail string `json:"npm_email"`
|
||||
NpmPassword string `json:"npm_password"`
|
||||
PollingInterval string `json:"polling_interval"`
|
||||
SSLCertificateID *int `json:"ssl_certificate_id,omitempty"`
|
||||
SSLCertificateID *int `json:"ssl_certificate_id,omitempty"`
|
||||
StaleThresholdDays *int `json:"stale_threshold_days,omitempty"`
|
||||
}
|
||||
|
||||
// getSettings handles GET /api/settings.
|
||||
@@ -37,17 +38,18 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Return settings without sensitive fields.
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"domain": settings.Domain,
|
||||
"server_ip": settings.ServerIP,
|
||||
"network": settings.Network,
|
||||
"subdomain_pattern": settings.SubdomainPattern,
|
||||
"notification_url": settings.NotificationURL,
|
||||
"npm_url": settings.NpmURL,
|
||||
"npm_email": settings.NpmEmail,
|
||||
"has_npm_password": settings.NpmPassword != "",
|
||||
"polling_interval": settings.PollingInterval,
|
||||
"ssl_certificate_id": settings.SSLCertificateID,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
"domain": settings.Domain,
|
||||
"server_ip": settings.ServerIP,
|
||||
"network": settings.Network,
|
||||
"subdomain_pattern": settings.SubdomainPattern,
|
||||
"notification_url": settings.NotificationURL,
|
||||
"npm_url": settings.NpmURL,
|
||||
"npm_email": settings.NpmEmail,
|
||||
"has_npm_password": settings.NpmPassword != "",
|
||||
"polling_interval": settings.PollingInterval,
|
||||
"ssl_certificate_id": settings.SSLCertificateID,
|
||||
"stale_threshold_days": settings.StaleThresholdDays,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,6 +103,13 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
updated.SSLCertificateID = *req.SSLCertificateID
|
||||
sslChanged = true
|
||||
}
|
||||
if req.StaleThresholdDays != nil {
|
||||
if *req.StaleThresholdDays < 1 {
|
||||
respondError(w, http.StatusBadRequest, "stale_threshold_days must be at least 1")
|
||||
return
|
||||
}
|
||||
updated.StaleThresholdDays = *req.StaleThresholdDays
|
||||
}
|
||||
|
||||
if err := s.store.UpdateSettings(updated); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
|
||||
|
||||
+2
-2
@@ -150,9 +150,9 @@ func (s *Server) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
// Subscribe to instance status and deploy status events.
|
||||
// Subscribe to instance status, deploy status, and persistent event log events.
|
||||
sub := s.eventBus.Subscribe(func(evt events.Event) bool {
|
||||
return evt.Type == events.EventInstanceStatus || evt.Type == events.EventDeployStatus
|
||||
return evt.Type == events.EventInstanceStatus || evt.Type == events.EventDeployStatus || evt.Type == events.EventLog
|
||||
})
|
||||
defer s.eventBus.Unsubscribe(sub)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user