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.
138 lines
2.5 KiB
Go
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
|
|
}
|