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 }