coda/internal/logger/logger.go
Rodrigo Verdiani 715d680f97 feat(logger): include attributes in persisted log message
Store attributes as key=value pairs in the SQLite log message
so they are visible in the web UI. job_id is extracted into the
dedicated column and excluded from the message text.
2026-07-06 22:58:28 -03:00

138 lines
2.5 KiB
Go

package logger
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
"sync"
"coda/internal/models"
)
type LogWriter interface {
Insert(ctx context.Context, e *models.LogEntry) error
}
type Options struct {
Level slog.Level
Stdout io.Writer
}
type Handler struct {
stdout slog.Handler
repo LogWriter
ch chan models.LogEntry
wg *sync.WaitGroup
attrs []slog.Attr
group string
}
const channelBufferSize = 1024
func New(repo LogWriter, opts Options) *Handler {
if opts.Stdout == nil {
opts.Stdout = os.Stdout
}
h := &Handler{
stdout: slog.NewTextHandler(opts.Stdout, &slog.HandlerOptions{Level: opts.Level}),
repo: repo,
ch: make(chan models.LogEntry, channelBufferSize),
wg: &sync.WaitGroup{},
}
h.wg.Add(1)
go h.writer()
return h
}
func (h *Handler) writer() {
defer h.wg.Done()
for entry := range h.ch {
if err := h.repo.Insert(context.Background(), &entry); err != nil {
fmt.Fprintf(os.Stderr, "logger: failed to insert log: %v\n", err)
}
}
}
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
return h.stdout.Enabled(ctx, level)
}
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
if err := h.stdout.Handle(ctx, r); err != nil {
return err
}
r2 := r.Clone()
if len(h.attrs) > 0 {
r2.AddAttrs(h.attrs...)
}
entry := models.LogEntry{
Level: r.Level.String(),
Timestamp: r.Time,
}
var msg strings.Builder
msg.WriteString(r.Message)
r2.Attrs(func(a slog.Attr) bool {
if a.Key == "job_id" && a.Value.Kind() == slog.KindString {
entry.JobID = a.Value.String()
return true
}
msg.WriteByte(' ')
msg.WriteString(a.Key)
msg.WriteByte('=')
msg.WriteString(fmt.Sprintf("%v", a.Value.Any()))
return true
})
entry.Message = msg.String()
select {
case h.ch <- entry:
default:
fmt.Fprintf(os.Stderr, "logger: channel full, dropping log: %s\n", r.Message)
}
return nil
}
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs))
copy(newAttrs, h.attrs)
copy(newAttrs[len(h.attrs):], attrs)
return &Handler{
stdout: h.stdout.WithAttrs(attrs),
repo: h.repo,
ch: h.ch,
wg: h.wg,
attrs: newAttrs,
group: h.group,
}
}
func (h *Handler) WithGroup(name string) slog.Handler {
return &Handler{
stdout: h.stdout.WithGroup(name),
repo: h.repo,
ch: h.ch,
wg: h.wg,
attrs: h.attrs,
group: h.group,
}
}
func (h *Handler) Close() error {
close(h.ch)
h.wg.Wait()
return nil
}