Skip to content

Commit

Permalink
Implemented WithGroup and WithAttrs
Browse files Browse the repository at this point in the history
  • Loading branch information
mirackara committed Dec 17, 2024
1 parent 7a0d5c1 commit e2d1087
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 36 deletions.
2 changes: 1 addition & 1 deletion v3/integrations/logcontext-v2/nrslog/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func main() {
}

app.WaitForConnection(time.Second * 5)
log := slog.New(nrslog.WrapHandler(app, slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})))
log := slog.New(nrslog.WrapHandler(app, slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}), os.Stdout))

log.Info("I am a log message")

Expand Down
145 changes: 115 additions & 30 deletions v3/integrations/logcontext-v2/nrslog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"runtime"
"sync"
"time"

"github.com/newrelic/go-agent/v3/newrelic"
Expand All @@ -15,6 +18,15 @@ type NRHandler struct {
handler slog.Handler
app *newrelic.Application
txn *newrelic.Transaction
goas []groupOrAttrs
mu *sync.Mutex
out io.Writer
}

// groupOrAttrs is a structure that holds either a group name or a slice of attributes
type groupOrAttrs struct {
group string // group name if non-empty
attrs []slog.Attr // attrs if non-empty
}

// WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic
Expand All @@ -26,10 +38,12 @@ func WithTransactionFromContext(handler *NRHandler) *NRHandler {

// WrapHandler returns a new handler that is wrapped with New Relic tools to capture
// log data based on your application's logs in context settings.
func WrapHandler(app *newrelic.Application, handler slog.Handler) *NRHandler {
func WrapHandler(app *newrelic.Application, handler slog.Handler, w io.Writer) *NRHandler {
return &NRHandler{
handler: handler,
app: app,
mu: &sync.Mutex{},
out: w,
}
}

Expand All @@ -40,6 +54,8 @@ func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler {
handler: h.handler,
app: h.app,
txn: txn,
mu: &sync.Mutex{},
out: h.out,
}

return handler
Expand Down Expand Up @@ -74,23 +90,54 @@ func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
// - If a group's key is empty, inline the group's Attrs.
// - If a group has no Attrs (even if it has a non-empty key),
// ignore it.

func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error {
buf := make([]byte, 0, 1024)
attrs := map[string]interface{}{}

record.Attrs(func(attr slog.Attr) bool {
// ignore empty attributes
if !attr.Equal(slog.Attr{}) {
attrs[attr.Key] = attr.Value.Any()
}
return true
})

// timestamp must be sent to newrelic
logTime := record.Time.UnixMilli()
if record.Time.IsZero() {
logTime = time.Now().UnixMilli()
} else {
buf = h.appendAttr(buf, slog.Time(slog.TimeKey, record.Time))
}

// Construct the log message into a buffer
buf = h.appendAttr(buf, slog.String(slog.MessageKey, record.Message))
buf = h.appendAttr(buf, slog.Any(slog.LevelKey, record.Level))

// Configure the source file and line number if available
if record.PC != 0 {
fs := runtime.CallersFrames([]uintptr{record.PC})
f, _ := fs.Next()
buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)))
}

// Add any groups or attributes to the log message
goas := h.goas
group := ""
for _, goa := range goas {
if goa.group != "" {
group = goa.group
} else {
for _, a := range goa.attrs {
// if group is not "", then we need to add it to the key
if group != "" {
a.Key = group + "." + a.Key
}
attrs[a.Key] = a.Value.Any()
buf = h.appendAttr(buf, a)
}
}
}
record.Attrs(func(a slog.Attr) bool {
if !a.Equal(slog.Attr{}) {
attrs[a.Key] = a.Value.Any()
buf = h.appendAttr(buf, a)
}
return true
})

data := newrelic.LogData{
Severity: record.Level.String(),
Timestamp: logTime,
Expand All @@ -114,51 +161,91 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error {

// enrich log with newrelic metadata
// this will always return a valid log message even if an error occurs
enrichedRecord, enrichErr := enrichLog(record.Message, h.app, txn)
record.Message = enrichedRecord
enrichedBuf, enrichErr := enrichLog(buf, h.app, txn)
if enrichErr != nil {
err = fmt.Errorf("failed to enrich logs with New Relic metadata: %v", enrichErr)
}
handleErr := h.handler.Handle(ctx, record)
if handleErr != nil {
buf = enrichedBuf

// write the log to the output
buf = append(buf, "\n"...)
h.mu.Lock()
defer h.mu.Unlock()
_, bufErr := h.out.Write(buf)
if bufErr != nil {
if err != nil {
err = fmt.Errorf("%w; %w", err, handleErr)
err = fmt.Errorf("%v: %v", err, bufErr)
} else {
err = handleErr
err = bufErr
}
}

return err
}

func (h *NRHandler) appendAttr(buf []byte, a slog.Attr) []byte {
// Resolve the Attr's value before doing anything else.
a.Value = a.Value.Resolve()
// Ignore empty Attrs.
if a.Equal(slog.Attr{}) {
return buf
}
switch a.Value.Kind() {
case slog.KindString:
// Quote string values, to make them easy to parse.
buf = fmt.Appendf(buf, "%s=%q ", a.Key, a.Value.String())
case slog.KindTime:
// Write times in a standard way, without the monotonic time.
buf = fmt.Appendf(buf, "%s=%s ", a.Key, a.Value.Time().Format(time.RFC3339Nano))
case slog.KindGroup:
attrs := a.Value.Group()
// Ignore empty groups.
if len(attrs) == 0 {
return buf
}
for _, ga := range attrs {
buf = h.appendAttr(buf, ga)
}
default:
buf = fmt.Appendf(buf, "%s=%v ", a.Key, a.Value)
}
return buf
}

// enrich log always returns a valid log message even if an error occurs
func enrichLog(record string, app *newrelic.Application, txn *newrelic.Transaction) (string, error) {
func enrichLog(record []byte, app *newrelic.Application, txn *newrelic.Transaction) ([]byte, error) {
var buf *bytes.Buffer
var err error

if txn != nil {
buf = bytes.NewBuffer([]byte(record))
buf = bytes.NewBuffer(record)
err = newrelic.EnrichLog(buf, newrelic.FromTxn(txn))
} else if app != nil {
buf = bytes.NewBuffer([]byte(record))
buf = bytes.NewBuffer(record)
err = newrelic.EnrichLog(buf, newrelic.FromApp(app))
} else {
return record, nil
}

return buf.String(), err
return buf.Bytes(), err
}

func (h *NRHandler) withGroupOrAttrs(goa groupOrAttrs) *NRHandler {
h2 := *h
h2.goas = make([]groupOrAttrs, len(h.goas)+1)
copy(h2.goas, h.goas)
h2.goas[len(h2.goas)-1] = goa
return &h2
}

// WithAttrs returns a new Handler whose attributes consist of
// both the receiver's attributes and the arguments.
// The Handler owns the slice: it may retain, modify or discard it.
func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handler := h.handler.WithAttrs(attrs)
return &NRHandler{
handler: handler,
app: h.app,
txn: h.txn,
if len(attrs) == 0 {
return h
}
return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs})
}

// WithGroup returns a new Handler with the given group appended to
Expand All @@ -181,10 +268,8 @@ func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
//
// If the name is empty, WithGroup returns the receiver.
func (h *NRHandler) WithGroup(name string) slog.Handler {
handler := h.handler.WithGroup(name)
return &NRHandler{
handler: handler,
app: h.app,
txn: h.txn,
if name == "" {
return h
}
return h.withGroupOrAttrs(groupOrAttrs{group: name})
}
4 changes: 2 additions & 2 deletions v3/integrations/logcontext-v2/nrslog/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestHandlerZeroTime(t *testing.T) {
newrelic.ConfigAppLogForwardingEnabled(true),
)
out := bytes.NewBuffer([]byte{})
handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{}))
handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{}), out)
handler.Handle(context.Background(), slog.Record{
Level: slog.LevelInfo,
Message: "Hello World!",
Expand All @@ -52,7 +52,7 @@ func TestWrapHandler(t *testing.T) {
newrelic.ConfigAppLogForwardingEnabled(true),
)
out := bytes.NewBuffer([]byte{})
handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{}))
handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{}), out)
log := slog.New(handler)
message := "Hello World!"
log.Info(message)
Expand Down
6 changes: 3 additions & 3 deletions v3/integrations/logcontext-v2/nrslog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import (
)

// TextHandler is a wrapper on slog.NewTextHandler that includes New Relic Logs in Context.
// This method has been preserved for backwards compatibility, but is not longer recommended.
// This method has been preserved for backwards compatibility, but is not longer recommended..
// Deprecated: Use WrapHandler instead.
func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler {
return WrapHandler(app, slog.NewTextHandler(w, opts))
return WrapHandler(app, slog.NewTextHandler(w, opts), w)
}

// TextHandler is a wrapper on slog.NewTextHandler that includes New Relic Logs in Context.
// This method has been preserved for backwards compatibility, but is not longer recommended.
// Deprecated: Use WrapHandler instead.
func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler {
return WrapHandler(app, slog.NewJSONHandler(w, opts))
return WrapHandler(app, slog.NewJSONHandler(w, opts), w)
}

// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction.
Expand Down

0 comments on commit e2d1087

Please sign in to comment.