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.
This commit is contained in:
parent
2fd56d0f36
commit
715d680f97
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"coda/internal/models"
|
"coda/internal/models"
|
||||||
@ -74,17 +75,26 @@ func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
|
|||||||
|
|
||||||
entry := models.LogEntry{
|
entry := models.LogEntry{
|
||||||
Level: r.Level.String(),
|
Level: r.Level.String(),
|
||||||
Message: r.Message,
|
|
||||||
Timestamp: r.Time,
|
Timestamp: r.Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var msg strings.Builder
|
||||||
|
msg.WriteString(r.Message)
|
||||||
|
|
||||||
r2.Attrs(func(a slog.Attr) bool {
|
r2.Attrs(func(a slog.Attr) bool {
|
||||||
if a.Key == "job_id" && a.Value.Kind() == slog.KindString {
|
if a.Key == "job_id" && a.Value.Kind() == slog.KindString {
|
||||||
entry.JobID = a.Value.String()
|
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
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
entry.Message = msg.String()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case h.ch <- entry:
|
case h.ch <- entry:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"coda/internal/models"
|
"coda/internal/models"
|
||||||
@ -92,6 +93,37 @@ func TestHandlerRespectsLevel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlerIncludesAttrsInMessage(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
|
||||||
|
h := New(st.Logs, Options{Level: slog.LevelInfo, Stdout: io.Discard})
|
||||||
|
log := slog.New(h)
|
||||||
|
|
||||||
|
log.Warn("path outside music path", "path", "/mnt/media/Music/Artist", "music_path", "/music")
|
||||||
|
|
||||||
|
if err := h.Close(); err != nil {
|
||||||
|
t.Fatalf("Close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := st.Logs.List(context.Background(), store.ListLogsParams{Limit: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List: %v", err)
|
||||||
|
}
|
||||||
|
if len(logs) != 1 {
|
||||||
|
t.Fatalf("expected 1 log, got %d", len(logs))
|
||||||
|
}
|
||||||
|
msg := logs[0].Message
|
||||||
|
if !strings.Contains(msg, "path outside music path") {
|
||||||
|
t.Errorf("message missing base text: %q", msg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, "path=/mnt/media/Music/Artist") {
|
||||||
|
t.Errorf("message missing path attr: %q", msg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, "music_path=/music") {
|
||||||
|
t.Errorf("message missing music_path attr: %q", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type failingWriter struct{}
|
type failingWriter struct{}
|
||||||
|
|
||||||
func (failingWriter) Insert(ctx context.Context, e *models.LogEntry) error {
|
func (failingWriter) Insert(ctx context.Context, e *models.LogEntry) error {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user