CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 56s
CI / bird2 (push) Successful in 14s
CI / release (push) Successful in 20s
Added new endpoints for estimating and executing runtime log auto-cleanup based on tenant settings. Introduced configuration options for auto-cleanup policies, including scheduling and file size limits. Updated the API documentation and UI components to reflect these changes, improving user interaction with runtime log management. Enhanced error handling and added new UI elements for better visibility of audit logs and cleanup actions.
255 lines
7.7 KiB
Go
255 lines
7.7 KiB
Go
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.requireAtLeast(w, a, "viewer") || !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.requireAtLeast(w, a, "viewer") || !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.requireAtLeast(w, a, "operator") || !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.requireAtLeast(w, a, "operator") || !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.requireAtLeast(w, a, "operator") || !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.requireAtLeast(w, a, "viewer") {
|
|
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
|
|
}
|