package logging import ( "context" "fmt" "io" "log/slog" "os" "strings" "sync" "time" ) type Entry struct { Timestamp time.Time `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` Fields map[string]any `json:"fields,omitempty"` } type RecentStore struct { mu sync.RWMutex entries []Entry next int size int capacity int } func NewRecentStore(capacity int) *RecentStore { if capacity <= 0 { capacity = 200 } return &RecentStore{ entries: make([]Entry, capacity), capacity: capacity, } } func (s *RecentStore) Add(entry Entry) { if s == nil { return } s.mu.Lock() defer s.mu.Unlock() s.entries[s.next] = entry s.next = (s.next + 1) % s.capacity if s.size < s.capacity { s.size++ } } func (s *RecentStore) List(limit int) []Entry { if s == nil { return nil } s.mu.RLock() defer s.mu.RUnlock() if s.size == 0 { return nil } if limit <= 0 || limit > s.size { limit = s.size } out := make([]Entry, 0, limit) for i := 0; i < limit; i++ { idx := (s.next - 1 - i + s.capacity) % s.capacity out = append(out, s.entries[idx]) } return out } type recentHandler struct { store *RecentStore level slog.Leveler attrs []slog.Attr group []string } func NewRecentHandler(store *RecentStore, level slog.Leveler) slog.Handler { if level == nil { level = slog.LevelInfo } return &recentHandler{store: store, level: level} } func (h *recentHandler) Enabled(_ context.Context, level slog.Level) bool { if h == nil || h.level == nil { return level >= slog.LevelInfo } return level >= h.level.Level() } func (h *recentHandler) Handle(_ context.Context, r slog.Record) error { if h == nil || h.store == nil { return nil } fields := map[string]any{} for _, attr := range h.attrs { addField(fields, h.group, attr) } r.Attrs(func(attr slog.Attr) bool { addField(fields, h.group, attr) return true }) if len(fields) == 0 { fields = nil } h.store.Add(Entry{ Timestamp: r.Time.UTC(), Level: r.Level.String(), Message: strings.TrimSpace(r.Message), Fields: fields, }) return nil } func (h *recentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { next := &recentHandler{ store: h.store, level: h.level, group: append([]string(nil), h.group...), } next.attrs = append(append([]slog.Attr(nil), h.attrs...), attrs...) return next } func (h *recentHandler) WithGroup(name string) slog.Handler { next := &recentHandler{ store: h.store, level: h.level, attrs: append([]slog.Attr(nil), h.attrs...), group: append([]string(nil), h.group...), } if trimmed := strings.TrimSpace(name); trimmed != "" { next.group = append(next.group, trimmed) } return next } func addField(fields map[string]any, groups []string, attr slog.Attr) { if attr.Equal(slog.Attr{}) { return } key := strings.TrimSpace(attr.Key) if key == "" { return } prefix := strings.Join(groups, ".") if prefix != "" { key = prefix + "." + key } val := attr.Value.Resolve() if val.Kind() == slog.KindGroup { for _, nested := range val.Group() { addField(fields, append(groups, strings.TrimSpace(attr.Key)), nested) } return } fields[key] = slogValueToAny(val) } func slogValueToAny(v slog.Value) any { switch v.Kind() { case slog.KindString: return v.String() case slog.KindBool: return v.Bool() case slog.KindInt64: return v.Int64() case slog.KindUint64: return v.Uint64() case slog.KindFloat64: return v.Float64() case slog.KindDuration: return v.Duration().Milliseconds() case slog.KindTime: return v.Time().UTC().Format(time.RFC3339Nano) case slog.KindAny: return v.Any() default: return fmt.Sprint(v.Any()) } } type teeHandler struct { handlers []slog.Handler } func newTeeHandler(handlers ...slog.Handler) slog.Handler { nonNil := make([]slog.Handler, 0, len(handlers)) for _, h := range handlers { if h != nil { nonNil = append(nonNil, h) } } return &teeHandler{handlers: nonNil} } func (h *teeHandler) Enabled(ctx context.Context, level slog.Level) bool { for _, handler := range h.handlers { if handler.Enabled(ctx, level) { return true } } return false } func (h *teeHandler) Handle(ctx context.Context, r slog.Record) error { for _, handler := range h.handlers { if !handler.Enabled(ctx, r.Level) { continue } if err := handler.Handle(ctx, r.Clone()); err != nil { return err } } return nil } func (h *teeHandler) WithAttrs(attrs []slog.Attr) slog.Handler { next := make([]slog.Handler, 0, len(h.handlers)) for _, handler := range h.handlers { next = append(next, handler.WithAttrs(attrs)) } return &teeHandler{handlers: next} } func (h *teeHandler) WithGroup(name string) slog.Handler { next := make([]slog.Handler, 0, len(h.handlers)) for _, handler := range h.handlers { next = append(next, handler.WithGroup(name)) } return &teeHandler{handlers: next} } type SetupResult struct { Logger *slog.Logger Recent *RecentStore Close func() error } func Setup(levelRaw string) SetupResult { level := parseLevel(levelRaw) handlerOptions := &slog.HandlerOptions{Level: level} recent := NewRecentStore(400) stdoutHandler := slog.NewJSONHandler(os.Stdout, handlerOptions) handlers := []slog.Handler{stdoutHandler, NewRecentHandler(recent, level)} closers := make([]io.Closer, 0, 1) if path := strings.TrimSpace(os.Getenv("LOG_FILE")); path != "" { if file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err == nil { handlers = append(handlers, slog.NewJSONHandler(file, handlerOptions)) closers = append(closers, file) } } logger := slog.New(newTeeHandler(handlers...)) return SetupResult{ Logger: logger, Recent: recent, Close: func() error { for _, closer := range closers { _ = closer.Close() } return nil }, } } func parseLevel(raw string) slog.Level { switch strings.ToLower(strings.TrimSpace(raw)) { case "trace": return slog.LevelDebug - 4 case "debug": return slog.LevelDebug case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }