feat(runtime-logs): enhance runtime log management and configuration
Добавлены новые возможности для управления файловыми логами в Docker-сервисах: - Обновлены конфигурации для поддержки логов, включая переменные окружения и монтирование директорий. - Документация обновлена для описания новых эндпоинтов и параметров, связанных с логами. - Упрощен доступ к логам через API и интерфейс пользователя. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user