package log import ( "context" "fmt" "log/slog" "sync" "time" "git.lastassault.de/speatzle/morffix/types" slogmulti "github.com/samber/slog-multi" ) func GetTaskLogger(t *types.Task) *slog.Logger { return slog.New( slogmulti.Fanout( slog.Default().Handler(), New(t, &Options{slog.LevelDebug}), ), ) } // groupOrAttrs holds either a group name or a list of slog.Attrs. type groupOrAttrs struct { group string // group name if non-empty attrs []slog.Attr // attrs if non-empty } type TaskHandler struct { opts Options // TODO: state for WithGroup and WithAttrs mu *sync.Mutex t *types.Task goas []groupOrAttrs } type Options struct { // Level reports the minimum level to log. // Levels with lower levels are discarded. // If nil, the Handler uses [slog.LevelInfo]. Level slog.Leveler } func New(t *types.Task, opts *Options) *TaskHandler { h := &TaskHandler{t: t, mu: &sync.Mutex{}} if opts != nil { h.opts = *opts } if h.opts.Level == nil { h.opts.Level = slog.LevelInfo } return h } func (h *TaskHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.opts.Level.Level() } func (h *TaskHandler) Handle(ctx context.Context, r slog.Record) error { buf := make([]byte, 0, 1024) if !r.Time.IsZero() { buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time)) } buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message)) // Handle state from WithGroup and WithAttrs. goas := h.goas if r.NumAttrs() == 0 { // If the record has no Attrs, remove groups at the end of the list; they are empty. for len(goas) > 0 && goas[len(goas)-1].group != "" { goas = goas[:len(goas)-1] } } for _, goa := range goas { if goa.group == "" { for _, a := range goa.attrs { buf = h.appendAttr(buf, a) } } } r.Attrs(func(a slog.Attr) bool { buf = h.appendAttr(buf, a) return true }) h.mu.Lock() defer h.mu.Unlock() h.t.Log = append(h.t.Log, string(buf)) return nil } func (h *TaskHandler) appendAttr(buf []byte, a slog.Attr) []byte { // Resolve the Attr's value before doing anything else. a.Value = a.Value.Resolve() // Ignore empty Attrs. if a.Equal(slog.Attr{}) { return buf } switch a.Value.Kind() { case slog.KindString: // Quote string values, to make them easy to parse. buf = fmt.Appendf(buf, "%s: %q\n", a.Key, a.Value.String()) case slog.KindTime: // Write times in a standard way, without the monotonic time. buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value.Time().Format(time.DateTime)) case slog.KindGroup: attrs := a.Value.Group() // Ignore empty groups. if len(attrs) == 0 { return buf } // If the key is non-empty, write it out and indent the rest of the attrs. // Otherwise, inline the attrs. if a.Key != "" { buf = fmt.Appendf(buf, "%s:\n", a.Key) } for _, ga := range attrs { buf = h.appendAttr(buf, ga) } default: buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value) } return buf } func (h *TaskHandler) withGroupOrAttrs(goa groupOrAttrs) *TaskHandler { h2 := *h h2.goas = make([]groupOrAttrs, len(h.goas)+1) copy(h2.goas, h.goas) h2.goas[len(h2.goas)-1] = goa return &h2 } func (h *TaskHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return h.withGroupOrAttrs(groupOrAttrs{group: name}) } func (h *TaskHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) }