Files
EvoBGP/internal/httpapi/routes_runtime_logs.go
T
DenozordecandCursor 0c5502b5bb feat(runtime-logs): enhance runtime log management and configuration
Добавлены новые возможности для управления файловыми логами в Docker-сервисах:
- Обновлены конфигурации для поддержки логов, включая переменные окружения и монтирование директорий.
- Документация обновлена для описания новых эндпоинтов и параметров, связанных с логами.
- Упрощен доступ к логам через API и интерфейс пользователя.

Co-authored-by: Cursor <[email protected]>
2026-06-12 19:18:50 +07:00

180 lines
5.5 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)
}
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) 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
}