package service import ( "bytes" "net/http" "strconv" "strings" "sync" "time" ) const defaultRuntimeLogCapacity = 500 type RuntimeLog struct { ID int64 `json:"id"` CreatedAt string `json:"createdAt"` Level string `json:"level"` Message string `json:"message"` } type RuntimeLogResult struct { Items []RuntimeLog `json:"items"` Total int `json:"total"` } type RuntimeLogStore struct { mu sync.Mutex logs []RuntimeLog partial []byte nextID int64 capacity int now func() time.Time } func NewRuntimeLogStore(capacities ...int) *RuntimeLogStore { capacity := defaultRuntimeLogCapacity if len(capacities) > 0 { capacity = capacities[0] } return newRuntimeLogStore(capacity, time.Now) } func newRuntimeLogStore(capacity int, now func() time.Time) *RuntimeLogStore { if capacity < 1 { capacity = 1 } return &RuntimeLogStore{ logs: make([]RuntimeLog, 0, capacity), capacity: capacity, now: now, } } func (s *RuntimeLogStore) Write(p []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() written := len(p) s.partial = append(s.partial, p...) for { index := bytes.IndexByte(s.partial, '\n') if index < 0 { break } s.addLineLocked(string(bytes.TrimSuffix(s.partial[:index], []byte{'\r'}))) s.partial = append(s.partial[:0], s.partial[index+1:]...) } return written, nil } func (s *RuntimeLogStore) Latest(limit int) RuntimeLogResult { s.mu.Lock() defer s.mu.Unlock() if limit < 1 || limit > s.capacity { limit = s.capacity } start := len(s.logs) - limit if start < 0 { start = 0 } items := make([]RuntimeLog, len(s.logs)-start) for i := range items { items[i] = s.logs[len(s.logs)-1-i] } return RuntimeLogResult{Items: items, Total: len(s.logs)} } func (s *RuntimeLogStore) addLineLocked(message string) { message = strings.TrimSpace(message) if message == "" { return } s.nextID++ item := RuntimeLog{ ID: s.nextID, CreatedAt: s.now().Format("2006-01-02 15:04:05"), Level: runtimeLogLevel(message), Message: message, } if len(s.logs) == s.capacity { copy(s.logs, s.logs[1:]) s.logs[len(s.logs)-1] = item return } s.logs = append(s.logs, item) } func runtimeLogLevel(message string) string { upper := strings.ToUpper(message) switch { case strings.Contains(upper, "FATAL"), strings.Contains(upper, "PANIC"), strings.Contains(upper, "ERROR"), strings.Contains(upper, "[ERRO]"): return "ERROR" case strings.Contains(upper, "WARN"), strings.Contains(upper, "[WARN]"): return "WARN" case strings.Contains(upper, "DEBUG"), strings.Contains(upper, "[DEBU]"): return "DEBUG" default: return "INFO" } } func (s *Service) adminRuntimeLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) writeJSON(w, http.StatusOK, s.runtimeLogs.Latest(limit)) }