Files
EvoBGP/internal/httpapi/routes_runtime_logs.go
DenozordecandCursor 4d83b8d673
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
feat(auth): integrate portal JWT for enhanced authentication and authorization
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 23:23:52 +07:00

255 lines
7.8 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.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
}