package httpapi import ( "errors" "net/http" "strconv" "evobgp/internal/runtimelogs" "evobgp/internal/store" ) func (s *Server) registerRuntimeLogsRoutes(m *http.ServeMux) { m.HandleFunc("GET /runtime-logs/files", s.handleListRuntimeLogFiles) m.HandleFunc("GET /runtime-logs/files/{filename}", s.handleGetRuntimeLogTail) m.HandleFunc("DELETE /runtime-logs/files/{filename}", s.handleDeleteRuntimeLogFile) m.HandleFunc("GET /runtime-logs/cleanup-audit", s.handleListRuntimeLogCleanupAudit) m.HandleFunc("GET /runtime-logs/auto-estimate", s.handleRuntimeLogAutoEstimate) m.HandleFunc("POST /runtime-logs/auto-run", s.handleRuntimeLogAutoRun) } func (s *Server) requireRuntimeLogs(w http.ResponseWriter) bool { if s.runtimeLogs != nil && s.runtimeLogs.Available() { return true } writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "runtime_logs_unavailable") return false } func writeRuntimeLogsErr(w http.ResponseWriter, operation string, err error) { switch { case errors.Is(err, runtimelogs.ErrUnavailable): writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "runtime_logs_unavailable") case errors.Is(err, runtimelogs.ErrNotFound): writeProblem(w, http.StatusNotFound, "Not Found", notFoundDetail) case errors.Is(err, runtimelogs.ErrFileTooLarge): writeProblem(w, http.StatusRequestEntityTooLarge, "Payload Too Large", "file exceeds maximum size for cleanup") case errors.Is(err, runtimelogs.ErrInvalidFilename), errors.Is(err, runtimelogs.ErrNotAFile): writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", invalidInputDetail) default: writeInternalError(w, operation, err) } } func runtimeLogFileJSON(f store.RuntimeLogFile) map[string]any { return map[string]any{ "name": f.Name, "size_bytes": f.SizeBytes, "modified_at": f.ModifiedAt.UTC().Format("2006-01-02T15:04:05Z"), } } func runtimeLogCleanupAuditJSON(row *store.RuntimeLogCleanupAudit) map[string]any { out := map[string]any{ "id": row.ID, "tenant_id": row.TenantID, "actor_prefix": row.ActorPrefix, "filename": row.Filename, "action": row.Action, "size_before": row.SizeBefore, "created_at": row.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"), } if row.SizeAfter != nil { out["size_after"] = *row.SizeAfter } if row.Detail != nil { out["detail"] = row.Detail } return out } func (s *Server) handleListRuntimeLogFiles(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requireRuntimeLogs(w) { return } items, err := s.runtimeLogs.ListFiles() if err != nil { writeRuntimeLogsErr(w, "runtime_logs_list", err) return } out := make([]map[string]any, 0, len(items)) for _, f := range items { out = append(out, runtimeLogFileJSON(f)) } writeJSON(w, http.StatusOK, map[string]any{"items": out}) } func (s *Server) handleGetRuntimeLogTail(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requireRuntimeLogs(w) { return } filename := r.PathValue("filename") opts := runtimelogs.TailOptions{ Lines: parsePositiveIntQuery(r, "lines", runtimelogs.DefaultTailLines, runtimelogs.MaxTailLines), Bytes: parsePositiveIntQuery(r, "bytes", 0, runtimelogs.MaxTailBytes), Grep: r.URL.Query().Get("grep"), } tail, err := s.runtimeLogs.Tail(filename, opts) if err != nil { writeRuntimeLogsErr(w, "runtime_logs_tail", err) return } writeJSON(w, http.StatusOK, map[string]any{ "filename": tail.Filename, "content": tail.Content, "truncated": tail.Truncated, "lines_returned": tail.LinesReturned, }) } func (s *Server) handleDeleteRuntimeLogFile(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) { return } filename := r.PathValue("filename") mode := r.URL.Query().Get("mode") if mode == "" { mode = store.RuntimeLogCleanupTruncate } if !store.ValidRuntimeLogCleanupAction(mode) { writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", invalidInputDetail) return } sizeBefore, sizeAfter, err := s.runtimeLogs.Cleanup(filename, mode) if err != nil { writeRuntimeLogsErr(w, "runtime_logs_cleanup", err) return } auditID, err := s.store.AppendRuntimeLogCleanupAudit( a.TenantID, actorPrefix(a), filename, mode, sizeBefore, sizeAfter, nil) if err != nil { writeInternalError(w, "runtime_logs_cleanup_audit", err) return } out := map[string]any{ "audit_id": auditID, "filename": filename, "action": mode, "size_before": sizeBefore, } if sizeAfter != nil { out["size_after"] = *sizeAfter } writeJSON(w, http.StatusOK, out) } func (s *Server) runtimeLogAutoPolicy(w http.ResponseWriter, r *http.Request, tenantID string) (runtimelogs.AutoPolicy, bool) { settings, err := s.store.ListGlobalSettings(tenantID) if err != nil { writeInternalError(w, "runtime_logs_policy", err) return runtimelogs.AutoPolicy{}, false } return runtimelogs.PolicyFromSettings(settings), true } func (s *Server) handleRuntimeLogAutoEstimate(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) { return } policy, ok := s.runtimeLogAutoPolicy(w, r, a.TenantID) if !ok { return } items, err := runtimelogs.EstimateAutoCleanup(s.runtimeLogs, policy) if err != nil { writeRuntimeLogsErr(w, "runtime_logs_auto_estimate", err) return } out := make([]map[string]any, 0, len(items)) var wouldCount int for _, it := range items { if it.WouldCleanup { wouldCount++ } row := map[string]any{ "filename": it.Filename, "size_bytes": it.SizeBytes, "would_cleanup": it.WouldCleanup, } if it.SkipReason != "" { row["skip_reason"] = it.SkipReason } out = append(out, row) } writeJSON(w, http.StatusOK, map[string]any{ "policy": map[string]any{ "enabled": policy.Enabled, "max_file_bytes": policy.MaxFileBytes, "schedule": policy.Schedule, "mode": policy.Mode, }, "items": out, "would_count": wouldCount, }) } func (s *Server) handleRuntimeLogAutoRun(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) { return } policy, ok := s.runtimeLogAutoPolicy(w, r, a.TenantID) if !ok { return } dryRun := r.URL.Query().Get("dry_run") == "true" result, err := runtimelogs.RunAutoCleanup(r.Context(), runtimelogs.AutoCleanupDeps{ Service: s.runtimeLogs, Store: s.store, TenantID: a.TenantID, }, policy, dryRun, "manual") if err != nil { writeRuntimeLogsErr(w, "runtime_logs_auto_run", err) return } writeJSON(w, http.StatusOK, result) } func (s *Server) handleListRuntimeLogCleanupAudit(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") { return } cursor := r.URL.Query().Get("cursor") limit := parseLimitQuery(r, 20, 100) items, next, hasMore, err := s.store.ListRuntimeLogCleanupAudit(a.TenantID, cursor, limit) if err != nil { writeInternalError(w, "runtime_logs_cleanup_audit_list", err) return } out := make([]map[string]any, 0, len(items)) for _, row := range items { out = append(out, runtimeLogCleanupAuditJSON(row)) } writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore}) } func parsePositiveIntQuery(r *http.Request, key string, def, max int) int { v := r.URL.Query().Get(key) if v == "" { return def } n, err := strconv.Atoi(v) if err != nil || n <= 0 { return def } if max > 0 && n > max { return max } return n }