package logging import ( "context" "encoding/json" "io" "log/slog" "os" "path/filepath" "sync" "time" ) type Closer func() error type jsonLineHandler struct { mu *sync.Mutex out io.Writer attrs []slog.Attr } func New(dataDir string) (*slog.Logger, Closer, error) { logDir := filepath.Join(dataDir, "logs") if err := os.MkdirAll(logDir, 0o755); err != nil { return nil, nil, err } path := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log") file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return nil, nil, err } handler := &jsonLineHandler{ mu: &sync.Mutex{}, out: io.MultiWriter(os.Stdout, file), } return slog.New(handler), file.Close, nil } func (h *jsonLineHandler) Enabled(context.Context, slog.Level) bool { return true } func (h *jsonLineHandler) Handle(_ context.Context, record slog.Record) error { entry := map[string]any{ "date": record.Time.Format("2006-01-02"), "time": record.Time.Format("15:04:05"), "source": "app", "code": 0, "severity": severity(record.Level), "log": record.Message, } for _, attr := range h.attrs { applyAttr(entry, attr) } record.Attrs(func(attr slog.Attr) bool { applyAttr(entry, attr) return true }) line, err := json.Marshal(entry) if err != nil { return err } h.mu.Lock() defer h.mu.Unlock() _, err = h.out.Write(append(line, '\n')) return err } func (h *jsonLineHandler) WithAttrs(attrs []slog.Attr) slog.Handler { next := &jsonLineHandler{ mu: h.mu, out: h.out, attrs: append([]slog.Attr{}, h.attrs...), } next.attrs = append(next.attrs, attrs...) return next } func (h *jsonLineHandler) WithGroup(string) slog.Handler { return h } func applyAttr(entry map[string]any, attr slog.Attr) { attr.Value = attr.Value.Resolve() entry[attr.Key] = attr.Value.Any() } func severity(level slog.Level) string { switch { case level >= slog.LevelError: return "high" case level >= slog.LevelWarn: return "medium" case level <= slog.LevelDebug: return "low" default: return "info" } }