From 7cb8bf24ca0d07138638fab6925f1dfe6cd86f25 Mon Sep 17 00:00:00 2001 From: Tomer Froumin Date: Tue, 5 Mar 2024 11:13:40 +0200 Subject: [PATCH] Added slog Handler (#17) --- slog_handler.go | 130 +++++++++++++++++++++++++++++++++++++++++++ slog_handler_test.go | 89 +++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 slog_handler.go create mode 100644 slog_handler_test.go diff --git a/slog_handler.go b/slog_handler.go new file mode 100644 index 0000000..b24ca59 --- /dev/null +++ b/slog_handler.go @@ -0,0 +1,130 @@ +package coralogix + +import ( + "context" + "log/slog" + "runtime" +) + +type CoralogixHandler struct { + // Next represents the next handler in the chain. + Next slog.Handler + // cxLogger is the Coralogix logger. + cxLogger *CoralogixLogger + AddSource bool +} + +type source struct { + Function string `json:"function"` + File string `json:"file"` + Line int `json:"line"` +} + +type logMessage struct { + Message string `json:"message"` + Level string `json:"level"` + Data map[string]any `json:"data,omitempty"` + Source source `json:"source,omitempty"` +} + +func NewCoralogixHandler(privateKey, applicationName, subsystemName string, next slog.Handler) *CoralogixHandler { + logger := NewCoralogixLogger( + privateKey, + applicationName, + subsystemName, + ) + + return &CoralogixHandler{ + Next: next, + cxLogger: logger, + } +} + +// Handle handles the provided log record. +func (h *CoralogixHandler) Handle(ctx context.Context, r slog.Record) error { + fs := runtime.CallersFrames([]uintptr{r.PC}) + f, _ := fs.Next() + + log := logMessage{ + Message: r.Message, + Level: r.Level.String(), + Data: map[string]interface{}{}, + } + + if h.AddSource { + log.Source = source{ + Function: f.Function, + File: f.File, + Line: f.Line, + } + } + + if r.NumAttrs() > 0 { + r.Attrs(func(a slog.Attr) bool { + attrToMap(log.Data, a) + return true + }) + } + + h.cxLogger.Log(levelSlogToCoralogix(r.Level), log, "", "", f.Function, "") + + return h.Next.Handle(ctx, r) +} + +// WithAttrs returns a new Coralogix whose attributes consists of handler's attributes followed by attrs. +func (h *CoralogixHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &CoralogixHandler{ + Next: h.Next.WithAttrs(attrs), + cxLogger: h.cxLogger, + AddSource: h.AddSource, + } +} + +// WithGroup returns a new Coralogix with a group, provided the group's name. +func (h *CoralogixHandler) WithGroup(name string) slog.Handler { + return &CoralogixHandler{ + Next: h.Next.WithGroup(name), + cxLogger: h.cxLogger, + AddSource: h.AddSource, + } +} + +// Enabled reports whether the logger emits log records at the given context and level. +// Note: We handover the decision down to the next handler. +func (h *CoralogixHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.Next.Enabled(ctx, level) +} + +func (h *CoralogixHandler) Stop() { + h.cxLogger.Destroy() +} + +func attrToMap(m map[string]any, a slog.Attr) { + switch v := a.Value.Any().(type) { + case error: + m[a.Key] = v.Error() + case []slog.Attr: + m2 := map[string]any{} + for _, a2 := range v { + attrToMap(m2, a2) + m[a.Key] = m2 + } + default: + m[a.Key] = a.Value.Any() + } +} + +func levelSlogToCoralogix(level slog.Level) uint { + switch level { + case slog.LevelDebug: + return Level.DEBUG + case slog.LevelInfo: + return Level.INFO + case slog.LevelWarn: + return Level.WARNING + case slog.LevelError: + return Level.ERROR + default: + return uint(level) + } +} diff --git a/slog_handler_test.go b/slog_handler_test.go new file mode 100644 index 0000000..3a2903a --- /dev/null +++ b/slog_handler_test.go @@ -0,0 +1,89 @@ +package coralogix + +import ( + "fmt" + "log/slog" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSlogHandler_Send(t *testing.T) { + coralogixHandler := NewCoralogixHandler( + GetEnv( + "PRIVATE_KEY", + testPrivateKey, + ), + "sdk-go", + "test", + slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }), + ) + defer func() { recover() }() + defer coralogixHandler.Stop() + + log := slog.New(coralogixHandler) + slog.SetDefault(log) + + attr := slog.Attr{Key: "extra", Value: slog.StringValue("additional")} + + testcases := []struct { + name string + logfn func(message string, args ...interface{}) + severity uint + }{ + { + name: "test debug", + severity: Level.DEBUG, + logfn: func(message string, args ...interface{}) { + log.Debug(message, args...) + }, + }, + { + name: "test info", + severity: Level.INFO, + logfn: func(message string, args ...interface{}) { + log.Info(message, args...) + }, + }, + { + name: "test warn", + severity: Level.WARNING, + logfn: func(message string, args ...interface{}) { + log.Warn(message, args...) + }, + }, + { + name: "test error", + severity: Level.ERROR, + logfn: func(message string, args ...interface{}) { + log.Error(message, args...) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + msg := fmt.Sprintf("%s (%s)", tc.name, t.Name()) + tc.logfn(msg, attr) + time.Sleep(time.Duration(1) * time.Second) + bulk, ok := mockHTTPServerMap[t.Name()] + assert.True(t, ok, "%s key not found in mockHTTPServerMap", t.Name()) + + var msgExists bool + for _, entry := range bulk.LogEntries { + if msgExists = strings.Contains(entry.Text, tc.name); msgExists { + assert.Equal(t, tc.severity, entry.Severity) + assert.True(t, strings.Contains(entry.Text, attr.Value.String()), + "entry Text does not contain extra field", entry.Text, attr) + break + } + } + assert.True(t, msgExists, "no matching message found", string(bulk.ToJSON())) + }) + } +}