Добавлены новые возможности для управления файловыми логами в Docker-сервисах: - Обновлены конфигурации для поддержки логов, включая переменные окружения и монтирование директорий. - Документация обновлена для описания новых эндпоинтов и параметров, связанных с логами. - Упрощен доступ к логам через API и интерфейс пользователя. Co-authored-by: Cursor <[email protected]>
180 lines
5.5 KiB
Go
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
|
|
}
|