|
- 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() SetupResult {
- recent := NewRecentStore(400)
- stdoutHandler := slog.NewJSONHandler(os.Stdout, nil)
- handlers := []slog.Handler{stdoutHandler, NewRecentHandler(recent, slog.LevelInfo)}
-
- 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, nil))
- 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
- },
- }
- }
|