package main import ( "context" "database/sql" "encoding/json" "log" "os" "os/signal" "strconv" "strings" "syscall" "time" "github.com/bigtux/ophion/internal/aiapm" aiapmapi "github.com/bigtux/ophion/internal/api" "github.com/bigtux/ophion/internal/auth" "github.com/bigtux/ophion/internal/otel" "github.com/bigtux/ophion/internal/security" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/google/uuid" _ "github.com/lib/pq" ) // ═══════════════════════════════════════════════════════════ // 🐍 OPHION Server - Observability Platform API // ═══════════════════════════════════════════════════════════ type Server struct { app *fiber.App db *sql.DB authHandler *auth.AuthHandler } type Metric struct { Timestamp time.Time `json:"timestamp"` Service string `json:"service"` Host string `json:"host"` Name string `json:"name"` Value float64 `json:"value"` MetricType string `json:"metric_type"` Tags map[string]string `json:"tags,omitempty"` } type LogEntry struct { Timestamp time.Time `json:"timestamp"` Service string `json:"service"` Host string `json:"host"` Level string `json:"level"` Message string `json:"message"` TraceID string `json:"trace_id,omitempty"` SpanID string `json:"span_id,omitempty"` Source string `json:"source"` ContainerID string `json:"container_id,omitempty"` } type Span struct { TraceID string `json:"trace_id"` SpanID string `json:"span_id"` ParentSpanID string `json:"parent_span_id,omitempty"` Service string `json:"service"` Operation string `json:"operation"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` DurationNs int64 `json:"duration_ns"` StatusCode string `json:"status_code"` StatusMsg string `json:"status_message,omitempty"` Kind string `json:"kind"` Attributes map[string]any `json:"attributes,omitempty"` } type Alert struct { ID string `json:"id"` Name string `json:"name"` Severity string `json:"severity"` Service string `json:"service"` Host string `json:"host"` Message string `json:"message"` Status string `json:"status"` FiredAt time.Time `json:"fired_at"` ResolvedAt *time.Time `json:"resolved_at,omitempty"` } type Agent struct { ID string `json:"id"` Hostname string `json:"hostname"` IP string `json:"ip"` Version string `json:"version"` Status string `json:"status"` LastSeen time.Time `json:"last_seen"` CreatedAt time.Time `json:"created_at"` } func main() { // Initialize database pgDSN := getEnv("DATABASE_URL", "postgres://ophion:ophion@localhost:5432/ophion?sslmode=disable") db, err := sql.Open("postgres", pgDSN) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } defer db.Close() // Test connection if err := db.Ping(); err != nil { log.Printf("⚠ Database not available: %v", err) } else { log.Println("✓ Connected to PostgreSQL") initSchema(db) // Initialize AI APM table if err := aiapm.CreateTable(db); err != nil { log.Printf("⚠ Failed to create AI APM table: %v", err) } else { log.Println("✓ AI APM table initialized") } // Create default admin user if err := auth.CreateDefaultAdmin(db); err != nil { log.Printf("⚠ Failed to create default admin: %v", err) } } // Initialize auth handler jwtSecret := getEnv("JWT_SECRET", "ophion-super-secret-key-change-in-production") authHandler := auth.NewAuthHandler(db, jwtSecret) server := &Server{db: db, authHandler: authHandler} // Create Fiber app app := fiber.New(fiber.Config{ AppName: "OPHION Observability Platform", BodyLimit: 50 * 1024 * 1024, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, }) server.app = app // Middleware app.Use(recover.New()) app.Use(logger.New(logger.Config{ Format: "${time} ${status} ${method} ${path} ${latency}\n", })) app.Use(cors.New(cors.Config{ AllowOrigins: "*", AllowHeaders: "Origin, Content-Type, Accept, Authorization", })) // Routes server.setupRoutes() // Graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh log.Println("🛑 Shutting down server...") cancel() app.Shutdown() }() // Background jobs go server.runBackgroundJobs(ctx) port := getEnv("PORT", "8080") log.Printf("🐍 OPHION server starting on port %s", port) if err := app.Listen(":" + port); err != nil { log.Fatal(err) } } func initSchema(db *sql.DB) { schema := ` CREATE TABLE IF NOT EXISTS metrics ( id SERIAL PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), service VARCHAR(255) NOT NULL, host VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, value DOUBLE PRECISION NOT NULL, metric_type VARCHAR(50), tags JSONB ); CREATE TABLE IF NOT EXISTS logs ( id SERIAL PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), service VARCHAR(255) NOT NULL, host VARCHAR(255) NOT NULL, level VARCHAR(20), message TEXT, trace_id VARCHAR(64), span_id VARCHAR(32), source VARCHAR(50), container_id VARCHAR(64) ); CREATE TABLE IF NOT EXISTS spans ( id SERIAL PRIMARY KEY, trace_id VARCHAR(64) NOT NULL, span_id VARCHAR(32) NOT NULL, parent_span_id VARCHAR(32), service VARCHAR(255) NOT NULL, operation VARCHAR(255) NOT NULL, start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ NOT NULL, duration_ns BIGINT, status_code VARCHAR(20), status_message TEXT, kind VARCHAR(20), attributes JSONB ); CREATE TABLE IF NOT EXISTS agents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), hostname VARCHAR(255) NOT NULL UNIQUE, ip VARCHAR(45), version VARCHAR(50), status VARCHAR(20) DEFAULT 'active', last_seen TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS alerts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, severity VARCHAR(20), service VARCHAR(255), host VARCHAR(255), message TEXT, status VARCHAR(20) DEFAULT 'firing', fired_at TIMESTAMPTZ DEFAULT NOW(), resolved_at TIMESTAMPTZ ); -- Users table for JWT auth CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'user', created_at TIMESTAMPTZ DEFAULT NOW() ); -- API Keys table for agent auth CREATE TABLE IF NOT EXISTS api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key_hash VARCHAR(64) NOT NULL UNIQUE, prefix VARCHAR(20) NOT NULL, name VARCHAR(255) NOT NULL, user_id UUID REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), last_used TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_metrics_service_name ON metrics(service, name); CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service); CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON spans(trace_id); CREATE INDEX IF NOT EXISTS idx_spans_service ON spans(service); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash); ` if _, err := db.Exec(schema); err != nil { log.Printf("Error creating schema: %v", err) } else { log.Println("✓ Database schema initialized") } } func (s *Server) setupRoutes() { // Health check (public) s.app.Get("/health", s.healthCheck) // Security headers s.app.Use(security.SecurityHeaders()) // ═══════════════════════════════════════════════════════════ // 🔭 OTLP HTTP Receiver (OpenTelemetry Protocol) // ═══════════════════════════════════════════════════════════ // Standard OTLP endpoint - can be public or protected based on config otlpReceiver := otel.NewOTLPReceiver(s.db) // OTLP routes (public by default for easy integration) // For production, consider adding auth middleware s.app.Post("/v1/traces", otlpReceiver.HandleTraces) // Also support the full path that some SDKs use s.app.Post("/v1/traces/", otlpReceiver.HandleTraces) // API v1 api := s.app.Group("/api/v1") // ═══════════════════════════════════════════════════════════ // 🔓 PUBLIC ROUTES (no auth required) // ═══════════════════════════════════════════════════════════ // Auth endpoints authGroup := api.Group("/auth") authGroup.Post("/login", s.authHandler.Login) authGroup.Post("/register", s.authHandler.Register) // ═══════════════════════════════════════════════════════════ // 🔐 PROTECTED ROUTES (auth required) // ═══════════════════════════════════════════════════════════ // Ingest endpoints (for agents - API key or JWT) ingest := api.Group("/ingest", s.authMiddleware()) ingest.Post("/metrics", s.ingestMetrics) ingest.Post("/logs", s.ingestLogs) ingest.Post("/traces", s.ingestTraces) // Legacy ingest routes (agent key auth for simplicity) api.Post("/metrics", s.agentAuthMiddleware(), s.ingestMetrics) api.Post("/logs", s.agentAuthMiddleware(), s.ingestLogs) api.Post("/traces", s.agentAuthMiddleware(), s.ingestTraces) // Protected routes protected := api.Group("", s.authMiddleware()) // Query endpoints (for dashboard) protected.Get("/metrics", s.queryMetrics) protected.Get("/metrics/names", s.getMetricNames) protected.Get("/logs", s.queryLogs) protected.Get("/traces", s.queryTraces) protected.Get("/traces/:traceId", s.getTrace) protected.Get("/services", s.getServices) // Agents protected.Get("/agents", s.getAgents) protected.Post("/agents/register", s.registerAgent) // Alerts protected.Get("/alerts", s.getAlerts) protected.Post("/alerts", s.createAlert) protected.Put("/alerts/:id/resolve", s.resolveAlert) // Dashboard protected.Get("/dashboard/overview", s.getDashboardOverview) // AI APM routes aiapmapi.RegisterAIAPMRoutes(protected, s.db) // User info protected.Get("/me", s.authHandler.Me) // API Keys management (JWT only) protected.Post("/api-keys", s.authHandler.CreateAPIKey) protected.Get("/api-keys", s.authHandler.ListAPIKeys) protected.Delete("/api-keys/:id", s.authHandler.DeleteAPIKey) } // authMiddleware creates authentication middleware that accepts both JWT and API keys func (s *Server) authMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Missing Authorization header", }) } token := strings.TrimPrefix(authHeader, "Bearer ") // Check if it's an API key (starts with ophion_) if strings.HasPrefix(token, security.APIKeyPrefix) { return s.authenticateAPIKey(c, token) } // Otherwise treat as JWT return s.authenticateJWT(c, token) } } // authenticateAPIKey validates API key authentication func (s *Server) authenticateAPIKey(c *fiber.Ctx, apiKey string) error { if !security.ValidateAPIKeyFormat(apiKey) { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid API key format", }) } keyHash := security.HashAPIKey(apiKey) var keyID, name, userID string err := s.db.QueryRow(` SELECT id, name, user_id FROM api_keys WHERE key_hash = $1 `, keyHash).Scan(&keyID, &name, &userID) if err != nil { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid API key", }) } // Update last_used (async) go s.db.Exec(`UPDATE api_keys SET last_used = NOW() WHERE id = $1`, keyID) // Set context c.Locals("auth_type", "api_key") c.Locals("api_key_id", keyID) c.Locals("user_id", userID) return c.Next() } // authenticateJWT validates JWT token authentication func (s *Server) authenticateJWT(c *fiber.Ctx, token string) error { authService := s.authHandler.GetAuthService() claims, err := authService.ValidateAccessToken(token) if err != nil { return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": err.Error(), }) } // Set context c.Locals("auth_type", "jwt") c.Locals("user_id", claims.UserID) c.Locals("email", claims.Email) c.Locals("role", claims.Role) c.Locals("token_id", claims.TokenID) return c.Next() } func (s *Server) healthCheck(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "status": "healthy", "service": "ophion", "version": "0.2.0", "timestamp": time.Now(), }) } // ───────────────────────────────────────────────────────────── // Ingest Endpoints // ───────────────────────────────────────────────────────────── func (s *Server) ingestMetrics(c *fiber.Ctx) error { var req struct { Metrics []Metric `json:"metrics"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } for _, m := range req.Metrics { tags, _ := json.Marshal(m.Tags) _, err := s.db.Exec(` INSERT INTO metrics (timestamp, service, host, name, value, metric_type, tags) VALUES ($1, $2, $3, $4, $5, $6, $7)`, m.Timestamp, m.Service, m.Host, m.Name, m.Value, m.MetricType, tags) if err != nil { log.Printf("Error inserting metric: %v", err) } } // Update agent last_seen if len(req.Metrics) > 0 { host := req.Metrics[0].Host s.db.Exec(`UPDATE agents SET last_seen = NOW(), status = 'active' WHERE hostname = $1`, host) } return c.JSON(fiber.Map{"status": "received", "count": len(req.Metrics)}) } func (s *Server) ingestLogs(c *fiber.Ctx) error { var req struct { Logs []LogEntry `json:"logs"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } for _, l := range req.Logs { _, err := s.db.Exec(` INSERT INTO logs (timestamp, service, host, level, message, trace_id, span_id, source, container_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, l.Timestamp, l.Service, l.Host, l.Level, l.Message, l.TraceID, l.SpanID, l.Source, l.ContainerID) if err != nil { log.Printf("Error inserting log: %v", err) } } return c.JSON(fiber.Map{"status": "received", "count": len(req.Logs)}) } func (s *Server) ingestTraces(c *fiber.Ctx) error { var req struct { Spans []Span `json:"spans"` Host string `json:"host"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } for _, sp := range req.Spans { attrs, _ := json.Marshal(sp.Attributes) _, err := s.db.Exec(` INSERT INTO spans (trace_id, span_id, parent_span_id, service, operation, start_time, end_time, duration_ns, status_code, status_message, kind, attributes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, sp.TraceID, sp.SpanID, sp.ParentSpanID, sp.Service, sp.Operation, sp.StartTime, sp.EndTime, sp.DurationNs, sp.StatusCode, sp.StatusMsg, sp.Kind, attrs) if err != nil { log.Printf("Error inserting span: %v", err) } } return c.JSON(fiber.Map{"status": "received", "count": len(req.Spans)}) } // ───────────────────────────────────────────────────────────── // Query Endpoints // ───────────────────────────────────────────────────────────── func (s *Server) queryMetrics(c *fiber.Ctx) error { service := c.Query("service", "system") name := c.Query("name", "cpu.usage_percent") from := parseTime(c.Query("from"), time.Now().Add(-1*time.Hour)) to := parseTime(c.Query("to"), time.Now()) rows, err := s.db.Query(` SELECT timestamp, value FROM metrics WHERE service = $1 AND name = $2 AND timestamp >= $3 AND timestamp <= $4 ORDER BY timestamp ASC LIMIT 1000`, service, name, from, to) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var metrics []map[string]any for rows.Next() { var ts time.Time var val float64 if err := rows.Scan(&ts, &val); err == nil { metrics = append(metrics, map[string]any{"timestamp": ts, "value": val}) } } return c.JSON(fiber.Map{"metrics": metrics, "count": len(metrics)}) } func (s *Server) getMetricNames(c *fiber.Ctx) error { rows, err := s.db.Query(`SELECT DISTINCT name FROM metrics ORDER BY name`) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var names []string for rows.Next() { var name string if rows.Scan(&name) == nil { names = append(names, name) } } return c.JSON(fiber.Map{"names": names}) } func (s *Server) queryLogs(c *fiber.Ctx) error { service := c.Query("service") level := c.Query("level") query := c.Query("q") from := parseTime(c.Query("from"), time.Now().Add(-1*time.Hour)) to := parseTime(c.Query("to"), time.Now()) limit := parseInt(c.Query("limit"), 100) sql := `SELECT timestamp, service, host, level, message, source, container_id FROM logs WHERE timestamp >= $1 AND timestamp <= $2` args := []any{from, to} argN := 3 if service != "" { sql += ` AND service = $` + strconv.Itoa(argN) args = append(args, service) argN++ } if level != "" { sql += ` AND level = $` + strconv.Itoa(argN) args = append(args, level) argN++ } if query != "" { sql += ` AND message ILIKE $` + strconv.Itoa(argN) args = append(args, "%"+query+"%") argN++ } sql += ` ORDER BY timestamp DESC LIMIT $` + strconv.Itoa(argN) args = append(args, limit) rows, err := s.db.Query(sql, args...) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var logs []LogEntry for rows.Next() { var l LogEntry if err := rows.Scan(&l.Timestamp, &l.Service, &l.Host, &l.Level, &l.Message, &l.Source, &l.ContainerID); err == nil { logs = append(logs, l) } } return c.JSON(fiber.Map{"logs": logs, "count": len(logs)}) } func (s *Server) queryTraces(c *fiber.Ctx) error { service := c.Query("service") from := parseTime(c.Query("from"), time.Now().Add(-1*time.Hour)) to := parseTime(c.Query("to"), time.Now()) limit := parseInt(c.Query("limit"), 20) sql := `SELECT DISTINCT trace_id, service, operation, MIN(start_time), MAX(duration_ns) FROM spans WHERE start_time >= $1 AND start_time <= $2` args := []any{from, to} argN := 3 if service != "" { sql += ` AND service = $` + strconv.Itoa(argN) args = append(args, service) argN++ } sql += ` GROUP BY trace_id, service, operation ORDER BY MIN(start_time) DESC LIMIT $` + strconv.Itoa(argN) args = append(args, limit) rows, err := s.db.Query(sql, args...) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var traces []map[string]any for rows.Next() { var traceID, service, operation string var startTime time.Time var durationNs int64 if err := rows.Scan(&traceID, &service, &operation, &startTime, &durationNs); err == nil { traces = append(traces, map[string]any{ "trace_id": traceID, "service": service, "operation": operation, "start_time": startTime, "duration_ns": durationNs, }) } } return c.JSON(fiber.Map{"traces": traces, "count": len(traces)}) } func (s *Server) getTrace(c *fiber.Ctx) error { traceID := c.Params("traceId") rows, err := s.db.Query(` SELECT trace_id, span_id, parent_span_id, service, operation, start_time, end_time, duration_ns, status_code, status_message, kind FROM spans WHERE trace_id = $1 ORDER BY start_time`, traceID) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var spans []Span for rows.Next() { var sp Span var parentSpanID, statusMsg sql.NullString if err := rows.Scan(&sp.TraceID, &sp.SpanID, &parentSpanID, &sp.Service, &sp.Operation, &sp.StartTime, &sp.EndTime, &sp.DurationNs, &sp.StatusCode, &statusMsg, &sp.Kind); err == nil { sp.ParentSpanID = parentSpanID.String sp.StatusMsg = statusMsg.String spans = append(spans, sp) } } if len(spans) == 0 { return c.Status(404).JSON(fiber.Map{"error": "trace not found"}) } return c.JSON(fiber.Map{ "trace_id": traceID, "spans": spans, "duration_ns": spans[len(spans)-1].EndTime.Sub(spans[0].StartTime).Nanoseconds(), "start_time": spans[0].StartTime, }) } func (s *Server) getServices(c *fiber.Ctx) error { rows, err := s.db.Query(`SELECT DISTINCT service FROM metrics UNION SELECT DISTINCT service FROM spans ORDER BY service`) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var services []string for rows.Next() { var svc string if rows.Scan(&svc) == nil { services = append(services, svc) } } return c.JSON(fiber.Map{"services": services}) } // ───────────────────────────────────────────────────────────── // Agents // ───────────────────────────────────────────────────────────── func (s *Server) getAgents(c *fiber.Ctx) error { rows, err := s.db.Query(`SELECT id, hostname, ip, version, status, last_seen, created_at FROM agents ORDER BY last_seen DESC`) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var agents []Agent for rows.Next() { var a Agent if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Version, &a.Status, &a.LastSeen, &a.CreatedAt); err == nil { agents = append(agents, a) } } return c.JSON(fiber.Map{"agents": agents}) } func (s *Server) registerAgent(c *fiber.Ctx) error { var agent Agent if err := c.BodyParser(&agent); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } agent.ID = uuid.New().String() agent.Status = "active" agent.LastSeen = time.Now() agent.CreatedAt = time.Now() _, err := s.db.Exec(` INSERT INTO agents (id, hostname, ip, version, status, last_seen, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (hostname) DO UPDATE SET ip = $3, version = $4, status = 'active', last_seen = NOW()`, agent.ID, agent.Hostname, agent.IP, agent.Version, agent.Status, agent.LastSeen, agent.CreatedAt) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"status": "registered", "agent": agent}) } // ───────────────────────────────────────────────────────────── // Alerts // ───────────────────────────────────────────────────────────── func (s *Server) getAlerts(c *fiber.Ctx) error { status := c.Query("status") limit := parseInt(c.Query("limit"), 50) sql := `SELECT id, name, severity, service, host, message, status, fired_at, resolved_at FROM alerts` var args []any if status != "" { sql += ` WHERE status = $1` args = append(args, status) } sql += ` ORDER BY fired_at DESC LIMIT ` + strconv.Itoa(limit) rows, err := s.db.Query(sql, args...) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } defer rows.Close() var alerts []Alert for rows.Next() { var a Alert var resolvedAt *time.Time if err := rows.Scan(&a.ID, &a.Name, &a.Severity, &a.Service, &a.Host, &a.Message, &a.Status, &a.FiredAt, &resolvedAt); err == nil { a.ResolvedAt = resolvedAt alerts = append(alerts, a) } } return c.JSON(fiber.Map{"alerts": alerts}) } func (s *Server) createAlert(c *fiber.Ctx) error { var alert Alert if err := c.BodyParser(&alert); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } alert.ID = uuid.New().String() alert.Status = "firing" alert.FiredAt = time.Now() _, err := s.db.Exec(` INSERT INTO alerts (id, name, severity, service, host, message, status, fired_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, alert.ID, alert.Name, alert.Severity, alert.Service, alert.Host, alert.Message, alert.Status, alert.FiredAt) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"status": "created", "alert": alert}) } func (s *Server) resolveAlert(c *fiber.Ctx) error { alertID := c.Params("id") _, err := s.db.Exec(`UPDATE alerts SET status = 'resolved', resolved_at = NOW() WHERE id = $1`, alertID) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"status": "resolved"}) } // ───────────────────────────────────────────────────────────── // Dashboard // ───────────────────────────────────────────────────────────── func (s *Server) getDashboardOverview(c *fiber.Ctx) error { overview := fiber.Map{"timestamp": time.Now()} // Agents var totalAgents, activeAgents int s.db.QueryRow(`SELECT COUNT(*) FROM agents`).Scan(&totalAgents) s.db.QueryRow(`SELECT COUNT(*) FROM agents WHERE status = 'active'`).Scan(&activeAgents) overview["agents"] = fiber.Map{"total": totalAgents, "active": activeAgents} // Alerts var firingAlerts int s.db.QueryRow(`SELECT COUNT(*) FROM alerts WHERE status = 'firing'`).Scan(&firingAlerts) overview["alerts"] = fiber.Map{"firing": firingAlerts} // Services var serviceCount int s.db.QueryRow(`SELECT COUNT(DISTINCT service) FROM metrics`).Scan(&serviceCount) overview["services"] = fiber.Map{"count": serviceCount} return c.JSON(overview) } // ───────────────────────────────────────────────────────────── // Background Jobs // ───────────────────────────────────────────────────────────── func (s *Server) runBackgroundJobs(ctx context.Context) { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: // Mark stale agents s.db.Exec(`UPDATE agents SET status = 'inactive' WHERE last_seen < NOW() - INTERVAL '5 minutes' AND status = 'active'`) } } } // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── func getEnv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func parseTime(s string, def time.Time) time.Time { if s == "" { return def } if t, err := time.Parse(time.RFC3339, s); err == nil { return t } if ts, err := strconv.ParseInt(s, 10, 64); err == nil { return time.Unix(ts, 0) } if d, err := time.ParseDuration(s); err == nil { return time.Now().Add(-d) } return def } func parseInt(s string, def int) int { if s == "" { return def } if v, err := strconv.Atoi(s); err == nil { return v } return def } // agentAuthMiddleware creates middleware that accepts a simple agent key func (s *Server) agentAuthMiddleware() fiber.Handler { agentKey := getEnv("AGENT_KEY", "") return func(c *fiber.Ctx) error { // If no agent key is configured, allow all (for backwards compat) if agentKey == "" { return c.Next() } authHeader := c.Get("Authorization") token := strings.TrimPrefix(authHeader, "Bearer ") // Check agent key if token == agentKey { return c.Next() } // Also check X-Agent-Key header if c.Get("X-Agent-Key") == agentKey { return c.Next() } return c.Status(401).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid agent key", }) } }