summaryrefslogtreecommitdiffstats
path: root/logging/log.go
diff options
context:
space:
mode:
Diffstat (limited to 'logging/log.go')
-rw-r--r--logging/log.go172
1 files changed, 152 insertions, 20 deletions
diff --git a/logging/log.go b/logging/log.go
index 5f9af8d..944b817 100644
--- a/logging/log.go
+++ b/logging/log.go
@@ -13,6 +13,7 @@ import (
"os"
"runtime"
"strings"
+ "time"
)
type ctxLoggerKeyType struct{}
@@ -63,6 +64,12 @@ func WithGroup(ctx context.Context, name string) (*slog.Logger, context.Context)
return logger, ctx
}
+type StackFrame struct {
+ Function string `json:"function"`
+ File string `json:"file"`
+ Line int `json:"line"`
+}
+
// RecoverAndLog logs a recovered panic or error with a stack trace using the logger from ctx.
//
// Typically used in a deferred recover block:
@@ -74,31 +81,142 @@ func WithGroup(ctx context.Context, name string) (*slog.Logger, context.Context)
// }
// }()
func RecoverAndLog(ctx context.Context, msg string, err any) {
- const size = 64 << 10 // 64 KB stack trace buffer
- buf := make([]byte, size)
- buf = buf[:runtime.Stack(buf, false)]
logger := FromContext(ctx)
- logger.ErrorContext(ctx, msg, "Error", err, "stack", string(buf))
+ handler := logger.Handler()
+
+ if !handler.Enabled(ctx, slog.LevelError) {
+ return
+ }
+
+ // Capture stack
+ const depth = 32
+ pcs := make([]uintptr, depth)
+ n := runtime.Callers(3, pcs) // Skip: Callers, RecoverAndLog, defer-fn
+ frames := runtime.CallersFrames(pcs[:n])
+
+ var stack []StackFrame
+ for {
+ frame, more := frames.Next()
+ stack = append(stack, StackFrame{
+ Function: frame.Function,
+ File: frame.File,
+ Line: frame.Line,
+ })
+ if !more {
+ break
+ }
+ }
+ var pc uintptr
+ if n > 0 {
+ pc = pcs[0]
+ }
+ r := slog.NewRecord(time.Now(), slog.LevelError, msg, pc)
+ r.Add("error", err, "stack", stack)
+ handler.Handle(ctx, r)
}
// LogSink defines where log output should be written.
//
// It is used to configure the log destination in Setup.
-type LogSink string
+type LogSink int
const (
// SinkStdout directs logs to the standard output (os.Stdout).
- SinkStdout LogSink = "stdout"
+ SinkStdout LogSink = iota
// SinkStderr directs logs to the standard error (os.Stderr).
- SinkStderr LogSink = "stderr"
+ SinkStderr
// SinkFile directs logs to a file specified by the File field in LogConfig.
//
// If configured, the File field must be a valid path for writing logs.
- SinkFile LogSink = "file"
+ SinkFile
)
+func (s LogSink) String() string {
+ switch s {
+ case SinkStdout:
+ return "stdout"
+ case SinkStderr:
+ return "stderr"
+ case SinkFile:
+ return "file"
+ default:
+ return fmt.Sprintf("LogSink(%d)", s)
+ }
+}
+
+func (s *LogSink) Set(str string) error {
+ switch strings.ToLower(str) {
+ case "stdout":
+ *s = SinkStdout
+ case "stderr":
+ *s = SinkStderr
+ case "file":
+ *s = SinkFile
+ default:
+ return fmt.Errorf("invalid LogSink %q", str)
+ }
+ return nil
+}
+
+func (s LogSink) AppendText(b []byte) ([]byte, error) {
+ return append(b, s.String()...), nil
+}
+
+func (s LogSink) MarshalText() ([]byte, error) {
+ return s.AppendText(nil)
+}
+
+func (s *LogSink) UnmarshalText(b []byte) error {
+ return s.Set(string(b))
+}
+
+type LogFormat int
+
+const (
+ // SinkStdout directs logs to the standard output (os.Stdout).
+ FormatText LogFormat = iota
+
+ // SinkStderr directs logs to the standard error (os.Stderr).
+ FormatJSON
+)
+
+func (s LogFormat) String() string {
+ switch s {
+ case FormatJSON:
+ return "json"
+ case FormatText:
+ return "text"
+ default:
+ return fmt.Sprintf("LogSink(%d)", s)
+ }
+}
+
+func (s *LogFormat) Set(str string) error {
+ switch strings.ToLower(str) {
+ case "text":
+ *s = FormatText
+ case "json":
+ *s = FormatJSON
+ default:
+ return fmt.Errorf("invalid LogSink %q", str)
+ }
+ return nil
+}
+
+func (s LogFormat) AppendText(b []byte) ([]byte, error) {
+ return append(b, s.String()...), nil
+}
+
+func (s LogFormat) MarshalText() ([]byte, error) {
+ return s.AppendText(nil)
+}
+
+func (s *LogFormat) UnmarshalText(b []byte) error {
+ return s.Set(string(b))
+}
+
// LogConfig holds configuration for the logging setup.
//
// Level specifies the minimum level for logs (e.g., slog.LevelInfo).
@@ -113,10 +231,14 @@ const (
type LogConfig struct {
Level slog.Level `toml:"level"`
Sink LogSink `toml:"sink"`
- Format string `toml:"format"`
+ Format LogFormat `toml:"format"`
File string `toml:"file"`
}
+func init() {
+ Setup(LogConfig{}) // Setup Sensible Defaults
+}
+
// Setup initializes the default slog.Logger based on the provided LogConfig.
//
// It configures the log output sink, log format, and log level.
@@ -128,7 +250,7 @@ type LogConfig struct {
// cfg := LogConfig{
// Level: slog.LevelInfo,
// Sink: SinkFile,
-// Format: "json",
+// Format: FormatJSON,
// File: "/var/log/myapp.log",
// }
// if err := Setup(cfg); err != nil {
@@ -137,24 +259,34 @@ type LogConfig struct {
//
// After a successful call, the global slog logger will log accordingly.
func Setup(cfg LogConfig) error {
+ logger, err := New(cfg)
+ if err != nil {
+ return nil
+ }
+ slog.SetDefault(logger)
+ return nil
+}
+
+// Same as Setup but returns the logger instead of setting up the global logger
+func New(cfg LogConfig) (*slog.Logger, error) {
var w io.Writer
switch cfg.Sink {
- case SinkStdout, "":
+ case SinkStdout:
w = os.Stdout
case SinkStderr:
w = os.Stderr
case SinkFile:
if cfg.File == "" {
- return fmt.Errorf("log sink set to 'file' but no file path provided")
+ return nil, fmt.Errorf("log sink set to 'file' but no file path provided")
}
f, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
- return fmt.Errorf("failed to open log file: %w", err)
+ return nil, fmt.Errorf("failed to open log file: %w", err)
}
w = f
default:
- return fmt.Errorf("unsupported log sink: %s", cfg.Sink)
+ return nil, fmt.Errorf("invalid LogSink %v", cfg.Sink)
}
var handler slog.Handler
@@ -162,15 +294,15 @@ func Setup(cfg LogConfig) error {
Level: cfg.Level,
}
- switch strings.ToLower(cfg.Format) {
- case "text":
+ switch cfg.Format {
+ case FormatText:
handler = slog.NewTextHandler(w, opts)
- case "json", "":
+ case FormatJSON:
handler = slog.NewJSONHandler(w, opts)
default:
- return fmt.Errorf("unsupported log format: %s", cfg.Format)
+ return nil, fmt.Errorf("invalid LogFormat: %v", cfg.Format)
}
- slog.SetDefault(slog.New(handler))
- return nil
+ logger := slog.New(handler)
+ return logger, nil
}