From 57cfe4516f23a845d789f36dd1fd9b243c46b04c Mon Sep 17 00:00:00 2001 From: db123-ai Date: Wed, 10 Jun 2026 00:48:41 +0330 Subject: [PATCH] feat(log): add structured logging handler [LEVEL] [TIME] [FILE:LINE] --- internal/log/handler.go | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 internal/log/handler.go diff --git a/internal/log/handler.go b/internal/log/handler.go new file mode 100644 index 0000000..448c29b --- /dev/null +++ b/internal/log/handler.go @@ -0,0 +1,125 @@ +package log + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "runtime" + "strings" + "sync" +) + +type Handler struct { + mu sync.Mutex + w io.Writer + level slog.Level + attrs []slog.Attr + groups []string +} + +func New(w io.Writer, level slog.Level) *Handler { + return &Handler{w: w, level: level} +} + +func (h *Handler) Enabled(_ context.Context, l slog.Level) bool { + return l >= h.level +} + +func (h *Handler) Handle(_ context.Context, r slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + + level := r.Level.String() + switch r.Level { + case slog.LevelDebug: + level = "DEBUG" + case slog.LevelInfo: + level = "INFO" + case slog.LevelWarn: + level = "WARN" + case slog.LevelError: + level = "ERROR" + } + + _, file, line, ok := runtime.Caller(4) + if !ok { + file = "???" + } + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break + } + } + + t := r.Time.Format("2006-01-02 15:04:05.000") + + var b strings.Builder + b.WriteString(fmt.Sprintf("[%s] [%s] [%s:%d]", level, t, short, line)) + + if len(h.groups) > 0 { + b.WriteString(" [" + strings.Join(h.groups, ".") + "]") + } + if r.Message != "" { + b.WriteString(" " + r.Message) + } + + r.Attrs(func(a slog.Attr) bool { + b.WriteString(fmt.Sprintf(" %s=%v", a.Key, a.Value.Any())) + return true + }) + for _, a := range h.attrs { + b.WriteString(fmt.Sprintf(" %s=%v", a.Key, a.Value.Any())) + } + + b.WriteString("\n") + _, err := h.w.Write([]byte(b.String())) + return err +} + +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + h2 := h.clone() + h2.attrs = append(h2.attrs, attrs...) + return h2 +} + +func (h *Handler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + h2 := h.clone() + h2.groups = append(h2.groups, name) + return h2 +} + +func (h *Handler) clone() *Handler { + return &Handler{ + w: h.w, + level: h.level, + attrs: append([]slog.Attr{}, h.attrs...), + groups: append([]string{}, h.groups...), + } +} + +func Init(level string) { + var l slog.Level + switch strings.ToUpper(level) { + case "DEBUG": + l = slog.LevelDebug + case "INFO": + l = slog.LevelInfo + case "WARN": + l = slog.LevelWarn + case "ERROR": + l = slog.LevelError + default: + l = slog.LevelInfo + } + slog.SetDefault(slog.New(New(os.Stderr, l))) +}