strimertul/log/setup.go

147 lines
3.1 KiB
Go

package log
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"math/rand"
"os"
"time"
"git.sr.ht/~ashkeel/containers/sync"
slogmulti "github.com/samber/slog-multi"
"gopkg.in/natefinch/lumberjack.v2"
)
const (
History = 50
Filename = "strimertul.log"
PanicFilename = "strimertul-panic.log"
)
var (
LastLogs = sync.NewSlice[Entry]()
IncomingLogs = make(chan Entry, 100)
)
func Init(level slog.Level) {
logStorage := NewLogStorage(level)
fileLogger := &lumberjack.Logger{
Filename: Filename,
MaxSize: 20,
MaxBackups: 3,
MaxAge: 28,
}
consoleHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
})
fileHandler := slog.NewJSONHandler(fileLogger, &slog.HandlerOptions{
AddSource: true,
Level: level,
})
logger := slog.New(slogmulti.Fanout(consoleHandler, fileHandler, logStorage))
slog.SetDefault(logger)
}
type Entry struct {
ID string `json:"id"`
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
Data string `json:"data"`
}
type Storage struct {
minLevel slog.Level
attrs []slog.Attr
group string
}
func (core *Storage) Enabled(_ context.Context, level slog.Level) bool {
return level >= core.minLevel
}
func (core *Storage) Handle(_ context.Context, record slog.Record) error {
attributes := flatAttributeMap(record)
attrJSON, _ := json.Marshal(attributes)
// Generate unique log ID
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
logEntry := Entry{
ID: id,
Time: record.Time.Format(time.RFC3339),
Level: record.Level.String(),
Message: record.Message,
Data: string(attrJSON),
}
LastLogs.Push(logEntry)
if LastLogs.Size() > History {
LastLogs.Splice(0, 1)
}
IncomingLogs <- logEntry
return nil
}
func flatAttributeMap(record slog.Record) map[string]any {
attributes := map[string]any{}
flatAttributes := func(attr slog.Attr) bool {
type attrToParse struct {
Prefix string
Attr slog.Attr
}
remaining := []attrToParse{{"", attr}}
for len(remaining) > 0 {
var current attrToParse
current, remaining = remaining[0], remaining[1:]
switch current.Attr.Value.Kind() {
case slog.KindGroup:
for _, subAttr := range current.Attr.Value.Group() {
remaining = append(remaining, attrToParse{
Prefix: current.Attr.Key + ".",
Attr: subAttr,
})
}
default:
attributes[current.Prefix+current.Attr.Key] = current.Attr.Value.Any()
}
}
return true
}
record.Attrs(flatAttributes)
return attributes
}
func (core *Storage) WithAttrs(attrs []slog.Attr) slog.Handler {
return &Storage{
minLevel: core.minLevel,
attrs: append(core.attrs, attrs...),
group: core.group,
}
}
func (core *Storage) WithGroup(name string) slog.Handler {
return &Storage{
minLevel: core.minLevel,
attrs: core.attrs,
group: name,
}
}
func NewLogStorage(level slog.Level) *Storage {
return &Storage{
minLevel: level,
}
}
func ParseLogFields(data map[string]any) []any {
fields := []any{}
for k, v := range data {
fields = append(fields, k, v)
}
return fields
}