Skip to content

Commit

Permalink
Added slog Handler (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomerf authored Mar 5, 2024
1 parent c4f53ce commit 7cb8bf2
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 0 deletions.
130 changes: 130 additions & 0 deletions slog_handler.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
89 changes: 89 additions & 0 deletions slog_handler_test.go
Original file line number Diff line number Diff line change
@@ -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()))
})
}
}

0 comments on commit 7cb8bf2

Please sign in to comment.