diff options
Diffstat (limited to 'internal/logging')
-rw-r--r-- | internal/logging/context.go | 39 | ||||
-rw-r--r-- | internal/logging/logging.go | 92 | ||||
-rw-r--r-- | internal/logging/trace.go | 61 |
3 files changed, 192 insertions, 0 deletions
diff --git a/internal/logging/context.go b/internal/logging/context.go new file mode 100644 index 0000000..29bad62 --- /dev/null +++ b/internal/logging/context.go @@ -0,0 +1,39 @@ +package logging + +import ( + "context" + "log/slog" +) + +type ctxTracker struct { + ctxKey interface{} + logKey string + next slog.Handler +} + +func (h ctxTracker) Handle(ctx context.Context, r slog.Record) error { + if v := ctx.Value(h.ctxKey); v != nil { + r.AddAttrs(slog.Any(h.logKey, v)) + } + return h.next.Handle(ctx, r) +} + +func (h ctxTracker) Enabled(ctx context.Context, lvl slog.Level) bool { + return h.next.Enabled(ctx, lvl) +} + +func (h ctxTracker) WithAttrs(attrs []slog.Attr) slog.Handler { + return h.next.WithAttrs(attrs) +} + +func (h ctxTracker) WithGroup(name string) slog.Handler { + return h.next.WithGroup(name) +} + +func withTrackedContext(current slog.Handler, ctxKey interface{}, logKey string) *ctxTracker { + return &ctxTracker{ + ctxKey: ctxKey, + logKey: logKey, + next: current, + } +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 0000000..91a9734 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,92 @@ +package logging + +import ( + "errors" + "io" + "log/slog" + "os" + "strings" + "time" + + "github.com/ChausseBenjamin/rafta/internal/util" + "github.com/charmbracelet/log" +) + +const ( + ErrKey = "error_message" +) + +var ( + ErrInvalidLevel = errors.New("invalid log level") + ErrInvalidFormat = errors.New("invalid log format") +) + +func Setup(lvlStr, fmtStr, outStr string) error { + output, outputErr := setOutput(outStr) + format, formatErr := setFormat(fmtStr) + level, levelErr := setLevel(lvlStr) + + prefixStr := "" + if format != log.JSONFormatter { + prefixStr = "Rafta 🚢" + } + + var h slog.Handler = log.NewWithOptions( + output, + log.Options{ + TimeFormat: time.DateTime, + Prefix: prefixStr, + Level: level, + ReportCaller: true, + Formatter: format, + }, + ) + + h = withTrackedContext(h, util.ReqIDKey, "request_id") + h = withStackTrace(h) + slog.SetDefault(slog.New(h)) + return errors.Join(outputErr, formatErr, levelErr) +} + +func setLevel(target string) (log.Level, error) { + for _, l := range []struct { + prefix string + level log.Level + }{ + {"deb", log.DebugLevel}, + {"inf", log.InfoLevel}, + {"warn", log.WarnLevel}, + {"err", log.ErrorLevel}, + } { + if strings.HasPrefix(strings.ToLower(target), l.prefix) { + return l.level, nil + } + } + return log.InfoLevel, ErrInvalidLevel +} + +func setFormat(f string) (log.Formatter, error) { + switch f { + case "plain", "text": + return log.TextFormatter, nil + case "json", "structured": + return log.JSONFormatter, nil + } + return log.TextFormatter, ErrInvalidFormat +} + +func setOutput(path string) (io.Writer, error) { + switch path { + case "stdout": + return os.Stdout, nil + case "stderr": + return os.Stderr, nil + default: + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return os.Stdout, err + } else { + return f, nil + } + } +} diff --git a/internal/logging/trace.go b/internal/logging/trace.go new file mode 100644 index 0000000..8900727 --- /dev/null +++ b/internal/logging/trace.go @@ -0,0 +1,61 @@ +package logging + +import ( + "context" + "log/slog" + "runtime" + "strconv" + "strings" +) + +const ( + prgCount = 20 + defSkip = 6 +) + +type stackTracer struct { + h slog.Handler + nSkip int +} + +func (h stackTracer) Enabled(ctx context.Context, lvl slog.Level) bool { + return h.h.Enabled(ctx, lvl) +} + +func (h stackTracer) WithAttrs(attrs []slog.Attr) slog.Handler { + return h.h.WithAttrs(attrs) +} + +func (h stackTracer) WithGroup(name string) slog.Handler { + return h.h.WithGroup(name) +} + +func (h stackTracer) Handle(ctx context.Context, r slog.Record) error { + if r.Level < slog.LevelError { + return h.h.Handle(ctx, r) + } + + trace := h.GetTrace() + r.AddAttrs(slog.String("trace", trace)) + + return h.h.Handle(ctx, r) +} + +func (h stackTracer) GetTrace() string { + var b strings.Builder + pc := make([]uintptr, prgCount) + n := runtime.Callers(h.nSkip, pc) + frames := runtime.CallersFrames(pc[:n]) + + for frame, more := frames.Next(); more; frame, more = frames.Next() { + b.WriteString(frame.Function + "\n " + frame.File + ":" + strconv.Itoa(frame.Line) + "\n") + } + return b.String() +} + +func withStackTrace(h slog.Handler) slog.Handler { + return stackTracer{ + h: h, + nSkip: defSkip, + } +} |