From cc16684a9a7c41a1c16c8e8637132f5017efadf1 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Thu, 30 Jan 2025 15:35:27 -0500 Subject: [PATCH 01/14] use pointers for slog handlers --- v3/integrations/logcontext-v2/nrslog/go.mod | 8 + .../logcontext-v2/nrslog/handler.go | 154 ++---------------- .../logcontext-v2/nrslog/handler_test.go | 6 +- v3/integrations/logcontext-v2/nrslog/wrap.go | 69 ++++++++ 4 files changed, 99 insertions(+), 138 deletions(-) create mode 100644 v3/integrations/logcontext-v2/nrslog/wrap.go diff --git a/v3/integrations/logcontext-v2/nrslog/go.mod b/v3/integrations/logcontext-v2/nrslog/go.mod index 0dc5ec35e..4aec5d520 100644 --- a/v3/integrations/logcontext-v2/nrslog/go.mod +++ b/v3/integrations/logcontext-v2/nrslog/go.mod @@ -4,5 +4,13 @@ go 1.21 require github.com/newrelic/go-agent/v3 v3.36.0 +require ( + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 9c476910f..170ed0899 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -2,7 +2,6 @@ package nrslog import ( "context" - "io" "log/slog" "github.com/newrelic/go-agent/v3/newrelic" @@ -16,66 +15,6 @@ type NRHandler struct { txn *newrelic.Transaction } -// TextHandler creates a wrapped Slog TextHandler, enabling it to both automatically capture logs -// and to enrich logs locally depending on your logs in context configuration in your New Relic -// application. -func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) NRHandler { - nrWriter := NewWriter(w, app) - textHandler := slog.NewTextHandler(nrWriter, opts) - wrappedHandler := WrapHandler(app, textHandler) - wrappedHandler.addWriter(&nrWriter) - return wrappedHandler -} - -// JSONHandler creates a wrapped Slog JSONHandler, enabling it to both automatically capture logs -// and to enrich logs locally depending on your logs in context configuration in your New Relic -// application. -func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) NRHandler { - nrWriter := NewWriter(w, app) - jsonHandler := slog.NewJSONHandler(nrWriter, opts) - wrappedHandler := WrapHandler(app, jsonHandler) - wrappedHandler.addWriter(&nrWriter) - return wrappedHandler -} - -// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction. -// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. -func WithTransaction(txn *newrelic.Transaction, logger *slog.Logger) *slog.Logger { - if txn == nil || logger == nil { - return logger - } - - h := logger.Handler() - switch nrHandler := h.(type) { - case NRHandler: - txnHandler := nrHandler.WithTransaction(txn) - return slog.New(txnHandler) - default: - return logger - } -} - -// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction it its found -// in a context. -// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. -func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { - if ctx == nil { - return logger - } - - txn := newrelic.FromContext(ctx) - return WithTransaction(txn, logger) -} - -// 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 { - return NRHandler{ - handler: handler, - app: app, - } -} - // addWriter is an internal helper function to append an io.Writer to the NRHandler object func (h *NRHandler) addWriter(w *LogWriter) { h.w = w @@ -83,7 +22,7 @@ func (h *NRHandler) addWriter(w *LogWriter) { // WithTransaction returns a new handler that is configured to capture log data // and attribute it to a specific transaction. -func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) NRHandler { +func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { handler := NRHandler{ handler: h.handler, app: h.app, @@ -95,19 +34,12 @@ func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) NRHandler { handler.addWriter(&writer) } - return handler + return &handler } // Enabled reports whether the handler handles records at the given level. // The handler ignores records whose level is lower. -// It is called early, before any arguments are processed, -// to save effort if the log event should be discarded. -// If called from a Logger method, the first argument is the context -// passed to that method, or context.Background() if nil was passed -// or the method does not take a context. -// The context is passed so Enabled can use its values -// to make a decision. -func (h NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { +func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { return h.handler.Enabled(ctx, lvl) } @@ -118,17 +50,12 @@ func (h NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { // Canceling the context should not affect record processing. // (Among other things, log messages may be necessary to debug a // cancellation-related problem.) -// -// Handle methods that produce output should observe the following rules: -// - If r.Time is the zero time, ignore the time. -// - If r.PC is zero, ignore it. -// - Attr's values should be resolved. -// - If an Attr's key and value are both the zero value, ignore the Attr. -// This can be tested with attr.Equal(Attr{}). -// - 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 { +func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { + nrTxn := h.txn + if txn := newrelic.FromContext(ctx); txn != nil { + nrTxn = txn + } + attrs := map[string]interface{}{} record.Attrs(func(attr slog.Attr) bool { @@ -142,8 +69,8 @@ func (h NRHandler) Handle(ctx context.Context, record slog.Record) error { Message: record.Message, Attributes: attrs, } - if h.txn != nil { - h.txn.RecordLog(data) + if nrTxn != nil { + nrTxn.RecordLog(data) } else { h.app.RecordLog(data) } @@ -154,78 +81,33 @@ func (h NRHandler) Handle(ctx context.Context, record slog.Record) error { // 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 { +func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handler := h.handler.WithAttrs(attrs) - return NRHandler{ + return &NRHandler{ handler: handler, app: h.app, txn: h.txn, } - } // WithGroup returns a new Handler with the given group appended to // the receiver's existing groups. // The keys of all subsequent attributes, whether added by With or in a // Record, should be qualified by the sequence of group names. -// -// How this qualification happens is up to the Handler, so long as -// this Handler's attribute keys differ from those of another Handler -// with a different sequence of group names. -// -// A Handler should treat WithGroup as starting a Group of Attrs that ends -// at the end of the log event. That is, -// -// logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2)) -// -// should behave like -// -// logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) -// // If the name is empty, WithGroup returns the receiver. -func (h NRHandler) WithGroup(name string) slog.Handler { +func (h *NRHandler) WithGroup(name string) slog.Handler { handler := h.handler.WithGroup(name) - return NRHandler{ + return &NRHandler{ handler: handler, app: h.app, txn: h.txn, } } -// NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context. -// New Relic transaction value is taken from context. It cannot be set directly. -// This serves as a quality of life improvement for cases where slog.Default global instance is -// referenced, allowing to use slog methods directly and maintaining New Relic instrumentation. -type TransactionFromContextHandler struct { - NRHandler -} - // WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic // transaction from context. -func WithTransactionFromContext(handler NRHandler) TransactionFromContextHandler { - return TransactionFromContextHandler{handler} -} - -// Handle handles the Record. -// It will only be called when Enabled returns true. -// The Context argument is as for Enabled and NewRelic transaction. -// Canceling the context should not affect record processing. -// (Among other things, log messages may be necessary to debug a -// cancellation-related problem.) // -// Handle methods that produce output should observe the following rules: -// - If r.Time is the zero time, ignore the time. -// - If r.PC is zero, ignore it. -// - Attr's values should be resolved. -// - If an Attr's key and value are both the zero value, ignore the Attr. -// This can be tested with attr.Equal(Attr{}). -// - 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 TransactionFromContextHandler) Handle(ctx context.Context, record slog.Record) error { - if txn := newrelic.FromContext(ctx); txn != nil { - return h.NRHandler.WithTransaction(txn).Handle(ctx, record) - } - - return h.NRHandler.Handle(ctx, record) +// Deprecated: this is a no-op +func WithTransactionFromContext(handler *NRHandler) *NRHandler { + return handler } diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 4e3d6d774..8c3d6f480 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -243,7 +243,7 @@ func TestWithAttributesFromContext(t *testing.T) { slog.Int("answer", 42), slog.Any("some_map", map[string]interface{}{"a": 1.0, "b": 2}), ) - + metadata := txn.GetTraceMetadata() txn.End() app.ExpectLogEvents(t, []internal.WantLog{ @@ -261,9 +261,10 @@ func TestWithAttributesFromContext(t *testing.T) { "answer": 42, "some_map": map[string]interface{}{"a": 1.0, "b": 2}, }, + TraceID: metadata.TraceID, + SpanID: metadata.SpanID, }, }) - } func TestWithGroup(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, @@ -299,6 +300,7 @@ func TestWithGroup(t *testing.T) { } +// Ensure deprecation compatibility func TestTransactionFromContextHandler(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), diff --git a/v3/integrations/logcontext-v2/nrslog/wrap.go b/v3/integrations/logcontext-v2/nrslog/wrap.go new file mode 100644 index 000000000..38bf72fd3 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/wrap.go @@ -0,0 +1,69 @@ +package nrslog + +import ( + "context" + "io" + "log/slog" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +// TextHandler creates a wrapped Slog TextHandler, enabling it to both automatically capture logs +// and to enrich logs locally depending on your logs in context configuration in your New Relic +// application. +func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { + nrWriter := NewWriter(w, app) + textHandler := slog.NewTextHandler(nrWriter, opts) + wrappedHandler := WrapHandler(app, textHandler) + wrappedHandler.addWriter(&nrWriter) + return wrappedHandler +} + +// JSONHandler creates a wrapped Slog JSONHandler, enabling it to both automatically capture logs +// and to enrich logs locally depending on your logs in context configuration in your New Relic +// application. +func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { + nrWriter := NewWriter(w, app) + jsonHandler := slog.NewJSONHandler(nrWriter, opts) + wrappedHandler := WrapHandler(app, jsonHandler) + wrappedHandler.addWriter(&nrWriter) + return wrappedHandler +} + +// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction it its found +// in a context. +// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. +func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { + if ctx == nil { + return logger + } + + txn := newrelic.FromContext(ctx) + return WithTransaction(txn, logger) +} + +// 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 { + return &NRHandler{ + handler: handler, + app: app, + } +} + +// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction. +// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. +func WithTransaction(txn *newrelic.Transaction, logger *slog.Logger) *slog.Logger { + if txn == nil || logger == nil { + return logger + } + + h := logger.Handler() + switch nrHandler := h.(type) { + case *NRHandler: + txnHandler := nrHandler.WithTransaction(txn) + return slog.New(txnHandler) + default: + return logger + } +} From 51f3abd856aa226e7726d71ea2a9dd17c941e2fa Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Fri, 31 Jan 2025 00:43:14 -0800 Subject: [PATCH 02/14] initial implementation of oom callback trigger mechanism --- v3/examples/oom/main.go | 56 +++++++++++++ v3/go.mod | 6 ++ v3/newrelic/internal_app.go | 3 + v3/newrelic/oom_monitor.go | 155 ++++++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 v3/examples/oom/main.go create mode 100644 v3/newrelic/oom_monitor.go diff --git a/v3/examples/oom/main.go b/v3/examples/oom/main.go new file mode 100644 index 000000000..6ad9a8a07 --- /dev/null +++ b/v3/examples/oom/main.go @@ -0,0 +1,56 @@ +// Copyright 2020 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" + "runtime" + "time" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +const MB = 1024 * 1024 + +func main() { + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("OOM Response High Water Mark App"), + newrelic.ConfigFromEnvironment(), + newrelic.ConfigDebugLogger(os.Stdout), + ) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Wait for the application to connect. + if err := app.WaitForConnection(5 * time.Second); nil != err { + fmt.Println(err) + } + + app.HeapHighWaterMarkAlarmSet(1*MB, megabyte) + app.HeapHighWaterMarkAlarmSet(10*MB, tenMegabyte) + app.HeapHighWaterMarkAlarmSet(100*MB, hundredMegabyte) + app.HeapHighWaterMarkAlarmEnable(2 * time.Second) + + var a [][]byte + for _ = range 100 { + a = append(a, make([]byte, MB, MB)) + time.Sleep(1 * time.Second) + } + + // Shut down the application to flush data to New Relic. + app.Shutdown(10 * time.Second) +} + +func megabyte(limit uint64, stats *runtime.MemStats) { + fmt.Printf("*** 1M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) +} +func tenMegabyte(limit uint64, stats *runtime.MemStats) { + fmt.Printf("*** 10M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) +} +func hundredMegabyte(limit uint64, stats *runtime.MemStats) { + fmt.Printf("*** 100M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc) +} diff --git a/v3/go.mod b/v3/go.mod index 3a673a907..560b2ac95 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -7,6 +7,12 @@ require ( google.golang.org/protobuf v1.34.2 ) +require ( + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect +) retract v3.22.0 // release process error corrected in v3.22.1 diff --git a/v3/newrelic/internal_app.go b/v3/newrelic/internal_app.go index 54b7b4fdc..d187cd85c 100644 --- a/v3/newrelic/internal_app.go +++ b/v3/newrelic/internal_app.go @@ -66,6 +66,9 @@ type app struct { // registered callback functions llmTokenCountCallback func(string, string) int + // high water mark alarms + heapHighWaterMarkAlarms heapHighWaterMarkAlarmSet + serverless *serverlessHarvest } diff --git a/v3/newrelic/oom_monitor.go b/v3/newrelic/oom_monitor.go new file mode 100644 index 000000000..0ce023844 --- /dev/null +++ b/v3/newrelic/oom_monitor.go @@ -0,0 +1,155 @@ +// Copyright 2022 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package newrelic + +import ( + "runtime" + "sync" + "time" +) + +type heapHighWaterMarkAlarmSet struct { + lock sync.RWMutex // protects creation of the ticker and access to map + sampleTicker *time.Ticker // once made, only read by monitor goroutine + alarms map[uint64]func(uint64, *runtime.MemStats) + done chan byte +} + +// This is a gross, high-level whole-heap memory monitor which can be used to monitor, track, +// and trigger an application's response to running out of memory as an initial step or when +// more expensive or sophisticated analysis such as per-routine memory usage tracking is not +// needed. +// +// For this, we simply configure one or more heap memory limits and for each, register a callback +// function to be called any time we notice that the total heap allocation reaches or exceeds that +// limit. Note that this means if the allocation size crosses multiple limits, then multiple +// callbacks will be triggered since each of their criteria will be met. +// +// HeapHighWaterMarkAlarmEnable starts the periodic sampling of the runtime heap allocation +// of the application, at the user-provided sampling interval. Calling HeapHighWaterMarkAlarmEnable +// with an interval less than or equal to 0 is equivalent to calling HeapHighWaterMarkAlarmDisable. +// +// If there was already a running heap monitor, this merely changes its sample interval time. +func (a *Application) HeapHighWaterMarkAlarmEnable(interval time.Duration) { + if a == nil || a.app == nil { + return + } + + if interval <= 0 { + a.HeapHighWaterMarkAlarmDisable() + return + } + + a.app.heapHighWaterMarkAlarms.lock.Lock() + defer a.app.heapHighWaterMarkAlarms.lock.Unlock() + if a.app.heapHighWaterMarkAlarms.sampleTicker == nil { + a.app.heapHighWaterMarkAlarms.sampleTicker = time.NewTicker(interval) + a.app.heapHighWaterMarkAlarms.done = make(chan byte) + go a.app.heapHighWaterMarkAlarms.monitor() + } else { + a.app.heapHighWaterMarkAlarms.sampleTicker.Reset(interval) + } +} + +func (as *heapHighWaterMarkAlarmSet) monitor() { + for { + select { + case <-as.sampleTicker.C: + var m runtime.MemStats + runtime.ReadMemStats(&m) + as.lock.RLock() + defer as.lock.RUnlock() + if as.alarms != nil { + for limit, callback := range as.alarms { + if m.HeapAlloc >= limit { + callback(limit, &m) + } + } + } + case <-as.done: + return + } + } +} + +// HeapHighWaterMarkAlarmShutdown stops the monitoring goroutine and deallocates the entire +// monitoring completely. All alarms are calcelled and disabled. +func (a *Application) HeapHighWaterMarkAlarmShutdown() { + if a == nil || a.app == nil { + return + } + + a.app.heapHighWaterMarkAlarms.lock.Lock() + defer a.app.heapHighWaterMarkAlarms.lock.Unlock() + a.app.heapHighWaterMarkAlarms.sampleTicker.Stop() + if a.app.heapHighWaterMarkAlarms.done != nil { + a.app.heapHighWaterMarkAlarms.done <- 0 + } + if a.app.heapHighWaterMarkAlarms.alarms != nil { + clear(a.app.heapHighWaterMarkAlarms.alarms) + a.app.heapHighWaterMarkAlarms.alarms = nil + } +} + +// HeapHighWaterMarkAlarmDisable stops sampling the heap memory allocation started by +// HeapHighWaterMarkAlarmEnable. It is safe to call even if HeapHighWaterMarkAlarmEnable was +// never called or the alarms were already disabled. +func (a *Application) HeapHighWaterMarkAlarmDisable() { + if a == nil || a.app == nil { + return + } + + a.app.heapHighWaterMarkAlarms.lock.Lock() + defer a.app.heapHighWaterMarkAlarms.lock.Unlock() + if a.app.heapHighWaterMarkAlarms.sampleTicker != nil { + a.app.heapHighWaterMarkAlarms.sampleTicker.Stop() + } +} + +// HeapHighWaterMarkAlarmSet adds a heap memory high water mark alarm to the set of alarms +// being tracked by the running heap monitor. Memory is checked on the interval specified to +// the last call to HeapHighWaterMarkAlarmEnable, and if at that point the globally allocated heap +// memory is at least the specified size, the provided callback function will be invoked. This +// method may be called multiple times to register any number of callback functions to respond +// to different memory thresholds. For example, you may wish to make measurements or warnings +// of various urgency levels before finally taking action. +// +// If HeapHighWaterMarkAlarmSet is called with the same memory limit as a previous call, the +// supplied callback function will replace the one previously registered for that limit. If +// the function is given as nil, then that memory limit alarm is removed from the list. +func (a *Application) HeapHighWaterMarkAlarmSet(limit uint64, f func(uint64, *runtime.MemStats)) { + if a == nil || a.app == nil { + return + } + + a.app.heapHighWaterMarkAlarms.lock.Lock() + defer a.app.heapHighWaterMarkAlarms.lock.Unlock() + + if a.app.heapHighWaterMarkAlarms.alarms == nil { + a.app.heapHighWaterMarkAlarms.alarms = make(map[uint64]func(uint64, *runtime.MemStats)) + } + + if f == nil { + delete(a.app.heapHighWaterMarkAlarms.alarms, limit) + } else { + a.app.heapHighWaterMarkAlarms.alarms[limit] = f + } +} + +// HeapHighWaterMarkAlarmClearAll removes all high water mark alarms from the memory monitor +// set. +func (a *Application) HeapHighWaterMarkAlarmClearAll() { + if a == nil || a.app == nil { + return + } + + a.app.heapHighWaterMarkAlarms.lock.Lock() + defer a.app.heapHighWaterMarkAlarms.lock.Unlock() + + if a.app.heapHighWaterMarkAlarms.alarms == nil { + return + } + + clear(a.app.heapHighWaterMarkAlarms.alarms) +} From 2f9dc002248d91fcedf6d6fe7c61b3aef48e4c06 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 31 Jan 2025 10:43:18 -0500 Subject: [PATCH 03/14] improved user interface --- .../logcontext-v2/nrslog/handler.go | 55 ++++++++++----- .../logcontext-v2/nrslog/handler_test.go | 40 +++++++++-- v3/integrations/logcontext-v2/nrslog/wrap.go | 57 ++++++++------- .../logcontext-v2/nrslog/writer.go | 70 ------------------- 4 files changed, 105 insertions(+), 117 deletions(-) delete mode 100644 v3/integrations/logcontext-v2/nrslog/writer.go diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 170ed0899..d00e09e0f 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -1,8 +1,10 @@ package nrslog import ( + "bytes" "context" "log/slog" + "time" "github.com/newrelic/go-agent/v3/newrelic" ) @@ -10,16 +12,10 @@ import ( // NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context type NRHandler struct { handler slog.Handler - w *LogWriter app *newrelic.Application txn *newrelic.Transaction } -// addWriter is an internal helper function to append an io.Writer to the NRHandler object -func (h *NRHandler) addWriter(w *LogWriter) { - h.w = w -} - // WithTransaction returns a new handler that is configured to capture log data // and attribute it to a specific transaction. func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { @@ -29,11 +25,6 @@ func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { txn: txn, } - if h.w != nil { - writer := h.w.WithTransaction(txn) - handler.addWriter(&writer) - } - return &handler } @@ -51,31 +42,57 @@ func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { // (Among other things, log messages may be necessary to debug a // cancellation-related problem.) func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { - nrTxn := h.txn - if txn := newrelic.FromContext(ctx); txn != nil { - nrTxn = txn - } - attrs := map[string]interface{}{} record.Attrs(func(attr slog.Attr) bool { - attrs[attr.Key] = attr.Value.Any() + // ignore empty attributes + if !attr.Equal(slog.Attr{}) { + attrs[attr.Key] = attr.Value.Any() + } return true }) + // timestamp must be sent to newrelic + var timestamp int64 + if record.Time.IsZero() { + timestamp = time.Now().UnixMilli() + } else { + timestamp = record.Time.UnixMilli() + } + data := newrelic.LogData{ Severity: record.Level.String(), - Timestamp: record.Time.UnixMilli(), + Timestamp: timestamp, Message: record.Message, Attributes: attrs, } + + nrTxn := h.txn + + ctxTxn := newrelic.FromContext(ctx) + if ctxTxn != nil { + nrTxn = ctxTxn + } + + var enricherOpts newrelic.EnricherOption if nrTxn != nil { nrTxn.RecordLog(data) + enricherOpts = newrelic.FromTxn(nrTxn) } else { h.app.RecordLog(data) + enricherOpts = newrelic.FromApp(h.app) + } + + // add linking metadata as an attribute + // without disrupting normal usage of the handler + nrLinking := bytes.NewBuffer([]byte{}) + err := newrelic.EnrichLog(nrLinking, enricherOpts) + if err == nil { + record.AddAttrs(slog.String("newrelic", nrLinking.String())) } - return h.handler.Handle(ctx, record) + err = h.handler.Handle(ctx, record) + return err } // WithAttrs returns a new Handler whose attributes consist of diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 8c3d6f480..93bd798c5 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "log/slog" - "os" "strings" "testing" @@ -228,12 +227,28 @@ func TestWithAttributes(t *testing.T) { func TestWithAttributesFromContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, - newrelic.ConfigAppLogDecoratingEnabled(false), + newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) - log := slog.New(TextHandler(app.Application, os.Stdout, &slog.HandlerOptions{})) + + writer := &bytes.Buffer{} + log := New(app.Application, slog.NewTextHandler(writer, &slog.HandlerOptions{})) log.Info("I am a log message") + logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + EntityName: integrationsupport.SampleAppName, + Hostname: host, + }) + + logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + EntityName: integrationsupport.SampleAppName, + Hostname: host, + }) + + // purge the buffer + writer.Reset() txn := app.StartTransaction("example transaction") ctx := newrelic.NewContext(context.Background(), txn) @@ -265,7 +280,16 @@ func TestWithAttributesFromContext(t *testing.T) { SpanID: metadata.SpanID, }, }) + + logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + EntityName: integrationsupport.SampleAppName, + Hostname: host, + TraceID: metadata.TraceID, + SpanID: metadata.SpanID, + }) } + func TestWithGroup(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(false), @@ -297,7 +321,6 @@ func TestWithGroup(t *testing.T) { if !strings.Contains(log2, attrString) { t.Errorf("expected %s to contain %s", log2, attrString) } - } // Ensure deprecation compatibility @@ -337,3 +360,12 @@ func TestTransactionFromContextHandler(t *testing.T) { }, }) } + +// shockingly cheap +func BenchmarkGetContextValue(b *testing.B) { + var a *bytes.Buffer + for i := 0; i < b.N; i++ { + a = bytes.NewBuffer([]byte{}) + } + a.Reset() +} diff --git a/v3/integrations/logcontext-v2/nrslog/wrap.go b/v3/integrations/logcontext-v2/nrslog/wrap.go index 38bf72fd3..e533bb49c 100644 --- a/v3/integrations/logcontext-v2/nrslog/wrap.go +++ b/v3/integrations/logcontext-v2/nrslog/wrap.go @@ -11,35 +11,19 @@ import ( // TextHandler creates a wrapped Slog TextHandler, enabling it to both automatically capture logs // and to enrich logs locally depending on your logs in context configuration in your New Relic // application. +// +// Deprecated: Use WrapHandler() instead. func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { - nrWriter := NewWriter(w, app) - textHandler := slog.NewTextHandler(nrWriter, opts) - wrappedHandler := WrapHandler(app, textHandler) - wrappedHandler.addWriter(&nrWriter) - return wrappedHandler + return WrapHandler(app, slog.NewTextHandler(w, opts)) } // JSONHandler creates a wrapped Slog JSONHandler, enabling it to both automatically capture logs // and to enrich logs locally depending on your logs in context configuration in your New Relic // application. +// +// Deprecated: Use WrapHandler() instead. func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { - nrWriter := NewWriter(w, app) - jsonHandler := slog.NewJSONHandler(nrWriter, opts) - wrappedHandler := WrapHandler(app, jsonHandler) - wrappedHandler.addWriter(&nrWriter) - return wrappedHandler -} - -// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction it its found -// in a context. -// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. -func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { - if ctx == nil { - return logger - } - - txn := newrelic.FromContext(ctx) - return WithTransaction(txn, logger) + return WrapHandler(app, slog.NewJSONHandler(w, opts)) } // WrapHandler returns a new handler that is wrapped with New Relic tools to capture @@ -51,8 +35,33 @@ func WrapHandler(app *newrelic.Application, handler slog.Handler) *NRHandler { } } -// WithTransaction creates a new Slog Logger object to be used for logging within a given transaction. -// Calling this function with a logger having underlying TransactionFromContextHandler handler is a no-op. +// New Returns a new slog.Logger object wrapped with a New Relic handler that controls +// logs in context features. +func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { + return slog.New(WrapHandler(app, handler)) +} + +// WithTransaction creates a new Slog Logger object to be used for logging within a given +// transaction it its found in a context. Creating a transaction logger can have a performance +// benefit when transactions are long running, and have a high log volume. +// +// Note: transaction contexts can also be passed to the logger without creating a new +// logger using logger.InfoContext() or similar commands. +func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { + if ctx == nil { + return logger + } + + txn := newrelic.FromContext(ctx) + return WithTransaction(txn, logger) +} + +// WithTransaction creates a new Slog Logger object to be used for logging +// within a given transaction. Creating a transaction logger can have a performance +// benefit when transactions are long running, and have a high log volume. +// +// Note: transaction contexts can also be passed to the logger without creating a new +// logger using logger.InfoContext() or similar commands. func WithTransaction(txn *newrelic.Transaction, logger *slog.Logger) *slog.Logger { if txn == nil || logger == nil { return logger diff --git a/v3/integrations/logcontext-v2/nrslog/writer.go b/v3/integrations/logcontext-v2/nrslog/writer.go deleted file mode 100644 index 4248469a4..000000000 --- a/v3/integrations/logcontext-v2/nrslog/writer.go +++ /dev/null @@ -1,70 +0,0 @@ -package nrslog - -import ( - "bytes" - "io" - - "github.com/newrelic/go-agent/v3/newrelic" -) - -// LogWriter is an io.Writer that captures log data for use with New Relic Logs in Context -type LogWriter struct { - debug bool - out io.Writer - app *newrelic.Application - txn *newrelic.Transaction -} - -// New creates a new NewRelicWriter Object -// output is the io.Writer destination that you want your log to be written to -// app must be a vaild, non nil new relic Application -func NewWriter(output io.Writer, app *newrelic.Application) LogWriter { - return LogWriter{ - out: output, - app: app, - } -} - -// DebugLogging enables or disables debug error messages being written in the IO output. -// By default, the nrwriter debug logging is set to false and will fail silently -func (b *LogWriter) DebugLogging(enabled bool) { - b.debug = enabled -} - -// WithTransaction duplicates the current NewRelicWriter and sets the transaction to txn -func (b *LogWriter) WithTransaction(txn *newrelic.Transaction) LogWriter { - return LogWriter{ - out: b.out, - app: b.app, - debug: b.debug, - txn: txn, - } -} - -// EnrichLog attempts to enrich a log with New Relic linking metadata. If it fails, -// it will return the original log line unless debug=true, otherwise it will print -// an error on a following line. -func (b *LogWriter) EnrichLog(p []byte) []byte { - logLine := bytes.TrimRight(p, "\n") - buf := bytes.NewBuffer(logLine) - - var enrichErr error - if b.txn != nil { - enrichErr = newrelic.EnrichLog(buf, newrelic.FromTxn(b.txn)) - } else { - enrichErr = newrelic.EnrichLog(buf, newrelic.FromApp(b.app)) - } - - if b.debug && enrichErr != nil { - buf.WriteString("\n") - buf.WriteString(enrichErr.Error()) - } - - buf.WriteString("\n") - return buf.Bytes() -} - -// Write implements io.Write -func (b LogWriter) Write(p []byte) (n int, err error) { - return b.out.Write(b.EnrichLog(p)) -} From 19d78566a0d0e8984c50aceda419053c45d4859b Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Fri, 31 Jan 2025 09:29:33 -0800 Subject: [PATCH 04/14] fix shutdown --- v3/newrelic/oom_monitor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/newrelic/oom_monitor.go b/v3/newrelic/oom_monitor.go index 0ce023844..fcae5ff9e 100644 --- a/v3/newrelic/oom_monitor.go +++ b/v3/newrelic/oom_monitor.go @@ -90,6 +90,7 @@ func (a *Application) HeapHighWaterMarkAlarmShutdown() { clear(a.app.heapHighWaterMarkAlarms.alarms) a.app.heapHighWaterMarkAlarms.alarms = nil } + a.app.heapHighWaterMarkAlarms.sampleTicker = nil } // HeapHighWaterMarkAlarmDisable stops sampling the heap memory allocation started by From 877e3bcd521ffe50f7cd471a0b35503a038a9165 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 31 Jan 2025 13:38:33 -0500 Subject: [PATCH 05/14] 28% faster log enrichment --- v3/integrations/logcontext-v2/nrslog/go.mod | 9 -- .../logcontext-v2/nrslog/handler.go | 41 ++++------ .../logcontext-v2/nrslog/handler_test.go | 82 ++++++++++++++++--- .../logcontext-v2/nrslog/linking.go | 74 +++++++++++++++++ v3/newrelic/application.go | 25 ++++++ 5 files changed, 185 insertions(+), 46 deletions(-) create mode 100644 v3/integrations/logcontext-v2/nrslog/linking.go diff --git a/v3/integrations/logcontext-v2/nrslog/go.mod b/v3/integrations/logcontext-v2/nrslog/go.mod index 4aec5d520..816aec86b 100644 --- a/v3/integrations/logcontext-v2/nrslog/go.mod +++ b/v3/integrations/logcontext-v2/nrslog/go.mod @@ -4,13 +4,4 @@ go 1.21 require github.com/newrelic/go-agent/v3 v3.36.0 -require ( - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect -) - replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index d00e09e0f..938adbc6a 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -1,7 +1,6 @@ package nrslog import ( - "bytes" "context" "log/slog" "time" @@ -36,12 +35,19 @@ func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { // Handle handles the Record. // It will only be called when Enabled returns true. -// The Context argument is as for Enabled. -// It is present solely to provide Handlers access to the context's values. -// Canceling the context should not affect record processing. -// (Among other things, log messages may be necessary to debug a -// cancellation-related problem.) func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { + nrTxn := h.txn + + ctxTxn := newrelic.FromContext(ctx) + if ctxTxn != nil { + nrTxn = ctxTxn + } + + // if no app or txn, do nothing + if h.app == nil && nrTxn == nil { + return h.handler.Handle(ctx, record) + } + attrs := map[string]interface{}{} record.Attrs(func(attr slog.Attr) bool { @@ -67,32 +73,15 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { Attributes: attrs, } - nrTxn := h.txn - - ctxTxn := newrelic.FromContext(ctx) - if ctxTxn != nil { - nrTxn = ctxTxn - } - - var enricherOpts newrelic.EnricherOption if nrTxn != nil { nrTxn.RecordLog(data) - enricherOpts = newrelic.FromTxn(nrTxn) + enrichRecordTxn(nrTxn, &record) } else { h.app.RecordLog(data) - enricherOpts = newrelic.FromApp(h.app) - } - - // add linking metadata as an attribute - // without disrupting normal usage of the handler - nrLinking := bytes.NewBuffer([]byte{}) - err := newrelic.EnrichLog(nrLinking, enricherOpts) - if err == nil { - record.AddAttrs(slog.String("newrelic", nrLinking.String())) + enrichRecord(h.app, &record) } - err = h.handler.Handle(ctx, record) - return err + return h.handler.Handle(ctx, record) } // WithAttrs returns a new Handler whose attributes consist of diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 93bd798c5..26ab72fbe 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -42,6 +42,21 @@ func TestHandler(t *testing.T) { }) } +func TestHandlerNilApp(t *testing.T) { + out := bytes.NewBuffer([]byte{}) + logger := New(nil, slog.NewTextHandler(out, &slog.HandlerOptions{})) + message := "Hello World!" + logger.Info(message) + + logStr := out.String() + if strings.Contains(logStr, nrlinking) { + t.Errorf(" %s should not contain %s", logStr, nrlinking) + } + if len(logStr) == 0 { + t.Errorf("log string should not be empty") + } +} + func TestJSONHandler(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), @@ -88,12 +103,11 @@ func TestHandlerTransactions(t *testing.T) { log.Debug(backgroundMsg) txn.End() - /* - logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ - EntityGUID: integrationsupport.TestEntityGUID, - Hostname: host, - EntityName: integrationsupport.SampleAppName, - }) */ + logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + Hostname: host, + EntityName: integrationsupport.SampleAppName, + }) app.ExpectLogEvents(t, []internal.WantLog{ { @@ -361,11 +375,57 @@ func TestTransactionFromContextHandler(t *testing.T) { }) } -// shockingly cheap -func BenchmarkGetContextValue(b *testing.B) { - var a *bytes.Buffer +func BenchmarkEnrichLog(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + txn := app.Application.StartTransaction("my txn") + defer txn.End() + record := slog.Record{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + nrLinking := bytes.NewBuffer([]byte{}) + err := newrelic.EnrichLog(nrLinking, newrelic.FromTxn(txn)) + if err == nil { + record.AddAttrs(slog.String("newrelic", nrLinking.String())) + } + } +} + +func BenchmarkLinkingStringEnrichment(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + txn := app.Application.StartTransaction("my txn") + defer txn.End() + record := slog.Record{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + enrichRecordTxn(txn, &record) + } +} + +func BenchmarkStringBuilder(b *testing.B) { + md := newrelic.LinkingMetadata{ + EntityGUID: "entityGUID", + Hostname: "hostname", + TraceID: "traceID", + SpanID: "spanID", + EntityName: "entityName", + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { - a = bytes.NewBuffer([]byte{}) + nrLinkingString(md) } - a.Reset() } diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go new file mode 100644 index 000000000..d96f7d957 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/linking.go @@ -0,0 +1,74 @@ +package nrslog + +import ( + "log/slog" + "strings" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +const ( + nrlinking = "NR-LINKING" + key = "newrelic" +) + +func enrichRecord(app *newrelic.Application, record *slog.Record) { + if !shouldEnrichLog(app) { + return + } + + str := nrLinkingString(app.GetLinkingMetadata()) + if str == "" { + return + } + + record.AddAttrs(slog.String(key, str)) +} + +func enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { + if !shouldEnrichLog(txn.Application()) { + return + } + + str := nrLinkingString(txn.GetLinkingMetadata()) + if str == "" { + return + } + + record.AddAttrs(slog.String(key, str)) +} + +func shouldEnrichLog(app *newrelic.Application) bool { + config, ok := app.Config() + if !ok { + return false + } + + return config.ApplicationLogging.Enabled && config.ApplicationLogging.LocalDecorating.Enabled +} + +// nrLinkingString returns a string that represents the linking metadata +func nrLinkingString(data newrelic.LinkingMetadata) string { + if data.EntityGUID == "" { + return "" + } + + len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) + str := strings.Builder{} + str.Grow(len) // only 1 alloc + + str.WriteString(nrlinking) + str.WriteByte('|') + str.WriteString(data.EntityGUID) + str.WriteByte('|') + str.WriteString(data.Hostname) + str.WriteByte('|') + str.WriteString(data.TraceID) + str.WriteByte('|') + str.WriteString(data.SpanID) + str.WriteByte('|') + str.WriteString(data.EntityName) + str.WriteByte('|') + + return str.String() +} diff --git a/v3/newrelic/application.go b/v3/newrelic/application.go index 06f3c6413..e2691b04f 100644 --- a/v3/newrelic/application.go +++ b/v3/newrelic/application.go @@ -39,6 +39,31 @@ func (app *Application) IsAIMonitoringEnabled(integration string, streaming bool } */ +// GetLinkingMetadata returns the fields needed to link data to +// an entity. This will return an empty struct if the application +// is not connected or nil. +func (app *Application) GetLinkingMetadata() LinkingMetadata { + if app == nil || app.app == nil { + return LinkingMetadata{} + } + + reply, err := app.app.getState() + if err != nil { + app.app.Error("unable to record custom event", map[string]interface{}{ + "event-type": "AppState", + "reason": err.Error(), + }) + } + + md := LinkingMetadata{ + EntityName: app.app.config.AppName, + Hostname: app.app.config.hostname, + EntityGUID: reply.Reply.EntityGUID, + } + + return md +} + // StartTransaction begins a Transaction with the given name. func (app *Application) StartTransaction(name string, opts ...TraceOption) *Transaction { if app == nil { From 8655644ac4e133b490296f9ab63b631c448b24a0 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Mon, 3 Feb 2025 06:59:59 -0800 Subject: [PATCH 06/14] fixed yoda --- v3/examples/oom/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/examples/oom/main.go b/v3/examples/oom/main.go index 6ad9a8a07..396e041da 100644 --- a/v3/examples/oom/main.go +++ b/v3/examples/oom/main.go @@ -26,7 +26,7 @@ func main() { } // Wait for the application to connect. - if err := app.WaitForConnection(5 * time.Second); nil != err { + if err := app.WaitForConnection(5 * time.Second); err != nil { fmt.Println(err) } From ee64b76f4dd02295149de96ef240a8ce3ad9aae6 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 5 Feb 2025 16:03:38 -0500 Subject: [PATCH 07/14] working attribute capture system --- .../logcontext-v2/nrslog/handler.go | 199 ++++++++++++++---- .../logcontext-v2/nrslog/handler_test.go | 128 ++++++++++- .../logcontext-v2/nrslog/linking.go | 9 + v3/integrations/logcontext-v2/nrslog/wrap.go | 25 +-- 4 files changed, 302 insertions(+), 59 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 938adbc6a..6f1d7c32a 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -1,8 +1,11 @@ package nrslog import ( + "bytes" "context" "log/slog" + "slices" + "strings" "time" "github.com/newrelic/go-agent/v3/newrelic" @@ -11,20 +14,64 @@ import ( // NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context type NRHandler struct { handler slog.Handler - app *newrelic.Application - txn *newrelic.Transaction + + app *newrelic.Application + txn *newrelic.Transaction + + // group logic + goas []groupOrAttrs } -// WithTransaction returns a new handler that is configured to capture log data -// and attribute it to a specific transaction. -func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { - handler := NRHandler{ +type groupOrAttrs struct { + group string + attrs []slog.Attr +} + +// 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. +// +// Note: if your app is nil, or your handler is already wrapped with a NRHandler, WrapHandler will return +// the handler as is. +// +// TODO: does this need to return an error? +func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { + if app == nil { + return handler + } + switch handler.(type) { + case *NRHandler: + return handler + default: + return &NRHandler{ + handler: handler, + app: app, + } + } +} + +// New Returns a new slog.Logger object wrapped with a New Relic handler that controls +// logs in context features. +func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { + return slog.New(WrapHandler(app, handler)) +} + +// clone duplicates the handler, creating a new instance with the same configuration. +// This is a deep copy. +func (h *NRHandler) clone() *NRHandler { + return &NRHandler{ handler: h.handler, app: h.app, - txn: txn, + txn: h.txn, + goas: slices.Clone(h.goas), } +} - return &handler +// WithTransaction returns a new handler that is configured to capture log data +// and attribute it to a specific transaction. +func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { + h2 := h.clone() + h2.txn = txn + return h2 } // Enabled reports whether the handler handles records at the given level. @@ -43,21 +90,11 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { nrTxn = ctxTxn } - // if no app or txn, do nothing + // if no app or txn, invoke underlying handler if h.app == nil && nrTxn == nil { return h.handler.Handle(ctx, record) } - 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 var timestamp int64 if record.Time.IsZero() { @@ -66,34 +103,111 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { timestamp = record.Time.UnixMilli() } - data := newrelic.LogData{ - Severity: record.Level.String(), - Timestamp: timestamp, - Message: record.Message, - Attributes: attrs, + goas := h.goas + if record.NumAttrs() == 0 { + // If the record has no Attrs, remove groups at the end of the list; they are empty. + for len(goas) > 0 && goas[len(goas)-1].group != "" { + goas = goas[:len(goas)-1] + } + } + + var data newrelic.LogData + + // TODO: can we cache this? + if shouldForwardLogs(h.app) { + // TODO: optimize this to avoid maps, its very expensive + attrs := map[string]interface{}{} + groupPrefix := strings.Builder{} + + for _, goa := range goas { + if goa.group != "" { + if len(groupPrefix.String()) > 0 { + groupPrefix.WriteByte('.') + } + groupPrefix.WriteString(goa.group) + } else { + for _, a := range goa.attrs { + h.appendAttr(attrs, a, groupPrefix.String()) + } + } + } + + record.Attrs(func(attr slog.Attr) bool { + h.appendAttr(attrs, attr, groupPrefix.String()) + return true + }) + + data = newrelic.LogData{ + Severity: record.Level.String(), + Timestamp: timestamp, + Message: record.Message, + Attributes: attrs, + } } if nrTxn != nil { - nrTxn.RecordLog(data) + if data.Message != "" { + nrTxn.RecordLog(data) + } enrichRecordTxn(nrTxn, &record) } else { - h.app.RecordLog(data) + if data.Message != "" { + h.app.RecordLog(data) + } enrichRecord(h.app, &record) } return h.handler.Handle(ctx, record) } +func (h *NRHandler) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) { + // Resolve the Attr's value before doing anything else. + a.Value = a.Value.Resolve() + // Ignore empty Attrs. + if a.Equal(slog.Attr{}) { + return + } + + groupBuffer := bytes.Buffer{} + groupBuffer.WriteString(groupPrefix) + + if groupBuffer.Len() > 0 { + groupBuffer.WriteByte('.') + } + groupBuffer.WriteString(a.Key) + key := groupBuffer.String() + + // If the Attr is a group, append its attributes + if a.Value.Kind() == slog.KindGroup { + attrs := a.Value.Group() + // Ignore empty groups. + if len(attrs) == 0 { + return + } + + for _, ga := range attrs { + h.appendAttr(nrAttrs, ga, key) + } + return + } + + // attr is an attribute + nrAttrs[key] = a.Value.Any() +} + // 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. +// +// This wraps the WithAttrs of the underlying handler, and will not modify the +// attributes slice in any way. 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 } + + newHandler := h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) + newHandler.handler = newHandler.handler.WithAttrs(attrs) + return newHandler } // WithGroup returns a new Handler with the given group appended to @@ -102,18 +216,27 @@ func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { // Record, should be qualified by the sequence of group names. // 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 } + + newHandler := h.withGroupOrAttrs(groupOrAttrs{group: name}) + newHandler.handler = newHandler.handler.WithGroup(name) + return newHandler +} + +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 } // WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic // transaction from context. // // Deprecated: this is a no-op -func WithTransactionFromContext(handler *NRHandler) *NRHandler { +func WithTransactionFromContext(handler slog.Handler) slog.Handler { return handler } diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 26ab72fbe..a7cf64f8c 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -3,6 +3,7 @@ package nrslog import ( "bytes" "context" + "io" "log/slog" "strings" "testing" @@ -313,7 +314,8 @@ func TestWithGroup(t *testing.T) { handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) log := slog.New(handler) message := "Hello World!" - log = log.With(slog.Group("test group", slog.String("string key", "val"))) + group := slog.Group("test group", slog.String("string key", "val")) + log = log.With(group) log = log.WithGroup("test group") log.Info(message) @@ -338,7 +340,7 @@ func TestWithGroup(t *testing.T) { } // Ensure deprecation compatibility -func TestTransactionFromContextHandler(t *testing.T) { +func TestDeprecatedWithTransactionFromContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), @@ -375,6 +377,112 @@ func TestTransactionFromContextHandler(t *testing.T) { }) } +func TestAttributeCapture(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + + message := "Hello World!" + attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) + log := New(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + + log.Info(message, attr) + fooLog := log.WithGroup("foo") + fooLog.Info(message, attr) + + app.ExpectLogEvents(t, []internal.WantLog{ + { + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + Attributes: map[string]interface{}{ + "group.key": "val", + "group.group2.key2": "val2", + }, + }, + { + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + Attributes: map[string]interface{}{ + "foo.group.key": "val", + "foo.group.group2.key2": "val2", + }, + }, + }) +} + +func TestAppendAttr(t *testing.T) { + h := &NRHandler{} + nrAttrs := map[string]interface{}{} + + attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) + h.appendAttr(nrAttrs, attr, "") + if len(nrAttrs) != 2 { + t.Errorf("expected 2 attributes, got %d", len(nrAttrs)) + } + + entry1, ok := nrAttrs["group.key"] + if !ok { + t.Errorf("expected group.key to be in the map") + } + if entry1 != "val" { + t.Errorf("expected value of 'group.key' to be val, got '%s'", entry1) + } + + entry2, ok := nrAttrs["group.group2.key2"] + if !ok { + t.Errorf("expected group.group2.key2 to be in the map") + } + if entry2 != "val2" { + t.Errorf("expected value of 'group.group2.key2' to be val2, got '%s'", entry2) + } +} + +func TestAppendAttrWithGroupPrefix(t *testing.T) { + h := &NRHandler{} + nrAttrs := map[string]interface{}{} + + attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) + h.appendAttr(nrAttrs, attr, "prefix") + + if len(nrAttrs) != 2 { + t.Errorf("expected 2 attributes, got %d", len(nrAttrs)) + } + + entry1, ok := nrAttrs["prefix.group.key"] + if !ok { + t.Errorf("expected group.key to be in the map") + } + if entry1 != "val" { + t.Errorf("expected value of 'group.key' to be val, got '%s'", entry1) + } + + entry2, ok := nrAttrs["prefix.group.group2.key2"] + if !ok { + t.Errorf("expected group.group2.key2 to be in the map") + } + if entry2 != "val2" { + t.Errorf("expected value of 'group.group2.key2' to be val2, got '%s'", entry2) + } +} + +// the maps are costing so much here +func BenchmarkAppendAttribute(b *testing.B) { + h := &NRHandler{} + nrAttrs := map[string]interface{}{} + + attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + h.appendAttr(nrAttrs, attr, "") + } +} + func BenchmarkEnrichLog(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), @@ -429,3 +537,19 @@ func BenchmarkStringBuilder(b *testing.B) { nrLinkingString(md) } } + +func BenchmarkShouldEnrichLog(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + txn := app.Application.StartTransaction("my txn") + defer txn.End() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + shouldEnrichLog(app.Application) + } +} diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go index d96f7d957..53824fccc 100644 --- a/v3/integrations/logcontext-v2/nrslog/linking.go +++ b/v3/integrations/logcontext-v2/nrslog/linking.go @@ -47,6 +47,15 @@ func shouldEnrichLog(app *newrelic.Application) bool { return config.ApplicationLogging.Enabled && config.ApplicationLogging.LocalDecorating.Enabled } +func shouldForwardLogs(app *newrelic.Application) bool { + config, ok := app.Config() + if !ok { + return false + } + + return config.ApplicationLogging.Enabled && config.ApplicationLogging.Forwarding.Enabled +} + // nrLinkingString returns a string that represents the linking metadata func nrLinkingString(data newrelic.LinkingMetadata) string { if data.EntityGUID == "" { diff --git a/v3/integrations/logcontext-v2/nrslog/wrap.go b/v3/integrations/logcontext-v2/nrslog/wrap.go index e533bb49c..ef35ab7be 100644 --- a/v3/integrations/logcontext-v2/nrslog/wrap.go +++ b/v3/integrations/logcontext-v2/nrslog/wrap.go @@ -13,7 +13,7 @@ import ( // application. // // Deprecated: Use WrapHandler() instead. -func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { +func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) slog.Handler { return WrapHandler(app, slog.NewTextHandler(w, opts)) } @@ -22,28 +22,14 @@ func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptio // application. // // Deprecated: Use WrapHandler() instead. -func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) *NRHandler { +func JSONHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) slog.Handler { return WrapHandler(app, slog.NewJSONHandler(w, opts)) } -// 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 { - return &NRHandler{ - handler: handler, - app: app, - } -} - -// New Returns a new slog.Logger object wrapped with a New Relic handler that controls -// logs in context features. -func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { - return slog.New(WrapHandler(app, handler)) -} - // WithTransaction creates a new Slog Logger object to be used for logging within a given // transaction it its found in a context. Creating a transaction logger can have a performance -// benefit when transactions are long running, and have a high log volume. +// benefit when transactions are long running, and have a high log volume in comparison to +// reading transactions from context on every log message. // // Note: transaction contexts can also be passed to the logger without creating a new // logger using logger.InfoContext() or similar commands. @@ -58,7 +44,8 @@ func WithContext(ctx context.Context, logger *slog.Logger) *slog.Logger { // WithTransaction creates a new Slog Logger object to be used for logging // within a given transaction. Creating a transaction logger can have a performance -// benefit when transactions are long running, and have a high log volume. +// benefit when transactions are long running, and have a high log volume in comparison to +// reading transactions from context on every log message. // // Note: transaction contexts can also be passed to the logger without creating a new // logger using logger.InfoContext() or similar commands. From 07f6f786d16eafe0c88e41423b6cb8b3572e7dac Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Thu, 6 Feb 2025 14:54:19 -0500 Subject: [PATCH 08/14] caching elements to increase speed --- .../logcontext-v2/nrslog/attribute_cache.go | 97 +++++++ .../logcontext-v2/nrslog/config_cache.go | 66 +++++ .../logcontext-v2/nrslog/handler.go | 181 +++++++------ .../logcontext-v2/nrslog/handler_test.go | 246 ++++++++++++++---- .../logcontext-v2/nrslog/linking.go | 83 ------ 5 files changed, 460 insertions(+), 213 deletions(-) create mode 100644 v3/integrations/logcontext-v2/nrslog/attribute_cache.go create mode 100644 v3/integrations/logcontext-v2/nrslog/config_cache.go delete mode 100644 v3/integrations/logcontext-v2/nrslog/linking.go diff --git a/v3/integrations/logcontext-v2/nrslog/attribute_cache.go b/v3/integrations/logcontext-v2/nrslog/attribute_cache.go new file mode 100644 index 000000000..57d9653a0 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/attribute_cache.go @@ -0,0 +1,97 @@ +package nrslog + +import ( + "log/slog" + "maps" + "strings" +) + +type attributeCache struct { + preCompiledAttributes map[string]interface{} + prefix string +} + +func (c *attributeCache) getPreCompiledAttributes() map[string]interface{} { + if c.preCompiledAttributes == nil { + return make(map[string]interface{}) + } + return maps.Clone(c.preCompiledAttributes) +} + +func (c *attributeCache) getPrefix() string { + return c.prefix +} + +func (c *attributeCache) computePrecompiledAttributes(goas []groupOrAttrs) { + if len(goas) == 0 { + return + } + + // if just one element, we can avoid allocation for the sting builder + if len(goas) == 1 { + if goas[0].group != "" { + c.prefix = goas[0].group + } else { + attrs := make(map[string]interface{}) + for _, a := range goas[0].attrs { + c.appendAttr(attrs, a, "") + } + } + return + } + + // string builder worth the pre-allocation cost + groupPrefix := strings.Builder{} + attrs := make(map[string]interface{}) + + for _, goa := range goas { + if goa.group != "" { + if len(groupPrefix.String()) > 0 { + groupPrefix.WriteByte('.') + } + groupPrefix.WriteString(goa.group) + } else { + for _, a := range goa.attrs { + c.appendAttr(attrs, a, groupPrefix.String()) + } + } + } + + c.preCompiledAttributes = attrs + c.prefix = groupPrefix.String() +} + +func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) { + // Resolve the Attr's value before doing anything else. + a.Value = a.Value.Resolve() + // Ignore empty Attrs. + if a.Equal(slog.Attr{}) { + return + } + + group := strings.Builder{} + group.WriteString(groupPrefix) + + if group.Len() > 0 { + group.WriteByte('.') + } + group.WriteString(a.Key) + key := group.String() + + // If the Attr is a group, append its attributes + if a.Value.Kind() == slog.KindGroup { + attrs := a.Value.Group() + // Ignore empty groups. + if len(attrs) == 0 { + return + } + + for _, ga := range attrs { + c.appendAttr(nrAttrs, ga, key) + } + return + } + + // attr is an attribute + nrAttrs[key] = a.Value.Any() +} diff --git a/v3/integrations/logcontext-v2/nrslog/config_cache.go b/v3/integrations/logcontext-v2/nrslog/config_cache.go new file mode 100644 index 000000000..370ba8d29 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/config_cache.go @@ -0,0 +1,66 @@ +package nrslog + +import ( + "time" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +const updateFrequency = 1 * time.Minute // check infrequently because the go agent config is not expected to change --> cost 50-100 uS + +// 44% faster than checking the config on every log message +type configCache struct { + lastCheck time.Time + + // true if we have successfully gotten the config at least once to verify the agent is connected + gotStartupConfig bool + // true if the logs in context feature is enabled as well as either local decorating or forwarding + enabled bool + enrichLogs bool + forwardLogs bool +} + +func (c *configCache) shouldEnrichLog(app *newrelic.Application) bool { + c.update(app) + return c.enrichLogs +} + +func (c *configCache) shouldForwardLogs(app *newrelic.Application) bool { + c.update(app) + return c.forwardLogs +} + +// isEnabled returns true if the logs in context feature is enabled +// as well as either local decorating or forwarding. +func (c *configCache) isEnabled(app *newrelic.Application) bool { + c.update(app) + return c.enabled +} + +// Note: this has a data race in async use cases, but it does not +// cause logical errors, only cache misses. This is acceptable in +// comparison to the cost of synchronization. +func (c *configCache) update(app *newrelic.Application) { + // do not get the config from agent if we have successfully gotten it before + // and it has been less than updateFrequency since the last check. This is + // because on startup, the agent will return a dummy config until it has + // connected and received the real config. + if c.gotStartupConfig && time.Since(c.lastCheck) < updateFrequency { + return + } + + config, ok := app.Config() + if !ok { + c.enrichLogs = false + c.forwardLogs = false + c.enabled = false + return + } + + c.gotStartupConfig = true + c.enrichLogs = config.ApplicationLogging.LocalDecorating.Enabled && config.ApplicationLogging.Enabled + c.forwardLogs = config.ApplicationLogging.Forwarding.Enabled && config.ApplicationLogging.Enabled + c.enabled = config.ApplicationLogging.Enabled && (c.enrichLogs || c.forwardLogs) + + c.lastCheck = time.Now() +} diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 6f1d7c32a..78a599922 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -1,8 +1,8 @@ package nrslog import ( - "bytes" "context" + "errors" "log/slog" "slices" "strings" @@ -13,10 +13,12 @@ import ( // NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context type NRHandler struct { - handler slog.Handler + configCache + attributeCache - app *newrelic.Application - txn *newrelic.Transaction + handler slog.Handler + app *newrelic.Application + txn *newrelic.Transaction // group logic goas []groupOrAttrs @@ -30,14 +32,17 @@ type groupOrAttrs struct { // 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. // -// Note: if your app is nil, or your handler is already wrapped with a NRHandler, WrapHandler will return -// the handler as is. -// -// TODO: does this need to return an error? +// Note: This function will silently fail, and always return a valid handler +// to avoid service disruptions. If you would prefer to handle when wrapping +// fails, use Wrap() instead. func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { if app == nil { return handler } + if handler == nil { + return handler + } + switch handler.(type) { case *NRHandler: return handler @@ -49,6 +54,29 @@ func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { } } +var ErrNilApp = errors.New("New Relic application cannot be nil") +var ErrNilHandler = errors.New("slog handler cannot be nil") +var ErrAlreadyWrapped = errors.New("handler is already wrapped with a New Relic handler") + +// 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 Wrap(app *newrelic.Application, handler slog.Handler) (*NRHandler, error) { + if app == nil { + return nil, ErrNilApp + } + if handler == nil { + return nil, ErrNilHandler + } + if _, ok := handler.(*NRHandler); ok { + return nil, ErrAlreadyWrapped + } + + return &NRHandler{ + handler: handler, + app: app, + }, nil +} + // New Returns a new slog.Logger object wrapped with a New Relic handler that controls // logs in context features. func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { @@ -83,8 +111,14 @@ func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool { // Handle handles the Record. // It will only be called when Enabled returns true. func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { - nrTxn := h.txn + // exit quickly logs in context is disabled in the agent + // to preserve resources + if !h.isEnabled(h.app) { + return h.handler.Handle(ctx, record) + } + // get transaction, preferring transaction from context + nrTxn := h.txn ctxTxn := newrelic.FromContext(ctx) if ctxTxn != nil { nrTxn = ctxTxn @@ -103,37 +137,14 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { timestamp = record.Time.UnixMilli() } - goas := h.goas - if record.NumAttrs() == 0 { - // If the record has no Attrs, remove groups at the end of the list; they are empty. - for len(goas) > 0 && goas[len(goas)-1].group != "" { - goas = goas[:len(goas)-1] - } - } - var data newrelic.LogData - // TODO: can we cache this? - if shouldForwardLogs(h.app) { - // TODO: optimize this to avoid maps, its very expensive - attrs := map[string]interface{}{} - groupPrefix := strings.Builder{} - - for _, goa := range goas { - if goa.group != "" { - if len(groupPrefix.String()) > 0 { - groupPrefix.WriteByte('.') - } - groupPrefix.WriteString(goa.group) - } else { - for _, a := range goa.attrs { - h.appendAttr(attrs, a, groupPrefix.String()) - } - } - } + if h.shouldForwardLogs(h.app) { + attrs := h.getPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map + prefix := h.getPrefix() record.Attrs(func(attr slog.Attr) bool { - h.appendAttr(attrs, attr, groupPrefix.String()) + h.appendAttr(attrs, attr, prefix) return true }) @@ -149,52 +160,17 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { if data.Message != "" { nrTxn.RecordLog(data) } - enrichRecordTxn(nrTxn, &record) + h.enrichRecordTxn(nrTxn, &record) } else { if data.Message != "" { h.app.RecordLog(data) } - enrichRecord(h.app, &record) + h.enrichRecord(h.app, &record) } return h.handler.Handle(ctx, record) } -func (h *NRHandler) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) { - // Resolve the Attr's value before doing anything else. - a.Value = a.Value.Resolve() - // Ignore empty Attrs. - if a.Equal(slog.Attr{}) { - return - } - - groupBuffer := bytes.Buffer{} - groupBuffer.WriteString(groupPrefix) - - if groupBuffer.Len() > 0 { - groupBuffer.WriteByte('.') - } - groupBuffer.WriteString(a.Key) - key := groupBuffer.String() - - // If the Attr is a group, append its attributes - if a.Value.Kind() == slog.KindGroup { - attrs := a.Value.Group() - // Ignore empty groups. - if len(attrs) == 0 { - return - } - - for _, ga := range attrs { - h.appendAttr(nrAttrs, ga, key) - } - return - } - - // attr is an attribute - nrAttrs[key] = a.Value.Any() -} - // WithAttrs returns a new Handler whose attributes consist of // both the receiver's attributes and the arguments. // @@ -207,6 +183,7 @@ func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { newHandler := h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) newHandler.handler = newHandler.handler.WithAttrs(attrs) + newHandler.computePrecompiledAttributes(newHandler.goas) return newHandler } @@ -222,6 +199,7 @@ func (h *NRHandler) WithGroup(name string) slog.Handler { newHandler := h.withGroupOrAttrs(groupOrAttrs{group: name}) newHandler.handler = newHandler.handler.WithGroup(name) + newHandler.computePrecompiledAttributes(newHandler.goas) return newHandler } @@ -240,3 +218,60 @@ func (h *NRHandler) withGroupOrAttrs(goa groupOrAttrs) *NRHandler { func WithTransactionFromContext(handler slog.Handler) slog.Handler { return handler } + +const ( + nrlinking = "NR-LINKING" + key = "newrelic" +) + +func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) { + if !h.shouldEnrichLog(app) { + return + } + + str := nrLinkingString(app.GetLinkingMetadata()) + if str == "" { + return + } + + record.AddAttrs(slog.String(key, str)) +} + +func (h *NRHandler) enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { + if !h.shouldEnrichLog(txn.Application()) { + return + } + + str := nrLinkingString(txn.GetLinkingMetadata()) + if str == "" { + return + } + + record.AddAttrs(slog.String(key, str)) +} + +// nrLinkingString returns a string that represents the linking metadata +func nrLinkingString(data newrelic.LinkingMetadata) string { + if data.EntityGUID == "" { + return "" + } + + len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) + str := strings.Builder{} + str.Grow(len) // only 1 alloc + + str.WriteString(nrlinking) + str.WriteByte('|') + str.WriteString(data.EntityGUID) + str.WriteByte('|') + str.WriteString(data.Hostname) + str.WriteByte('|') + str.WriteString(data.TraceID) + str.WriteByte('|') + str.WriteString(data.SpanID) + str.WriteByte('|') + str.WriteString(data.EntityName) + str.WriteByte('|') + + return str.String() +} diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index a7cf64f8c..6fba05a33 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -5,8 +5,10 @@ import ( "context" "io" "log/slog" + "os" "strings" "testing" + "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" @@ -43,6 +45,77 @@ func TestHandler(t *testing.T) { }) } +func TestWrap(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) + + type test struct { + name string + app *newrelic.Application + h slog.Handler + expectErr error + expectHandler *NRHandler + } + + tests := []test{ + { + name: "nil app", + app: nil, + h: handler, + expectErr: ErrNilApp, + expectHandler: nil, + }, + { + name: "nil handler", + app: app.Application, + h: nil, + expectErr: ErrNilHandler, + expectHandler: nil, + }, + { + name: "duplicated handler", + app: app.Application, + h: &NRHandler{}, + expectErr: ErrAlreadyWrapped, + expectHandler: nil, + }, + { + name: "valid", + app: app.Application, + h: handler, + expectErr: nil, + expectHandler: &NRHandler{ + app: app.Application, + handler: handler, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h, err := Wrap(tt.app, tt.h) + if err != tt.expectErr { + t.Errorf("incorrect error return; expected: %v; got: %v", tt.expectErr, err) + } + if tt.expectHandler != nil { + if h == nil { + t.Errorf("expected handler to not be nil") + } + if tt.expectHandler.app != h.app { + t.Errorf("expected: %v; got: %v", tt.expectHandler.app, h.app) + } + if tt.expectHandler.handler != h.handler { + t.Errorf("expected: %v; got: %v", tt.expectHandler.handler, h.handler) + } + } else if h != nil { + t.Errorf("expected handler to be nil") + } + }) + } +} + func TestHandlerNilApp(t *testing.T) { out := bytes.NewBuffer([]byte{}) logger := New(nil, slog.NewTextHandler(out, &slog.HandlerOptions{})) @@ -256,12 +329,6 @@ func TestWithAttributesFromContext(t *testing.T) { Hostname: host, }) - logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ - EntityGUID: integrationsupport.TestEntityGUID, - EntityName: integrationsupport.SampleAppName, - Hostname: host, - }) - // purge the buffer writer.Reset() @@ -271,11 +338,23 @@ func TestWithAttributesFromContext(t *testing.T) { log.InfoContext(ctx, "I am a log inside a transaction with custom attributes!", slog.String("foo", "bar"), slog.Int("answer", 42), - slog.Any("some_map", map[string]interface{}{"a": 1.0, "b": 2}), ) metadata := txn.GetTraceMetadata() txn.End() + logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + EntityName: integrationsupport.SampleAppName, + Hostname: host, + TraceID: metadata.TraceID, + SpanID: metadata.SpanID, + }) + + writer.Reset() + + gLog := log.WithGroup("group1") + gLog.Info("I am a log message inside a group", slog.String("foo", "bar"), slog.Int("answer", 42)) + app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), @@ -287,56 +366,22 @@ func TestWithAttributesFromContext(t *testing.T) { Message: "I am a log inside a transaction with custom attributes!", Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ - "foo": "bar", - "answer": 42, - "some_map": map[string]interface{}{"a": 1.0, "b": 2}, + "foo": "bar", + "answer": 42, }, TraceID: metadata.TraceID, SpanID: metadata.SpanID, }, + { + Severity: slog.LevelInfo.String(), + Message: "I am a log message inside a group", + Timestamp: internal.MatchAnyUnixMilli, + Attributes: map[string]interface{}{ + "group1.foo": "bar", + "group1.answer": 42, + }, + }, }) - - logcontext.ValidateDecoratedOutput(t, writer, &logcontext.DecorationExpect{ - EntityGUID: integrationsupport.TestEntityGUID, - EntityName: integrationsupport.SampleAppName, - Hostname: host, - TraceID: metadata.TraceID, - SpanID: metadata.SpanID, - }) -} - -func TestWithGroup(t *testing.T) { - app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, - newrelic.ConfigAppLogDecoratingEnabled(false), - newrelic.ConfigAppLogForwardingEnabled(true), - ) - out := bytes.NewBuffer([]byte{}) - handler := TextHandler(app.Application, out, &slog.HandlerOptions{}) - log := slog.New(handler) - message := "Hello World!" - group := slog.Group("test group", slog.String("string key", "val")) - log = log.With(group) - log = log.WithGroup("test group") - - log.Info(message) - - log1 := string(out.String()) - - txn := app.StartTransaction("hi") - txnLog := WithTransaction(txn, log) - txnLog.Info(message) - txn.End() - - log2 := string(out.String()) - - attrString := `"test group.string key"=val` - if !strings.Contains(log1, attrString) { - t.Errorf("expected %s to contain %s", log1, attrString) - } - - if !strings.Contains(log2, attrString) { - t.Errorf("expected %s to contain %s", log2, attrString) - } } // Ensure deprecation compatibility @@ -377,7 +422,7 @@ func TestDeprecatedWithTransactionFromContext(t *testing.T) { }) } -func TestAttributeCapture(t *testing.T) { +func TestWithComplexAttributeOrGroup(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), @@ -385,12 +430,14 @@ func TestAttributeCapture(t *testing.T) { message := "Hello World!" attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) - log := New(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + log := New(app.Application, slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) log.Info(message, attr) fooLog := log.WithGroup("foo") fooLog.Info(message, attr) + log.With(attr).WithGroup("group3").With(slog.String("key3", "val3")).Info(message) + app.ExpectLogEvents(t, []internal.WantLog{ { Severity: slog.LevelInfo.String(), @@ -410,6 +457,16 @@ func TestAttributeCapture(t *testing.T) { "foo.group.group2.key2": "val2", }, }, + { + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + Attributes: map[string]interface{}{ + "group.key": "val", + "group.group2.key2": "val2", + "group3.key3": "val3", + }, + }, }) } @@ -468,6 +525,78 @@ func TestAppendAttrWithGroupPrefix(t *testing.T) { } } +func TestHandlerZeroTime(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + out := bytes.NewBuffer([]byte{}) + handler := WrapHandler(app.Application, slog.NewTextHandler(out, &slog.HandlerOptions{})) + handler.Handle(context.Background(), slog.Record{ + Level: slog.LevelInfo, + Message: "Hello World!", + Time: time.Time{}, + }) + logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{ + EntityGUID: integrationsupport.TestEntityGUID, + Hostname: host, + EntityName: integrationsupport.SampleAppName, + }) + app.ExpectLogEvents(t, []internal.WantLog{ + { + Severity: slog.LevelInfo.String(), + Message: "Hello World!", + Timestamp: internal.MatchAnyUnixMilli, + }, + }) +} + +func BenchmarkDefaultHandler(b *testing.B) { + handler := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}) + record := slog.Record{ + Time: time.Now(), + Message: "Hello World!", + Level: slog.LevelInfo, + } + + ctx := context.Background() + record.AddAttrs(slog.String("key", "val"), slog.Int("int", 1)) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + handler.Handle(ctx, record) + } +} + +func BenchmarkHandler(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(false), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + handler, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + txn := app.Application.StartTransaction("my txn") + defer txn.End() + + ctx := newrelic.NewContext(context.Background(), txn) + + record := slog.Record{ + Time: time.Now(), + Message: "Hello World!", + Level: slog.LevelInfo, + } + + record.AddAttrs(slog.String("key", "val"), slog.Int("int", 1)) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + handler.Handle(ctx, record) + } +} + // the maps are costing so much here func BenchmarkAppendAttribute(b *testing.B) { h := &NRHandler{} @@ -509,6 +638,8 @@ func BenchmarkLinkingStringEnrichment(b *testing.B) { newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) + + h, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) txn := app.Application.StartTransaction("my txn") defer txn.End() record := slog.Record{} @@ -517,11 +648,11 @@ func BenchmarkLinkingStringEnrichment(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - enrichRecordTxn(txn, &record) + h.enrichRecordTxn(txn, &record) } } -func BenchmarkStringBuilder(b *testing.B) { +func BenchmarkLinkingString(b *testing.B) { md := newrelic.LinkingMetadata{ EntityGUID: "entityGUID", Hostname: "hostname", @@ -543,6 +674,7 @@ func BenchmarkShouldEnrichLog(b *testing.B) { newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) + h, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) txn := app.Application.StartTransaction("my txn") defer txn.End() @@ -550,6 +682,6 @@ func BenchmarkShouldEnrichLog(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - shouldEnrichLog(app.Application) + h.shouldEnrichLog(app.Application) } } diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go deleted file mode 100644 index 53824fccc..000000000 --- a/v3/integrations/logcontext-v2/nrslog/linking.go +++ /dev/null @@ -1,83 +0,0 @@ -package nrslog - -import ( - "log/slog" - "strings" - - "github.com/newrelic/go-agent/v3/newrelic" -) - -const ( - nrlinking = "NR-LINKING" - key = "newrelic" -) - -func enrichRecord(app *newrelic.Application, record *slog.Record) { - if !shouldEnrichLog(app) { - return - } - - str := nrLinkingString(app.GetLinkingMetadata()) - if str == "" { - return - } - - record.AddAttrs(slog.String(key, str)) -} - -func enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { - if !shouldEnrichLog(txn.Application()) { - return - } - - str := nrLinkingString(txn.GetLinkingMetadata()) - if str == "" { - return - } - - record.AddAttrs(slog.String(key, str)) -} - -func shouldEnrichLog(app *newrelic.Application) bool { - config, ok := app.Config() - if !ok { - return false - } - - return config.ApplicationLogging.Enabled && config.ApplicationLogging.LocalDecorating.Enabled -} - -func shouldForwardLogs(app *newrelic.Application) bool { - config, ok := app.Config() - if !ok { - return false - } - - return config.ApplicationLogging.Enabled && config.ApplicationLogging.Forwarding.Enabled -} - -// nrLinkingString returns a string that represents the linking metadata -func nrLinkingString(data newrelic.LinkingMetadata) string { - if data.EntityGUID == "" { - return "" - } - - len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) - str := strings.Builder{} - str.Grow(len) // only 1 alloc - - str.WriteString(nrlinking) - str.WriteByte('|') - str.WriteString(data.EntityGUID) - str.WriteByte('|') - str.WriteString(data.Hostname) - str.WriteByte('|') - str.WriteString(data.TraceID) - str.WriteByte('|') - str.WriteString(data.SpanID) - str.WriteByte('|') - str.WriteString(data.EntityName) - str.WriteByte('|') - - return str.String() -} From f3864bd68221355f13b56e5bf8922ddba1f06d3e Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 7 Feb 2025 15:16:34 -0500 Subject: [PATCH 09/14] more tuning --- .../{attribute_cache.go => attributes.go} | 11 + .../nrslog/{config_cache.go => config.go} | 14 + .../logcontext-v2/nrslog/handler.go | 73 +++--- .../logcontext-v2/nrslog/handler_test.go | 7 +- .../logcontext-v2/nrslog/linking.go | 80 ++++++ .../logcontext-v2/nrslog/linking_test.go | 248 ++++++++++++++++++ 6 files changed, 393 insertions(+), 40 deletions(-) rename v3/integrations/logcontext-v2/nrslog/{attribute_cache.go => attributes.go} (88%) rename v3/integrations/logcontext-v2/nrslog/{config_cache.go => config.go} (87%) create mode 100644 v3/integrations/logcontext-v2/nrslog/linking.go create mode 100644 v3/integrations/logcontext-v2/nrslog/linking_test.go diff --git a/v3/integrations/logcontext-v2/nrslog/attribute_cache.go b/v3/integrations/logcontext-v2/nrslog/attributes.go similarity index 88% rename from v3/integrations/logcontext-v2/nrslog/attribute_cache.go rename to v3/integrations/logcontext-v2/nrslog/attributes.go index 57d9653a0..0181d89be 100644 --- a/v3/integrations/logcontext-v2/nrslog/attribute_cache.go +++ b/v3/integrations/logcontext-v2/nrslog/attributes.go @@ -11,6 +11,17 @@ type attributeCache struct { prefix string } +func newAttributeCache() *attributeCache { + return &attributeCache{} +} + +func (c *attributeCache) clone() *attributeCache { + return &attributeCache{ + preCompiledAttributes: maps.Clone(c.preCompiledAttributes), + prefix: c.prefix, + } +} + func (c *attributeCache) getPreCompiledAttributes() map[string]interface{} { if c.preCompiledAttributes == nil { return make(map[string]interface{}) diff --git a/v3/integrations/logcontext-v2/nrslog/config_cache.go b/v3/integrations/logcontext-v2/nrslog/config.go similarity index 87% rename from v3/integrations/logcontext-v2/nrslog/config_cache.go rename to v3/integrations/logcontext-v2/nrslog/config.go index 370ba8d29..24e69a796 100644 --- a/v3/integrations/logcontext-v2/nrslog/config_cache.go +++ b/v3/integrations/logcontext-v2/nrslog/config.go @@ -20,6 +20,20 @@ type configCache struct { forwardLogs bool } +func newConfigCache() *configCache { + return &configCache{} +} + +func (c *configCache) clone() *configCache { + return &configCache{ + lastCheck: c.lastCheck, + gotStartupConfig: c.gotStartupConfig, + enabled: c.enabled, + enrichLogs: c.enrichLogs, + forwardLogs: c.forwardLogs, + } +} + func (c *configCache) shouldEnrichLog(app *newrelic.Application) bool { c.update(app) return c.enrichLogs diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 78a599922..93d0c9c29 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -13,8 +13,9 @@ import ( // NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context type NRHandler struct { - configCache - attributeCache + *attributeCache + *configCache + *linkingCache handler slog.Handler app *newrelic.Application @@ -29,6 +30,16 @@ type groupOrAttrs struct { attrs []slog.Attr } +func newHandler(app *newrelic.Application, handler slog.Handler) *NRHandler { + return &NRHandler{ + handler: handler, + attributeCache: newAttributeCache(), + configCache: newConfigCache(), + linkingCache: newLinkingCache(), + app: app, + } +} + // 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. // @@ -47,10 +58,7 @@ func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { case *NRHandler: return handler default: - return &NRHandler{ - handler: handler, - app: app, - } + return newHandler(app, handler) } } @@ -71,10 +79,7 @@ func Wrap(app *newrelic.Application, handler slog.Handler) (*NRHandler, error) { return nil, ErrAlreadyWrapped } - return &NRHandler{ - handler: handler, - app: app, - }, nil + return newHandler(app, handler), nil } // New Returns a new slog.Logger object wrapped with a New Relic handler that controls @@ -86,11 +91,15 @@ func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { // clone duplicates the handler, creating a new instance with the same configuration. // This is a deep copy. func (h *NRHandler) clone() *NRHandler { + return &NRHandler{ - handler: h.handler, - app: h.app, - txn: h.txn, - goas: slices.Clone(h.goas), + handler: h.handler, + attributeCache: h.attributeCache.clone(), + configCache: h.configCache.clone(), + linkingCache: h.linkingCache.clone(), + app: h.app, + txn: h.txn, + goas: slices.Clone(h.goas), } } @@ -137,8 +146,6 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { timestamp = record.Time.UnixMilli() } - var data newrelic.LogData - if h.shouldForwardLogs(h.app) { attrs := h.getPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map prefix := h.getPrefix() @@ -148,24 +155,26 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { return true }) - data = newrelic.LogData{ + data := newrelic.LogData{ Severity: record.Level.String(), Timestamp: timestamp, Message: record.Message, Attributes: attrs, } - } - - if nrTxn != nil { - if data.Message != "" { + if nrTxn != nil { nrTxn.RecordLog(data) - } - h.enrichRecordTxn(nrTxn, &record) - } else { - if data.Message != "" { + } else { h.app.RecordLog(data) } - h.enrichRecord(h.app, &record) + } + + // enrich logs + if h.shouldEnrichLog(h.app) { + if nrTxn != nil { + h.enrichRecordTxn(nrTxn, &record) + } else { + h.enrichRecord(h.app, &record) + } } return h.handler.Handle(ctx, record) @@ -225,11 +234,7 @@ const ( ) func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) { - if !h.shouldEnrichLog(app) { - return - } - - str := nrLinkingString(app.GetLinkingMetadata()) + str := nrLinkingString(h.getAgentLinkingMetadata(app)) if str == "" { return } @@ -238,11 +243,7 @@ func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) } func (h *NRHandler) enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { - if !h.shouldEnrichLog(txn.Application()) { - return - } - - str := nrLinkingString(txn.GetLinkingMetadata()) + str := nrLinkingString(h.getTransactionLinkingMetadata(txn)) if str == "" { return } diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 6fba05a33..8c8d3d089 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -5,7 +5,6 @@ import ( "context" "io" "log/slog" - "os" "strings" "testing" "time" @@ -50,7 +49,7 @@ func TestWrap(t *testing.T) { newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), ) - handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) + handler := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}) type test struct { name string @@ -430,7 +429,7 @@ func TestWithComplexAttributeOrGroup(t *testing.T) { message := "Hello World!" attr := slog.Group("group", slog.String("key", "val"), slog.Group("group2", slog.String("key2", "val2"))) - log := New(app.Application, slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) + log := New(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) log.Info(message, attr) fooLog := log.WithGroup("foo") @@ -648,7 +647,7 @@ func BenchmarkLinkingStringEnrichment(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - h.enrichRecordTxn(txn, &record) + h.enrichRecord(app.Application, &record) } } diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go new file mode 100644 index 000000000..574b2f21a --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/linking.go @@ -0,0 +1,80 @@ +package nrslog + +import "github.com/newrelic/go-agent/v3/newrelic" + +type linkingCache struct { + loaded bool + entityGUID string + entityName string + hostname string +} + +func newLinkingCache() *linkingCache { + return &linkingCache{} +} + +func (data *linkingCache) clone() *linkingCache { + return &linkingCache{ + entityGUID: data.entityGUID, + entityName: data.entityName, + hostname: data.hostname, + } +} + +// getAgentLinkingMetadata returns the linking metadata for the agent. +// we save a lot of time making calls to the go agent by caching the linking metadata +// which will never change during the lifetime of the agent. +// +// This returns a shallow copy of the cached metadata object +// 50% faster than calling GetLinkingMetadata() on every log message +// +// worst case: data race --> performance degrades to the cost of querying newrelic.Application.GetLinkingMetadata() +func (cache *linkingCache) getAgentLinkingMetadata(app *newrelic.Application) newrelic.LinkingMetadata { + // entityGUID will be empty until the agent has connected + if !cache.loaded { + metadata := app.GetLinkingMetadata() + cache.entityGUID = metadata.EntityGUID + cache.entityName = metadata.EntityName + cache.hostname = metadata.Hostname + + if cache.entityGUID != "" { + cache.loaded = true + } + return metadata + } + + return newrelic.LinkingMetadata{ + EntityGUID: cache.entityGUID, + EntityName: cache.entityName, + Hostname: cache.hostname, + } +} + +// getTransactionLinkingMetadata returns the linking metadata for a transaction. +// we save a lot of time making calls to the go agent by caching the linking metadata +// which will never change during the lifetime of the transaction. This still needs to +// query for the trace and span IDs, but this is much cheaper than getting the linking metadata. +// +// This returns a shallow copy of the cached metadata object +func (cache *linkingCache) getTransactionLinkingMetadata(txn *newrelic.Transaction) newrelic.LinkingMetadata { + if !cache.loaded { + metadata := txn.GetLinkingMetadata() // marginally more expensive + cache.entityGUID = metadata.EntityGUID + cache.entityName = metadata.EntityName + cache.hostname = metadata.Hostname + + if cache.entityGUID != "" { + cache.loaded = true + } + return metadata + } + + traceData := txn.GetTraceMetadata() + return newrelic.LinkingMetadata{ + EntityGUID: cache.entityGUID, + EntityName: cache.entityName, + Hostname: cache.hostname, + TraceID: traceData.TraceID, + SpanID: traceData.SpanID, + } +} diff --git a/v3/integrations/logcontext-v2/nrslog/linking_test.go b/v3/integrations/logcontext-v2/nrslog/linking_test.go new file mode 100644 index 000000000..3bc5c41b2 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/linking_test.go @@ -0,0 +1,248 @@ +package nrslog + +import ( + "os" + "reflect" + "testing" + + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" +) + +func Test_linkingCache_getAgentLinkingMetadata(t *testing.T) { + hostname, _ := os.Hostname() + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + + md := app.GetLinkingMetadata() + + tests := []struct { + name string + obj *linkingCache + app *newrelic.Application + wantMetadata newrelic.LinkingMetadata + wantCache linkingCache + }{ + { + name: "empty cache", + obj: &linkingCache{}, + app: app.Application, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: md.EntityGUID, + EntityName: "my app", + Hostname: hostname, + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: md.EntityGUID, + entityName: "my app", + hostname: hostname, + }, + }, + { + name: "loaded cache preserved", + obj: &linkingCache{ + loaded: true, + entityGUID: "test entity GUID", + entityName: "test app", + hostname: "test hostname", + }, + app: app.Application, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: "test entity GUID", + EntityName: "test app", + Hostname: "test hostname", + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: "test entity GUID", + entityName: "test app", + hostname: "test hostname", + }, + }, + { + name: "cache replaced when GUID is empty", + obj: &linkingCache{ + entityGUID: "", + entityName: "test app", + hostname: "test hostname", + }, + app: app.Application, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: md.EntityGUID, + EntityName: "my app", + Hostname: hostname, + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: md.EntityGUID, + entityName: "my app", + hostname: hostname, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.obj.getAgentLinkingMetadata(tt.app) + if got.EntityGUID != tt.wantMetadata.EntityGUID { + t.Errorf("got incorrect entity GUID for agent = %v, want %v", got.EntityGUID, tt.wantMetadata.EntityGUID) + } + if got.EntityName != tt.wantMetadata.EntityName { + t.Errorf("got incorrect entity name for agent = %v, want %v", got.EntityName, tt.wantMetadata.EntityName) + } + if got.Hostname != tt.wantMetadata.Hostname { + t.Errorf("got incorrect hostname for agent = %v, want %v", got.Hostname, tt.wantMetadata.Hostname) + } + if got.TraceID != tt.wantMetadata.TraceID { + t.Errorf("got incorrect trace ID for transaction = %v, want %v", got.TraceID, tt.wantMetadata.TraceID) + } + if got.SpanID != tt.wantMetadata.SpanID { + t.Errorf("got incorrect span ID for transaction = %v, want %v", got.SpanID, tt.wantMetadata.SpanID) + } + if !reflect.DeepEqual(tt.obj, &tt.wantCache) { + t.Errorf("linkingCache state is incorrect = %v, want %v", tt.obj, tt.wantCache) + } + }) + } +} + +func Test_linkingCache_getTransactionLinkingMetadata(t *testing.T) { + hostname, _ := os.Hostname() + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + txn := app.StartTransaction("txn") + defer txn.End() + + md := txn.GetLinkingMetadata() + + tests := []struct { + name string + obj *linkingCache + txn *newrelic.Transaction + wantMetadata newrelic.LinkingMetadata + wantCache linkingCache + }{ + { + name: "empty cache", + obj: &linkingCache{}, + txn: txn, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: md.EntityGUID, + EntityName: "my app", + Hostname: hostname, + TraceID: md.TraceID, + SpanID: md.SpanID, + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: md.EntityGUID, + entityName: "my app", + hostname: hostname, + }, + }, + { + name: "cache preserved when loaded", + obj: &linkingCache{ + loaded: true, + entityGUID: "test entity GUID", + entityName: "test app", + hostname: "test hostname", + }, + txn: txn, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: "test entity GUID", + EntityName: "test app", + Hostname: "test hostname", + TraceID: md.TraceID, + SpanID: md.SpanID, + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: "test entity GUID", + entityName: "test app", + hostname: "test hostname", + }, + }, + { + name: "cache replaced not fully loaded", + obj: &linkingCache{ + loaded: false, + entityGUID: "", + entityName: "test app", + hostname: "test hostname", + }, + txn: txn, + wantMetadata: newrelic.LinkingMetadata{ + EntityGUID: md.EntityGUID, + EntityName: "my app", + Hostname: hostname, + TraceID: md.TraceID, + SpanID: md.SpanID, + }, + wantCache: linkingCache{ + loaded: true, + entityGUID: md.EntityGUID, + entityName: "my app", + hostname: hostname, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.obj.getTransactionLinkingMetadata(tt.txn) + if got.EntityGUID != tt.wantMetadata.EntityGUID { + t.Errorf("got incorrect entity GUID for agent = %v, want %v", got.EntityGUID, tt.wantMetadata.EntityGUID) + } + if got.EntityName != tt.wantMetadata.EntityName { + t.Errorf("got incorrect entity name for agent = %v, want %v", got.EntityName, tt.wantMetadata.EntityName) + } + if got.Hostname != tt.wantMetadata.Hostname { + t.Errorf("got incorrect hostname for agent = %v, want %v", got.Hostname, tt.wantMetadata.Hostname) + } + if got.TraceID != tt.wantMetadata.TraceID { + t.Errorf("got incorrect trace ID for transaction = %v, want %v", got.TraceID, tt.wantMetadata.TraceID) + } + if got.SpanID != tt.wantMetadata.SpanID { + t.Errorf("got incorrect span ID for transaction = %v, want %v", got.SpanID, tt.wantMetadata.SpanID) + } + if !reflect.DeepEqual(tt.obj, &tt.wantCache) { + t.Errorf("linkingCache state is incorrect = %+v, want %+v", tt.obj, tt.wantCache) + } + }) + } +} + +func BenchmarkGetAgentLinkingMetadata(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + + cache := &linkingCache{} + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + cache.getAgentLinkingMetadata(app.Application) + } +} + +func BenchmarkGetTransactionLinkingMetadata(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + + txn := app.StartTransaction("txn") + defer txn.End() + + //cache := &linkingCache{} + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + txn.GetTraceMetadata() + } +} From e9de240211a5dd3ce639d09e495ae20d20494dc0 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Tue, 11 Feb 2025 15:55:19 -0500 Subject: [PATCH 10/14] improved storage and caching of pre-compiled attributes and groups --- .../logcontext-v2/nrslog/attributes.go | 67 +++++------- .../logcontext-v2/nrslog/handler.go | 102 ++++++------------ .../logcontext-v2/nrslog/handler_test.go | 71 ++++++++++-- .../logcontext-v2/nrslog/linking.go | 34 +++++- 4 files changed, 155 insertions(+), 119 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrslog/attributes.go b/v3/integrations/logcontext-v2/nrslog/attributes.go index 0181d89be..582069ca1 100644 --- a/v3/integrations/logcontext-v2/nrslog/attributes.go +++ b/v3/integrations/logcontext-v2/nrslog/attributes.go @@ -12,7 +12,10 @@ type attributeCache struct { } func newAttributeCache() *attributeCache { - return &attributeCache{} + return &attributeCache{ + preCompiledAttributes: make(map[string]interface{}), + prefix: "", + } } func (c *attributeCache) clone() *attributeCache { @@ -22,10 +25,7 @@ func (c *attributeCache) clone() *attributeCache { } } -func (c *attributeCache) getPreCompiledAttributes() map[string]interface{} { - if c.preCompiledAttributes == nil { - return make(map[string]interface{}) - } +func (c *attributeCache) copyPreCompiledAttributes() map[string]interface{} { return maps.Clone(c.preCompiledAttributes) } @@ -33,43 +33,27 @@ func (c *attributeCache) getPrefix() string { return c.prefix } -func (c *attributeCache) computePrecompiledAttributes(goas []groupOrAttrs) { - if len(goas) == 0 { - return +// precompileGroup sets the group prefix for the cache created by a handler +// precompileGroup call. This is used to avoid re-computing the group prefix +// and should only ever be called on newly created caches and handlers. +func (c *attributeCache) precompileGroup(group string) { + if c.prefix != "" { + c.prefix += "." } + c.prefix += group +} - // if just one element, we can avoid allocation for the sting builder - if len(goas) == 1 { - if goas[0].group != "" { - c.prefix = goas[0].group - } else { - attrs := make(map[string]interface{}) - for _, a := range goas[0].attrs { - c.appendAttr(attrs, a, "") - } - } +// precompileAttributes appends attributes to the cache created by a handler +// WithAttrs call. This is used to avoid re-computing the with Attrs attributes +// and should only ever be called on newly created caches and handlers. +func (c *attributeCache) precompileAttributes(attrs []slog.Attr) { + if len(attrs) == 0 { return } - // string builder worth the pre-allocation cost - groupPrefix := strings.Builder{} - attrs := make(map[string]interface{}) - - for _, goa := range goas { - if goa.group != "" { - if len(groupPrefix.String()) > 0 { - groupPrefix.WriteByte('.') - } - groupPrefix.WriteString(goa.group) - } else { - for _, a := range goa.attrs { - c.appendAttr(attrs, a, groupPrefix.String()) - } - } + for _, a := range attrs { + c.appendAttr(c.preCompiledAttributes, a, c.prefix) } - - c.preCompiledAttributes = attrs - c.prefix = groupPrefix.String() } func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) { @@ -80,13 +64,18 @@ func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, return } + // majority of runtime spent allocating and copying strings group := strings.Builder{} + group.Grow(len(groupPrefix) + len(a.Key) + 1) group.WriteString(groupPrefix) - if group.Len() > 0 { - group.WriteByte('.') + if a.Key != "" { + if group.Len() > 0 { + group.WriteByte('.') + } + group.WriteString(a.Key) } - group.WriteString(a.Key) + key := group.String() // If the Attr is a group, append its attributes diff --git a/v3/integrations/logcontext-v2/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 93d0c9c29..bad4b5153 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -4,32 +4,32 @@ import ( "context" "errors" "log/slog" - "slices" - "strings" "time" "github.com/newrelic/go-agent/v3/newrelic" ) -// NRHandler is an Slog handler that includes logic to implement New Relic Logs in Context +// NRHandler is an Slog handler that includes logic to implement +// New Relic Logs in Context. Please always create a new handler +// using the Wrap() or WrapHandler() functions to ensure proper +// initialization. +// +// Note: shallow coppies of this handler may not duplicate underlying +// datastructures, and may cause logical errors. Please use the Clone() +// method to create deep coppies, or use the WithTransaction, WithAttrs, +// or WithGroup methods to create new handlers with additional data. type NRHandler struct { *attributeCache *configCache *linkingCache + // underlying object pointers handler slog.Handler app *newrelic.Application txn *newrelic.Transaction - - // group logic - goas []groupOrAttrs -} - -type groupOrAttrs struct { - group string - attrs []slog.Attr } +// newHandler is an internal helper function to create a new NRHandler func newHandler(app *newrelic.Application, handler slog.Handler) *NRHandler { return &NRHandler{ handler: handler, @@ -43,9 +43,9 @@ func newHandler(app *newrelic.Application, handler slog.Handler) *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. // -// Note: This function will silently fail, and always return a valid handler -// to avoid service disruptions. If you would prefer to handle when wrapping -// fails, use Wrap() instead. +// Note: This function will silently error, and always return a valid handler +// to avoid service disruptions. If you would prefer to handle errors when +// wrapping your handler, use the Wrap() function instead. func WrapHandler(app *newrelic.Application, handler slog.Handler) slog.Handler { if app == nil { return handler @@ -88,10 +88,12 @@ func New(app *newrelic.Application, handler slog.Handler) *slog.Logger { return slog.New(WrapHandler(app, handler)) } -// clone duplicates the handler, creating a new instance with the same configuration. -// This is a deep copy. -func (h *NRHandler) clone() *NRHandler { - +// Clone creates a deep copy of the original handler, including a copy of all cached data +// and the underlying handler. +// +// Note: application, transaction, and handler pointers will be coppied, but the underlying +// data will not be duplicated. +func (h *NRHandler) Clone() *NRHandler { return &NRHandler{ handler: h.handler, attributeCache: h.attributeCache.clone(), @@ -99,14 +101,13 @@ func (h *NRHandler) clone() *NRHandler { linkingCache: h.linkingCache.clone(), app: h.app, txn: h.txn, - goas: slices.Clone(h.goas), } } // WithTransaction returns a new handler that is configured to capture log data // and attribute it to a specific transaction. func (h *NRHandler) WithTransaction(txn *newrelic.Transaction) *NRHandler { - h2 := h.clone() + h2 := h.Clone() h2.txn = txn return h2 } @@ -147,7 +148,7 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { } if h.shouldForwardLogs(h.app) { - attrs := h.getPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map + attrs := h.copyPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map coppies prefix := h.getPrefix() record.Attrs(func(attr slog.Attr) bool { @@ -190,10 +191,10 @@ func (h *NRHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } - newHandler := h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) - newHandler.handler = newHandler.handler.WithAttrs(attrs) - newHandler.computePrecompiledAttributes(newHandler.goas) - return newHandler + h2 := h.Clone() + h2.handler = h.handler.WithAttrs(attrs) + h2.precompileAttributes(attrs) + return h2 } // WithGroup returns a new Handler with the given group appended to @@ -206,18 +207,10 @@ func (h *NRHandler) WithGroup(name string) slog.Handler { return h } - newHandler := h.withGroupOrAttrs(groupOrAttrs{group: name}) - newHandler.handler = newHandler.handler.WithGroup(name) - newHandler.computePrecompiledAttributes(newHandler.goas) - return newHandler -} - -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 + h2 := h.Clone() + h2.handler = h.handler.WithGroup(name) + h2.precompileGroup(name) + return h2 } // WithTransactionFromContext creates a wrapped NRHandler, enabling it to automatically reference New Relic @@ -228,10 +221,7 @@ func WithTransactionFromContext(handler slog.Handler) slog.Handler { return handler } -const ( - nrlinking = "NR-LINKING" - key = "newrelic" -) +const newrelicAttributeKey = "newrelic" func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) { str := nrLinkingString(h.getAgentLinkingMetadata(app)) @@ -239,7 +229,7 @@ func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) return } - record.AddAttrs(slog.String(key, str)) + record.AddAttrs(slog.String(newrelicAttributeKey, str)) } func (h *NRHandler) enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { @@ -248,31 +238,5 @@ func (h *NRHandler) enrichRecordTxn(txn *newrelic.Transaction, record *slog.Reco return } - record.AddAttrs(slog.String(key, str)) -} - -// nrLinkingString returns a string that represents the linking metadata -func nrLinkingString(data newrelic.LinkingMetadata) string { - if data.EntityGUID == "" { - return "" - } - - len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) - str := strings.Builder{} - str.Grow(len) // only 1 alloc - - str.WriteString(nrlinking) - str.WriteByte('|') - str.WriteString(data.EntityGUID) - str.WriteByte('|') - str.WriteString(data.Hostname) - str.WriteByte('|') - str.WriteString(data.TraceID) - str.WriteByte('|') - str.WriteString(data.SpanID) - str.WriteByte('|') - str.WriteString(data.EntityName) - str.WriteByte('|') - - return str.String() + record.AddAttrs(slog.String(newrelicAttributeKey, str)) } diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 8c8d3d089..8f21b6fce 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -292,24 +292,75 @@ func TestWithAttributes(t *testing.T) { log.Info(message) - log1 := string(out.String()) - txn := app.StartTransaction("hi") txnLog := WithTransaction(txn, log) txnLog.Info(message) + data := txn.GetLinkingMetadata() txn.End() - log2 := string(out.String()) + additionalAttrs := slog.String("additional", "attr") - attrString := `"string key"=val "int key"=1` - if !strings.Contains(log1, attrString) { - t.Errorf("expected %s to contain %s", log1, attrString) - } + log = log.WithGroup("group1") + log.Info(message, additionalAttrs) - if !strings.Contains(log2, attrString) { - t.Errorf("expected %s to contain %s", log2, attrString) - } + log = log.WithGroup("group2") + log.Info(message, additionalAttrs) + log = log.With(additionalAttrs) + log.Info(message) + + app.ExpectLogEvents(t, []internal.WantLog{ + { + Attributes: map[string]interface{}{ + "string key": "val", + "int key": 1, + }, + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + }, + { + Attributes: map[string]interface{}{ + "string key": "val", + "int key": 1, + }, + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + SpanID: data.SpanID, + TraceID: data.TraceID, + }, + { + Attributes: map[string]interface{}{ + "string key": "val", + "int key": 1, + "group1.additional": "attr", + }, + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + }, + { + Attributes: map[string]interface{}{ + "string key": "val", + "int key": 1, + "group1.group2.additional": "attr", + }, + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + }, + { + Attributes: map[string]interface{}{ + "string key": "val", + "int key": 1, + "group1.group2.additional": "attr", + }, + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + }, + }) } func TestWithAttributesFromContext(t *testing.T) { diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go index 574b2f21a..e93064789 100644 --- a/v3/integrations/logcontext-v2/nrslog/linking.go +++ b/v3/integrations/logcontext-v2/nrslog/linking.go @@ -1,6 +1,10 @@ package nrslog -import "github.com/newrelic/go-agent/v3/newrelic" +import ( + "strings" + + "github.com/newrelic/go-agent/v3/newrelic" +) type linkingCache struct { loaded bool @@ -78,3 +82,31 @@ func (cache *linkingCache) getTransactionLinkingMetadata(txn *newrelic.Transacti SpanID: traceData.SpanID, } } + +const nrlinking = "NR-LINKING" + +// nrLinkingString returns a string that represents the linking metadata +func nrLinkingString(data newrelic.LinkingMetadata) string { + if data.EntityGUID == "" { + return "" + } + + len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName) + str := strings.Builder{} + str.Grow(len) // only 1 alloc + + str.WriteString(nrlinking) + str.WriteByte('|') + str.WriteString(data.EntityGUID) + str.WriteByte('|') + str.WriteString(data.Hostname) + str.WriteByte('|') + str.WriteString(data.TraceID) + str.WriteByte('|') + str.WriteString(data.SpanID) + str.WriteByte('|') + str.WriteString(data.EntityName) + str.WriteByte('|') + + return str.String() +} From d55d218f66e0d4e8ae5770d4ceef7662581db9f0 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 24 Feb 2025 13:07:10 -0500 Subject: [PATCH 11/14] prevent all transaction methods from nil transaction errors (#1001) * prevent all transaction methods from executing against nil transaction object * fix: incorrect nil logic for segments --- v3/go.mod | 1 - v3/newrelic/transaction.go | 67 ++++++++++++++++++------------ v3/newrelic/transaction_test.go | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 v3/newrelic/transaction_test.go diff --git a/v3/go.mod b/v3/go.mod index 3a673a907..9b3badbff 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -7,7 +7,6 @@ require ( google.golang.org/protobuf v1.34.2 ) - retract v3.22.0 // release process error corrected in v3.22.1 retract v3.25.0 // release process error corrected in v3.25.1 diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 16a384faa..ccdf274ac 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -24,11 +24,16 @@ type Transaction struct { thread *thread } +// nilTransaction guards against nil errors when handling a transaction. +func nilTransaction(txn *Transaction) bool { + return txn == nil || txn.thread == nil || txn.thread.txn == nil +} + // End finishes the Transaction. After that, subsequent calls to End or // other Transaction methods have no effect. All segments and // instrumentation must be completed before End is called. func (txn *Transaction) End() { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } @@ -55,7 +60,7 @@ func (txn *Transaction) End() { // The set of options should be the complete set you wish to have in effect, // just as if you were calling StartTransaction now with the same set of options. func (txn *Transaction) SetOption(options ...TraceOption) { - if txn == nil || txn.thread == nil || txn.thread.txn == nil { + if nilTransaction(txn) { return } txn.thread.txn.setOption(options...) @@ -63,7 +68,7 @@ func (txn *Transaction) SetOption(options ...TraceOption) { // Ignore prevents this transaction's data from being recorded. func (txn *Transaction) Ignore() { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.Ignore(), "ignore transaction", nil) @@ -72,7 +77,7 @@ func (txn *Transaction) Ignore() { // SetName names the transaction. Use a limited set of unique names to // ensure that Transactions are grouped usefully. func (txn *Transaction) SetName(name string) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.SetName(name), "set transaction name", nil) @@ -84,8 +89,7 @@ func (txn *Transaction) Name() string { // This is called Name rather than GetName to be consistent with the prevailing naming // conventions for the Go language, even though the underlying internal call must be called // something else (like GetName) because there's already a Name struct member. - - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return "" } return txn.thread.GetName() @@ -117,7 +121,7 @@ func (txn *Transaction) Name() string { // way to directly control the recorded error's message, class, stacktrace, // and attributes. func (txn *Transaction) NoticeError(err error) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.NoticeError(err, false), "notice error", nil) @@ -151,7 +155,7 @@ func (txn *Transaction) NoticeError(err error) { // way to directly control the recorded error's message, class, stacktrace, // and attributes. func (txn *Transaction) NoticeExpectedError(err error) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.NoticeError(err, true), "notice error", nil) @@ -166,7 +170,7 @@ func (txn *Transaction) NoticeExpectedError(err error) { // For more information, see: // https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/collect-custom-attributes func (txn *Transaction) AddAttribute(key string, value any) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.AddAttribute(key, value), "add attribute", nil) @@ -176,10 +180,9 @@ func (txn *Transaction) AddAttribute(key string, value any) { // belong to or interact with. This will propogate an attribute containing this information to all events that are // a child of this transaction, like errors and spans. func (txn *Transaction) SetUserID(userID string) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } - txn.thread.logAPIError(txn.thread.AddUserID(userID), "set user ID", nil) } @@ -192,6 +195,9 @@ func (txn *Transaction) SetUserID(userID string) { // as well as log metrics depending on how your application is // configured. func (txn *Transaction) RecordLog(log LogData) { + if nilTransaction(txn) { + return + } event, err := log.toLogEvent() if err != nil { txn.Application().app.Error("unable to record log", map[string]any{ @@ -212,6 +218,9 @@ func (txn *Transaction) RecordLog(log LogData) { // present, the agent will look for distributed tracing headers using // Transaction.AcceptDistributedTraceHeaders. func (txn *Transaction) SetWebRequestHTTP(r *http.Request) { + if nilTransaction(txn) { + return + } if r == nil { txn.SetWebRequest(WebRequest{}) return @@ -265,7 +274,7 @@ func reqBody(req *http.Request) *BodyBuffer { // distributed tracing headers using Transaction.AcceptDistributedTraceHeaders. // Use Transaction.SetWebRequestHTTP if you have a *http.Request. func (txn *Transaction) SetWebRequest(r WebRequest) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } if IsSecurityAgentPresent() { @@ -289,7 +298,7 @@ func (txn *Transaction) SetWebRequest(r WebRequest) { // package middlewares. Therefore, you probably want to use this only if you // are writing your own instrumentation middleware. func (txn *Transaction) SetWebResponse(w http.ResponseWriter) http.ResponseWriter { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return w } return txn.thread.SetWebResponse(w) @@ -304,7 +313,7 @@ func (txn *Transaction) StartSegmentNow() SegmentStartTime { } func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return SegmentStartTime{} } return txn.thread.startSegmentAt(at) @@ -324,7 +333,11 @@ func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { // // ... code you want to time here ... // segment.End() func (txn *Transaction) StartSegment(name string) *Segment { - if IsSecurityAgentPresent() && txn != nil && txn.thread != nil && txn.thread.thread != nil && txn.thread.thread.threadID > 0 { + if nilTransaction(txn) { + return &Segment{} // return a non-nil Segment to avoid nil dereference + } + + if IsSecurityAgentPresent() && txn.thread.thread != nil && txn.thread.thread.threadID > 0 { // async segment start secureAgent.SendEvent("NEW_GOROUTINE_LINKER", txn.thread.getCsecData()) } @@ -346,7 +359,7 @@ func (txn *Transaction) StartSegment(name string) *Segment { // StartExternalSegment calls InsertDistributedTraceHeaders, so you don't need // to use it for outbound HTTP calls: Just use StartExternalSegment! func (txn *Transaction) InsertDistributedTraceHeaders(hdrs http.Header) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.CreateDistributedTracePayload(hdrs) @@ -367,7 +380,7 @@ func (txn *Transaction) InsertDistributedTraceHeaders(hdrs http.Header) { // context headers. Only when those are not found will it look for the New // Relic distributed tracing header. func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http.Header) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.logAPIError(txn.thread.AcceptDistributedTraceHeaders(t, hdrs), "accept trace payload", nil) @@ -379,6 +392,10 @@ func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http // convert the JSON string to http headers. There is no guarantee that the header data found in JSON // is correct beyond conforming to the expected types and syntax. func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, jsondata string) error { + if nilTransaction(txn) { // do no work if txn is nil + return nil + } + hdrs, err := DistributedTraceHeadersFromJSON(jsondata) if err != nil { return err @@ -465,7 +482,7 @@ func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err err // Application returns the Application which started the transaction. func (txn *Transaction) Application() *Application { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return nil } return txn.thread.Application() @@ -484,7 +501,7 @@ func (txn *Transaction) Application() *Application { // monitoring is disabled, the application is not connected, or an error // occurred. It is safe to call the pointer's methods if it is nil. func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return nil } b, err := txn.thread.BrowserTimingHeader() @@ -506,7 +523,7 @@ func (txn *Transaction) BrowserTimingHeader() *BrowserTimingHeader { // Note that any segments that end after the transaction ends will not // be reported. func (txn *Transaction) NewGoroutine() *Transaction { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return nil } newTxn := txn.thread.NewGoroutine() @@ -519,7 +536,7 @@ func (txn *Transaction) NewGoroutine() *Transaction { // GetTraceMetadata returns distributed tracing identifiers. Empty // string identifiers are returned if the transaction has finished. func (txn *Transaction) GetTraceMetadata() TraceMetadata { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return TraceMetadata{} } return txn.thread.GetTraceMetadata() @@ -528,7 +545,7 @@ func (txn *Transaction) GetTraceMetadata() TraceMetadata { // GetLinkingMetadata returns the fields needed to link data to a trace or // entity. func (txn *Transaction) GetLinkingMetadata() LinkingMetadata { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return LinkingMetadata{} } return txn.thread.GetLinkingMetadata() @@ -539,21 +556,21 @@ func (txn *Transaction) GetLinkingMetadata() LinkingMetadata { // must be enabled for transactions to be sampled. False is returned if // the Transaction has finished. func (txn *Transaction) IsSampled() bool { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return false } return txn.thread.IsSampled() } func (txn *Transaction) GetCsecAttributes() map[string]any { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return nil } return txn.thread.getCsecAttributes() } func (txn *Transaction) SetCsecAttributes(key string, value any) { - if txn == nil || txn.thread == nil { + if nilTransaction(txn) { return } txn.thread.setCsecAttributes(key, value) diff --git a/v3/newrelic/transaction_test.go b/v3/newrelic/transaction_test.go new file mode 100644 index 000000000..e5dd70f27 --- /dev/null +++ b/v3/newrelic/transaction_test.go @@ -0,0 +1,72 @@ +package newrelic + +import ( + "fmt" + "net/http" + "testing" +) + +func TestTransaction_MethodsWithNilTransaction(t *testing.T) { + var nilTxn *Transaction + + defer func() { + if r := recover(); r != nil { + t.Errorf("panics should not occur on methods of Transaction: %v", r) + } + }() + + // Ensure no panic occurs when calling methods on a nil transaction + nilTxn.End() + nilTxn.SetOption() + nilTxn.Ignore() + nilTxn.SetName("test") + name := nilTxn.Name() + if name != "" { + t.Errorf("expected empty string, got %s", name) + } + nilTxn.NoticeError(fmt.Errorf("test error")) + nilTxn.NoticeExpectedError(fmt.Errorf("test expected error")) + nilTxn.AddAttribute("key", "value") + nilTxn.SetUserID("user123") + nilTxn.RecordLog(LogData{}) + nilTxn.SetWebRequestHTTP(nil) + nilTxn.SetWebRequest(WebRequest{}) + nilTxn.SetWebResponse(nil) + nilTxn.StartSegmentNow() + nilTxn.StartSegment("test segment") + nilTxn.InsertDistributedTraceHeaders(http.Header{}) + nilTxn.AcceptDistributedTraceHeaders(TransportHTTP, http.Header{}) + err := nilTxn.AcceptDistributedTraceHeadersFromJSON(TransportHTTP, "{}") + if err != nil { + t.Errorf("expected no error, got %v", err) + } + app := nilTxn.Application() + if app != nil { + t.Errorf("expected nil, got %v", app) + } + bth := nilTxn.BrowserTimingHeader() + if bth != nil { + t.Errorf("expected nil, got %v", bth) + } + newTxn := nilTxn.NewGoroutine() + if newTxn != nil { + t.Errorf("expected nil, got %v", newTxn) + } + traceMetadata := nilTxn.GetTraceMetadata() + if traceMetadata != (TraceMetadata{}) { + t.Errorf("expected empty TraceMetadata, got %v", traceMetadata) + } + linkingMetadata := nilTxn.GetLinkingMetadata() + if linkingMetadata != (LinkingMetadata{}) { + t.Errorf("expected empty LinkingMetadata, got %v", linkingMetadata) + } + isSampled := nilTxn.IsSampled() + if isSampled { + t.Errorf("expected false, got %v", isSampled) + } + csecAttributes := nilTxn.GetCsecAttributes() + if csecAttributes != nil { + t.Errorf("expected nil, got %v", csecAttributes) + } + nilTxn.SetCsecAttributes("key", "value") +} From a5ec16dea886ccbe1cc1904ce9887a9272b852ff Mon Sep 17 00:00:00 2001 From: Steve Willoughby <76975199+nr-swilloughby@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:12:59 -0800 Subject: [PATCH 12/14] Custom log tagging (labels) on forwarded application log events (#1002) * checkpoint * forwarded log tagging * complete --------- Co-authored-by: Mirac Kara <55501260+mirackara@users.noreply.github.com> --- v3/newrelic/app_run.go | 5 +- v3/newrelic/config.go | 8 ++ v3/newrelic/config_options.go | 159 ++++++++++++++++++++++++---------- v3/newrelic/config_test.go | 14 ++- v3/newrelic/harvest.go | 2 + v3/newrelic/log_events.go | 14 +++ v3/newrelic/metric_names.go | 12 +-- 7 files changed, 158 insertions(+), 56 deletions(-) diff --git a/v3/newrelic/app_run.go b/v3/newrelic/app_run.go index 4136fb4ac..e69daf1f4 100644 --- a/v3/newrelic/app_run.go +++ b/v3/newrelic/app_run.go @@ -240,7 +240,10 @@ func (run *appRun) LoggingConfig() (config loggingConfig) { config.maxLogEvents = run.MaxLogEvents() config.collectMetrics = logging.Enabled && logging.Metrics.Enabled config.localEnrichment = logging.Enabled && logging.LocalDecorating.Enabled - + if run.Config.Labels != nil && logging.Forwarding.Enabled && logging.Forwarding.Labels.Enabled { + config.includeLabels = run.Config.Labels + config.excludeLabels = &logging.Forwarding.Labels.Exclude + } return config } diff --git a/v3/newrelic/config.go b/v3/newrelic/config.go index b34537d68..3c0d441b6 100644 --- a/v3/newrelic/config.go +++ b/v3/newrelic/config.go @@ -572,6 +572,12 @@ type ApplicationLogging struct { // Controls the overall memory consumption when using log forwarding. // SHOULD be sent as part of the harvest_limits on Connect. MaxSamplesStored int + Labels struct { + // Toggles whether we send our labels with forwarded logs. + Enabled bool + // List of label types to exclude from forwarded logs. + Exclude []string + } } Metrics struct { // Toggles whether the agent gathers the the user facing Logging/lines and Logging/lines/{SEVERITY} @@ -661,6 +667,8 @@ func defaultConfig() Config { c.ApplicationLogging.Enabled = true c.ApplicationLogging.Forwarding.Enabled = true c.ApplicationLogging.Forwarding.MaxSamplesStored = internal.MaxLogEvents + c.ApplicationLogging.Forwarding.Labels.Enabled = false + c.ApplicationLogging.Forwarding.Labels.Exclude = nil c.ApplicationLogging.Metrics.Enabled = true c.ApplicationLogging.LocalDecorating.Enabled = false c.ApplicationLogging.ZapLogger.AttributesFrontloaded = true diff --git a/v3/newrelic/config_options.go b/v3/newrelic/config_options.go index 8c3daf6f7..608b07d3e 100644 --- a/v3/newrelic/config_options.go +++ b/v3/newrelic/config_options.go @@ -6,6 +6,7 @@ package newrelic import ( "fmt" "io" + "maps" "os" "strconv" "strings" @@ -229,6 +230,28 @@ func ConfigAppLogForwardingEnabled(enabled bool) ConfigOption { } } +// ConfigAppLogForwardingLabelsEnabled enables or disables sending our application +// labels (which are configured via ConfigLabels) with forwarded log events. +// Defaults: enabled=false +func ConfigAppLogForwardingLabelsEnabled(enabled bool) ConfigOption { + return func(cfg *Config) { + cfg.ApplicationLogging.Forwarding.Labels.Enabled = enabled + } +} + +// ConfigAppLogForwardingLabelsExclude specifies a list of specificd label types (i.e. keys) +// which should NOT be sent along with forwarded log events. +func ConfigAppLogForwardingLabelsExclude(labelType ...string) ConfigOption { + return func(cfg *Config) { + for _, t := range labelType { + t = strings.TrimSpace(t) + if t != "" && !strings.ContainsAny(t, ";:") { + cfg.ApplicationLogging.Forwarding.Labels.Exclude = append(cfg.ApplicationLogging.Forwarding.Labels.Exclude, t) + } + } + } +} + // ConfigAppLogDecoratingEnabled enables or disables the local decoration // of logs when using one of our logs in context plugins // Defaults: enabled=false @@ -357,44 +380,55 @@ func ConfigDebugLogger(w io.Writer) ConfigOption { return ConfigLogger(NewDebugLogger(w)) } +// ConfigLabels configures a set of labels for the application to report as attributes. +// This may also be set using the NEW_RELIC_LABELS environment variable. +func ConfigLabels(labels map[string]string) ConfigOption { + return func(cfg *Config) { + cfg.Labels = make(map[string]string) + maps.Copy(cfg.Labels, labels) + } +} + // ConfigFromEnvironment populates the config based on environment variables: // -// NEW_RELIC_APP_NAME sets AppName -// NEW_RELIC_ATTRIBUTES_EXCLUDE sets Attributes.Exclude using a comma-separated list, eg. "request.headers.host,request.method" -// NEW_RELIC_ATTRIBUTES_INCLUDE sets Attributes.Include using a comma-separated list -// NEW_RELIC_MODULE_DEPENDENCY_METRICS_ENABLED sets ModuleDependencyMetrics.Enabled -// NEW_RELIC_MODULE_DEPENDENCY_METRICS_IGNORED_PREFIXES sets ModuleDependencyMetrics.IgnoredPrefixes -// NEW_RELIC_MODULE_DEPENDENCY_METRICS_REDACT_IGNORED_PREFIXES sets ModuleDependencyMetrics.RedactIgnoredPrefixes to a boolean value -// NEW_RELIC_CODE_LEVEL_METRICS_ENABLED sets CodeLevelMetrics.Enabled -// NEW_RELIC_CODE_LEVEL_METRICS_SCOPE sets CodeLevelMetrics.Scope using a comma-separated list, e.g. "transaction" -// NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX sets CodeLevelMetrics.PathPrefixes using a comma-separated list -// NEW_RELIC_CODE_LEVEL_METRICS_REDACT_PATH_PREFIXES sets CodeLevelMetrics.RedactPathPrefixes to a boolean value -// NEW_RELIC_CODE_LEVEL_METRICS_REDACT_IGNORED_PREFIXES sets CodeLevelMetrics.RedactIgnoredPrefixes to a boolean value -// NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX sets CodeLevelMetrics.IgnoredPrefixes using a comma-separated list -// NEW_RELIC_DISTRIBUTED_TRACING_ENABLED sets DistributedTracer.Enabled using strconv.ParseBool -// NEW_RELIC_ENABLED sets Enabled using strconv.ParseBool -// NEW_RELIC_HIGH_SECURITY sets HighSecurity using strconv.ParseBool -// NEW_RELIC_HOST sets Host -// NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE sets InfiniteTracing.SpanEvents.QueueSize using strconv.Atoi -// NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT sets InfiniteTracing.TraceObserver.Port using strconv.Atoi -// NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST sets InfiniteTracing.TraceObserver.Host -// NEW_RELIC_LABELS sets Labels using a semi-colon delimited string of colon-separated pairs, eg. "Server:One;DataCenter:Primary" -// NEW_RELIC_LICENSE_KEY sets License -// NEW_RELIC_LOG sets Logger to log to either "stdout" or "stderr" (filenames are not supported) -// NEW_RELIC_LOG_LEVEL controls the NEW_RELIC_LOG level, must be "debug" for debug, or empty for info -// NEW_RELIC_PROCESS_HOST_DISPLAY_NAME sets HostDisplayName -// NEW_RELIC_SECURITY_POLICIES_TOKEN sets SecurityPoliciesToken -// NEW_RELIC_UTILIZATION_BILLING_HOSTNAME sets Utilization.BillingHostname -// NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS sets Utilization.LogicalProcessors using strconv.Atoi -// NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB sets Utilization.TotalRAMMIB using strconv.Atoi -// NEW_RELIC_APPLICATION_LOGGING_ENABLED sets ApplicationLogging.Enabled. Set to false to disable all application logging features. -// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED sets ApplicationLogging.LogForwarding.Enabled. Set to false to disable in agent log forwarding. -// NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED sets ApplicationLogging.Metrics.Enabled. Set to false to disable the collection of application log metrics. -// NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED sets ApplicationLogging.LocalDecoration.Enabled. Set to true to enable local log decoration. -// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED sets ApplicationLogging.LogForwarding.Limit. Set to 0 to prevent captured logs from being forwarded. -// NEW_RELIC_AI_MONITORING_ENABLED sets AIMonitoring.Enabled -// NEW_RELIC_AI_MONITORING_STREAMING_ENABLED sets AIMonitoring.Streaming.Enabled -// NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED sets AIMonitoring.RecordContent.Enabled +// NEW_RELIC_APP_NAME sets AppName +// NEW_RELIC_ATTRIBUTES_EXCLUDE sets Attributes.Exclude using a comma-separated list, eg. "request.headers.host,request.method" +// NEW_RELIC_ATTRIBUTES_INCLUDE sets Attributes.Include using a comma-separated list +// NEW_RELIC_MODULE_DEPENDENCY_METRICS_ENABLED sets ModuleDependencyMetrics.Enabled +// NEW_RELIC_MODULE_DEPENDENCY_METRICS_IGNORED_PREFIXES sets ModuleDependencyMetrics.IgnoredPrefixes +// NEW_RELIC_MODULE_DEPENDENCY_METRICS_REDACT_IGNORED_PREFIXES sets ModuleDependencyMetrics.RedactIgnoredPrefixes to a boolean value +// NEW_RELIC_CODE_LEVEL_METRICS_ENABLED sets CodeLevelMetrics.Enabled +// NEW_RELIC_CODE_LEVEL_METRICS_SCOPE sets CodeLevelMetrics.Scope using a comma-separated list, e.g. "transaction" +// NEW_RELIC_CODE_LEVEL_METRICS_PATH_PREFIX sets CodeLevelMetrics.PathPrefixes using a comma-separated list +// NEW_RELIC_CODE_LEVEL_METRICS_REDACT_PATH_PREFIXES sets CodeLevelMetrics.RedactPathPrefixes to a boolean value +// NEW_RELIC_CODE_LEVEL_METRICS_REDACT_IGNORED_PREFIXES sets CodeLevelMetrics.RedactIgnoredPrefixes to a boolean value +// NEW_RELIC_CODE_LEVEL_METRICS_IGNORED_PREFIX sets CodeLevelMetrics.IgnoredPrefixes using a comma-separated list +// NEW_RELIC_DISTRIBUTED_TRACING_ENABLED sets DistributedTracer.Enabled using strconv.ParseBool +// NEW_RELIC_ENABLED sets Enabled using strconv.ParseBool +// NEW_RELIC_HIGH_SECURITY sets HighSecurity using strconv.ParseBool +// NEW_RELIC_HOST sets Host +// NEW_RELIC_INFINITE_TRACING_SPAN_EVENTS_QUEUE_SIZE sets InfiniteTracing.SpanEvents.QueueSize using strconv.Atoi +// NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_PORT sets InfiniteTracing.TraceObserver.Port using strconv.Atoi +// NEW_RELIC_INFINITE_TRACING_TRACE_OBSERVER_HOST sets InfiniteTracing.TraceObserver.Host +// NEW_RELIC_LABELS sets Labels using a semi-colon delimited string of colon-separated pairs, eg. "Server:One;DataCenter:Primary" +// NEW_RELIC_LICENSE_KEY sets License +// NEW_RELIC_LOG sets Logger to log to either "stdout" or "stderr" (filenames are not supported) +// NEW_RELIC_LOG_LEVEL controls the NEW_RELIC_LOG level, must be "debug" for debug, or empty for info +// NEW_RELIC_PROCESS_HOST_DISPLAY_NAME sets HostDisplayName +// NEW_RELIC_SECURITY_POLICIES_TOKEN sets SecurityPoliciesToken +// NEW_RELIC_UTILIZATION_BILLING_HOSTNAME sets Utilization.BillingHostname +// NEW_RELIC_UTILIZATION_LOGICAL_PROCESSORS sets Utilization.LogicalProcessors using strconv.Atoi +// NEW_RELIC_UTILIZATION_TOTAL_RAM_MIB sets Utilization.TotalRAMMIB using strconv.Atoi +// NEW_RELIC_APPLICATION_LOGGING_ENABLED sets ApplicationLogging.Enabled. Set to false to disable all application logging features. +// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED sets ApplicationLogging.LogForwarding.Enabled. Set to false to disable in agent log forwarding. +// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED sets ApplicationLogging.LogForwarding.Labels.Enabled to enable sending application labels with forwarded logs. +// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE sets ApplicationLogging.LogForwarding.Labels.Exclude to filter out a set of unwanted label types from the ones reported with logs. +// NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED sets ApplicationLogging.Metrics.Enabled. Set to false to disable the collection of application log metrics. +// NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED sets ApplicationLogging.LocalDecoration.Enabled. Set to true to enable local log decoration. +// NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED sets ApplicationLogging.LogForwarding.Limit. Set to 0 to prevent captured logs from being forwarded. +// NEW_RELIC_AI_MONITORING_ENABLED sets AIMonitoring.Enabled +// NEW_RELIC_AI_MONITORING_STREAMING_ENABLED sets AIMonitoring.Streaming.Enabled +// NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED sets AIMonitoring.RecordContent.Enabled // // This function is strict and will assign Config.Error if any of the // environment variables cannot be parsed. @@ -431,6 +465,13 @@ func configFromEnvironment(getenv func(string) string) ConfigOption { *field = env } } + assignStringSlice := func(field *[]string, name string, delim string) { + if env := getenv(name); env != "" { + for _, part := range strings.Split(env, delim) { + *field = append(*field, strings.TrimSpace(part)) + } + } + } assignString(&cfg.AppName, "NEW_RELIC_APP_NAME") assignString(&cfg.License, "NEW_RELIC_LICENSE_KEY") @@ -455,6 +496,8 @@ func configFromEnvironment(getenv func(string) string) ConfigOption { // Application Logging Env Variables assignBool(&cfg.ApplicationLogging.Enabled, "NEW_RELIC_APPLICATION_LOGGING_ENABLED") assignBool(&cfg.ApplicationLogging.Forwarding.Enabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED") + assignBool(&cfg.ApplicationLogging.Forwarding.Labels.Enabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED") + assignStringSlice(&cfg.ApplicationLogging.Forwarding.Labels.Exclude, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE", ",") assignInt(&cfg.ApplicationLogging.Forwarding.MaxSamplesStored, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED") assignBool(&cfg.ApplicationLogging.Metrics.Enabled, "NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED") assignBool(&cfg.ApplicationLogging.LocalDecorating.Enabled, "NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED") @@ -463,10 +506,12 @@ func configFromEnvironment(getenv func(string) string) ConfigOption { assignBool(&cfg.AIMonitoring.RecordContent.Enabled, "NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED") if env := getenv("NEW_RELIC_LABELS"); env != "" { - if labels := getLabels(getenv("NEW_RELIC_LABELS")); len(labels) > 0 { + labels, err := getLabels(getenv("NEW_RELIC_LABELS")) + if err != nil { + cfg.Error = fmt.Errorf("invalid NEW_RELIC_LABELS value: %s: %v", env, err) + cfg.Labels = nil + } else if len(labels) > 0 { cfg.Labels = labels - } else { - cfg.Error = fmt.Errorf("invalid NEW_RELIC_LABELS value: %s", env) } } @@ -539,21 +584,37 @@ func isDebugEnv(env string) bool { // delimited string of colon-separated pairs (for example, "Server:One;Data // Center:Primary"). Label keys and values must be 255 characters or less in // length. No more than 64 Labels can be set. -func getLabels(env string) map[string]string { +// +// This has been updated as of 3.37.0 (February 2025) to conform to newer agent +// specifications by being more rigorous about what we expect and more explicitly +// rejecting invalid label lists. +// +// We disallow (and reject the entire list of labels if any are found): +// +// empty key +// empty value +// too many delimiters in a row +// not enough delimiters +// +// However, we silently ignore: +// +// leading and trailing extra semicolons +// whitespace around delimiters +func getLabels(env string) (map[string]string, error) { out := make(map[string]string) env = strings.Trim(env, ";\t\n\v\f\r ") for _, entry := range strings.Split(env, ";") { if entry == "" { - return nil + return nil, fmt.Errorf("labels list contains empty entry") } split := strings.Split(entry, ":") if len(split) != 2 { - return nil + return nil, fmt.Errorf("labels must each have \"type\":\"value\" format") } left := strings.TrimSpace(split[0]) right := strings.TrimSpace(split[1]) if left == "" || right == "" { - return nil + return nil, fmt.Errorf("labels list has missing type(s) and/or value(s)") } if utf8.RuneCountInString(left) > 255 { runes := []rune(left) @@ -563,10 +624,14 @@ func getLabels(env string) map[string]string { runes := []rune(right) right = string(runes[:255]) } - out[left] = right - if len(out) >= 64 { - return out + // Instead of bailing out if we exceed the maximum size, we'll + // just add to the output map if we still are under the allowed limit + // and continue processing the input string, because there's still the + // chance that we encounter an invalid string later on which would mean + // we're supposed to flag it as an error and reject the whole thing. + if len(out) < 64 { + out[left] = right } } - return out + return out, nil } diff --git a/v3/newrelic/config_test.go b/v3/newrelic/config_test.go index 6ce06a6e6..84d03c5bd 100644 --- a/v3/newrelic/config_test.go +++ b/v3/newrelic/config_test.go @@ -49,7 +49,7 @@ func runLabelsTestCase(t *testing.T, js json.RawMessage) { return } - actual := getLabels(tc.LabelString) + actual, _ := getLabels(tc.LabelString) if len(actual) != len(tc.Expected) { t.Errorf("%s: incorrect number of elements: actual=%d expect=%d", tc.Name, len(actual), len(tc.Expected)) return @@ -144,6 +144,10 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "Enabled": true, "Forwarding": { "Enabled": true, + "Labels": { + "Enabled": false, + "Exclude": null + }, "MaxSamplesStored": %d }, "LocalDecorating":{ @@ -304,7 +308,7 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { }`) var sp internal.SecurityPolicies err := json.Unmarshal(securityPoliciesInput, &sp) - if nil != err { + if err != nil { t.Fatal(err) } @@ -312,7 +316,7 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { "NEW_RELIC_METADATA_ZAP": "zip", } js, err := configConnectJSONInternal(cp, 123, &utilization.SampleData, sampleEnvironment, "0.2.2", sp.PointerIfPopulated(), metadata) - if nil != err { + if err != nil { t.Fatal(err) } out := standardizeNumbers(string(js)) @@ -352,6 +356,10 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { "Enabled": true, "Forwarding": { "Enabled": true, + "Labels": { + "Enabled": false, + "Exclude": null + }, "MaxSamplesStored": %d }, "LocalDecorating":{ diff --git a/v3/newrelic/harvest.go b/v3/newrelic/harvest.go index fb6f368d9..386a72c20 100644 --- a/v3/newrelic/harvest.go +++ b/v3/newrelic/harvest.go @@ -358,6 +358,8 @@ var ( true, false, internal.MaxLogEvents, + nil, + nil, }, } ) diff --git a/v3/newrelic/log_events.go b/v3/newrelic/log_events.go index df3861570..243a11ede 100644 --- a/v3/newrelic/log_events.go +++ b/v3/newrelic/log_events.go @@ -6,6 +6,8 @@ package newrelic import ( "bytes" "container/heap" + "slices" + "strings" "time" "github.com/newrelic/go-agent/v3/internal/jsonx" @@ -167,6 +169,18 @@ func (events *logEvents) CollectorJSON(agentRunID string) ([]byte, error) { buf.WriteByte(',') buf.WriteString(`"hostname":`) jsonx.AppendString(buf, events.hostname) + if events.config.includeLabels != nil { + for k, v := range events.config.includeLabels { + if events.config.excludeLabels == nil || !slices.ContainsFunc(*events.config.excludeLabels, func(s string) bool { + return strings.ToLower(s) == strings.ToLower(k) + }) { + buf.WriteByte(',') + jsonx.AppendString(buf, "tags."+k) + buf.WriteByte(':') + jsonx.AppendString(buf, v) + } + } + } buf.WriteByte('}') buf.WriteByte('}') buf.WriteByte(',') diff --git a/v3/newrelic/metric_names.go b/v3/newrelic/metric_names.go index cc0883416..7756ea1e4 100644 --- a/v3/newrelic/metric_names.go +++ b/v3/newrelic/metric_names.go @@ -91,11 +91,13 @@ func supportMetric(metrics *metricTable, b bool, metricName string) { // logging features for log data generation and supportability // metrics generation. type loggingConfig struct { - loggingEnabled bool // application logging features are enabled - collectEvents bool // collection of log event data is enabled - collectMetrics bool // collection of log metric data is enabled - localEnrichment bool // local log enrichment is enabled - maxLogEvents int // maximum number of log events allowed to be collected + loggingEnabled bool // application logging features are enabled + collectEvents bool // collection of log event data is enabled + collectMetrics bool // collection of log metric data is enabled + localEnrichment bool // local log enrichment is enabled + maxLogEvents int // maximum number of log events allowed to be collected + includeLabels map[string]string // READ ONLY: if not nil, add these labels to log common data too + excludeLabels *[]string // READ ONLY: if not nil, exclude these label keys from the included labels } // Logging metrics that are generated at connect response From f6ca6614b531e2e38729f9f5824776a4cbf2b102 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Mon, 24 Feb 2025 16:38:25 -0800 Subject: [PATCH 13/14] fixed nil transaction handling in StartSegment transaction method, preparation for 3.37 release --- CHANGELOG.md | 22 +++++++++++++++++++ README.md | 2 +- v3/go.mod | 3 ++- .../logcontext-v2/logWriter/go.mod | 4 ++-- v3/integrations/logcontext-v2/nrlogrus/go.mod | 4 ++-- v3/integrations/logcontext-v2/nrslog/go.mod | 5 +++-- v3/integrations/logcontext-v2/nrwriter/go.mod | 4 ++-- v3/integrations/logcontext-v2/nrzap/go.mod | 4 ++-- .../logcontext-v2/nrzerolog/go.mod | 4 ++-- .../logcontext-v2/zerologWriter/go.mod | 4 ++-- .../logcontext/nrlogrusplugin/go.mod | 4 ++-- v3/integrations/nramqp/go.mod | 4 ++-- v3/integrations/nrawsbedrock/go.mod | 4 ++-- v3/integrations/nrawssdk-v1/go.mod | 4 ++-- v3/integrations/nrawssdk-v2/go.mod | 6 ++--- v3/integrations/nrb3/go.mod | 4 ++-- v3/integrations/nrecho-v3/go.mod | 4 ++-- v3/integrations/nrecho-v4/go.mod | 4 ++-- v3/integrations/nrelasticsearch-v7/go.mod | 4 ++-- .../examples/client-fasthttp/go.mod | 4 ++-- .../examples/server-fasthttp/go.mod | 4 ++-- v3/integrations/nrfasthttp/go.mod | 4 ++-- v3/integrations/nrgin/go.mod | 4 ++-- v3/integrations/nrgorilla/go.mod | 4 ++-- v3/integrations/nrgraphgophers/go.mod | 4 ++-- v3/integrations/nrgraphqlgo/example/go.mod | 4 ++-- v3/integrations/nrgraphqlgo/go.mod | 4 ++-- v3/integrations/nrgrpc/go.mod | 4 ++-- v3/integrations/nrhttprouter/go.mod | 4 ++-- v3/integrations/nrlambda/go.mod | 4 ++-- v3/integrations/nrlogrus/go.mod | 4 ++-- v3/integrations/nrlogxi/go.mod | 4 ++-- v3/integrations/nrmicro/go.mod | 8 +++---- v3/integrations/nrmongo/go.mod | 4 ++-- v3/integrations/nrmssql/go.mod | 4 ++-- v3/integrations/nrmysql/go.mod | 4 ++-- v3/integrations/nrnats/go.mod | 4 ++-- v3/integrations/nrnats/test/go.mod | 4 ++-- v3/integrations/nropenai/go.mod | 4 ++-- v3/integrations/nrpgx/example/sqlx/go.mod | 4 ++-- v3/integrations/nrpgx/go.mod | 4 ++-- v3/integrations/nrpgx5/go.mod | 6 ++--- v3/integrations/nrpkgerrors/go.mod | 4 ++-- v3/integrations/nrpq/example/sqlx/go.mod | 4 ++-- v3/integrations/nrpq/go.mod | 4 ++-- v3/integrations/nrredis-v7/go.mod | 4 ++-- v3/integrations/nrredis-v8/go.mod | 4 ++-- v3/integrations/nrredis-v9/go.mod | 4 ++-- v3/integrations/nrsarama/go.mod | 6 ++--- v3/integrations/nrsecurityagent/go.mod | 4 ++-- v3/integrations/nrslog/go.mod | 4 ++-- v3/integrations/nrsnowflake/go.mod | 4 ++-- v3/integrations/nrsqlite3/go.mod | 4 ++-- v3/integrations/nrstan/examples/go.mod | 4 ++-- v3/integrations/nrstan/go.mod | 4 ++-- v3/integrations/nrstan/test/go.mod | 4 ++-- v3/integrations/nrzap/go.mod | 4 ++-- v3/integrations/nrzerolog/go.mod | 4 ++-- v3/newrelic/transaction.go | 6 +---- v3/newrelic/version.go | 2 +- 60 files changed, 142 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c04cb8e1d..fee2e790b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 3.37.0 +### Enhanced + - Implemented a new approach to integrating New Relic with SLOG that is more lightweight, out of the way, and collects richer data. These changes have been constructed to be completely backwards-compatible with v1 of nrslog. Changes include: + - Wrapping `slog.Handler` objects with errors to allow users to handle invalid use cases + - A complete rework of log enrichment so that New Relic linking metadata does not invalidate JSON, BSON, or YAML scanners. This new approach will instead inject the linking metadata as a key-value pair. + - Complete support for `With()`, `WithGroup()`, and attributes for automatic instrumentation. + - Performance operations. + - Robust testing (close to 90% coverage). + - **This updates logcontext-v2/nrslog to v1.4.0.** + - Now custom application tags (labels) may be added to all forwarded log events. + - Enabled if `ConfigAppLogForwardingLabelsEnabled(true)` or `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_ENABLED=TRUE` + - May exclude labels named in `ConfigAppLogForwardingLabelsExclude("label1","label2",...)` or `NEW_RELIC_APPLICATION_LOGGING_FORWARDING_LABELS_EXCLUDE="label1,label2,..."` + - Labels are defined via `ConfigLabels(...)` or `NEW_RELIC_LABELS` + - Added memory allocation limit detection/response mechanism to facilitate calling custom functions to perform application-specific resource management functionality, report custom metrics or events, or take other appropriate actions, in response to rising heap memory size. + +### Support statement +We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. +See the [Go agent EOL Policy](/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. + +### Fixed + - Added protection around transaction methods to gracefully return when the transaction object is `nil`. + ## 3.36.0 ### Enhanced - Internal improvements to securityagent integration to better support trace handling and other support for security analysis of applications under test, now v1.3.4; affects the following other integrations: diff --git a/README.md b/README.md index 8fe6b0921..499fb1442 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Go is a compiled language, and doesn’t use a virtual machine. This means that ### Compatibility and Requirements -For the latest version of the agent, Go 1.18+ is required. +For the latest version of the agent, Go 1.22+ is required. Linux, OS X, and Windows (Vista, Server 2008 and later) are supported. diff --git a/v3/go.mod b/v3/go.mod index 9b3badbff..588aede53 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -1,12 +1,13 @@ module github.com/newrelic/go-agent/v3 -go 1.21 +go 1.22 require ( google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) + retract v3.22.0 // release process error corrected in v3.22.1 retract v3.25.0 // release process error corrected in v3.25.1 diff --git a/v3/integrations/logcontext-v2/logWriter/go.mod b/v3/integrations/logcontext-v2/logWriter/go.mod index bdbcda240..61ae8c301 100644 --- a/v3/integrations/logcontext-v2/logWriter/go.mod +++ b/v3/integrations/logcontext-v2/logWriter/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/logWriter -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.0 ) diff --git a/v3/integrations/logcontext-v2/nrlogrus/go.mod b/v3/integrations/logcontext-v2/nrlogrus/go.mod index f10f76dc7..583f1e31d 100644 --- a/v3/integrations/logcontext-v2/nrlogrus/go.mod +++ b/v3/integrations/logcontext-v2/nrlogrus/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/sirupsen/logrus v1.8.1 ) diff --git a/v3/integrations/logcontext-v2/nrslog/go.mod b/v3/integrations/logcontext-v2/nrslog/go.mod index 816aec86b..4e2dfc63a 100644 --- a/v3/integrations/logcontext-v2/nrslog/go.mod +++ b/v3/integrations/logcontext-v2/nrslog/go.mod @@ -1,7 +1,8 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog -go 1.21 +go 1.22 + +require github.com/newrelic/go-agent/v3 v3.37.0 -require github.com/newrelic/go-agent/v3 v3.36.0 replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrwriter/go.mod b/v3/integrations/logcontext-v2/nrwriter/go.mod index 02365f42d..8d539a4f6 100644 --- a/v3/integrations/logcontext-v2/nrwriter/go.mod +++ b/v3/integrations/logcontext-v2/nrwriter/go.mod @@ -1,8 +1,8 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter -go 1.21 +go 1.22 -require github.com/newrelic/go-agent/v3 v3.36.0 +require github.com/newrelic/go-agent/v3 v3.37.0 replace github.com/newrelic/go-agent/v3 => ../../.. diff --git a/v3/integrations/logcontext-v2/nrzap/go.mod b/v3/integrations/logcontext-v2/nrzap/go.mod index 416fcbb0d..f446c9aa3 100644 --- a/v3/integrations/logcontext-v2/nrzap/go.mod +++ b/v3/integrations/logcontext-v2/nrzap/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzap -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 go.uber.org/zap v1.24.0 ) diff --git a/v3/integrations/logcontext-v2/nrzerolog/go.mod b/v3/integrations/logcontext-v2/nrzerolog/go.mod index 7a8a7c4cb..25792774c 100644 --- a/v3/integrations/logcontext-v2/nrzerolog/go.mod +++ b/v3/integrations/logcontext-v2/nrzerolog/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrzerolog -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/rs/zerolog v1.26.1 ) diff --git a/v3/integrations/logcontext-v2/zerologWriter/go.mod b/v3/integrations/logcontext-v2/zerologWriter/go.mod index 34c762fb2..662e4d796 100644 --- a/v3/integrations/logcontext-v2/zerologWriter/go.mod +++ b/v3/integrations/logcontext-v2/zerologWriter/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext-v2/zerologWriter -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrwriter v1.0.0 github.com/rs/zerolog v1.27.0 ) diff --git a/v3/integrations/logcontext/nrlogrusplugin/go.mod b/v3/integrations/logcontext/nrlogrusplugin/go.mod index d351d9259..af811dc6f 100644 --- a/v3/integrations/logcontext/nrlogrusplugin/go.mod +++ b/v3/integrations/logcontext/nrlogrusplugin/go.mod @@ -2,10 +2,10 @@ module github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin // As of Dec 2019, the logrus go.mod file uses 1.13: // https://github.com/sirupsen/logrus/blob/master/go.mod -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 // v1.4.0 is required for for the log.WithContext. github.com/sirupsen/logrus v1.4.0 ) diff --git a/v3/integrations/nramqp/go.mod b/v3/integrations/nramqp/go.mod index 37631d2dc..409a17049 100644 --- a/v3/integrations/nramqp/go.mod +++ b/v3/integrations/nramqp/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/nramqp -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/rabbitmq/amqp091-go v1.9.0 ) replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrawsbedrock/go.mod b/v3/integrations/nrawsbedrock/go.mod index 5ab131aeb..a947c61a7 100644 --- a/v3/integrations/nrawsbedrock/go.mod +++ b/v3/integrations/nrawsbedrock/go.mod @@ -1,6 +1,6 @@ module github.com/newrelic/go-agent/v3/integrations/nrawsbedrock -go 1.21 +go 1.22 require ( github.com/aws/aws-sdk-go-v2 v1.26.0 @@ -8,7 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/bedrock v1.7.3 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.1 github.com/google/uuid v1.6.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrawssdk-v1/go.mod b/v3/integrations/nrawssdk-v1/go.mod index 5591725f4..54f8660b9 100644 --- a/v3/integrations/nrawssdk-v1/go.mod +++ b/v3/integrations/nrawssdk-v1/go.mod @@ -3,12 +3,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrawssdk-v1 // As of Dec 2019, aws-sdk-go's go.mod does not specify a Go version. 1.6 is // the earliest version of Go tested by aws-sdk-go's CI: // https://github.com/aws/aws-sdk-go/blob/master/.travis.yml -go 1.21 +go 1.22 require ( // v1.15.0 is the first aws-sdk-go version with module support. github.com/aws/aws-sdk-go v1.34.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrawssdk-v2/go.mod b/v3/integrations/nrawssdk-v2/go.mod index 54980587d..6b06977d7 100644 --- a/v3/integrations/nrawssdk-v2/go.mod +++ b/v3/integrations/nrawssdk-v2/go.mod @@ -2,9 +2,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrawssdk-v2 // As of May 2021, the aws-sdk-go-v2 go.mod file uses 1.15: // https://github.com/aws/aws-sdk-go-v2/blob/master/go.mod -go 1.21 - -toolchain go1.21.0 +go 1.22 require ( github.com/aws/aws-sdk-go-v2 v1.30.4 @@ -14,7 +12,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.61.0 github.com/aws/aws-sdk-go-v2/service/sqs v1.34.6 github.com/aws/smithy-go v1.20.4 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrb3/go.mod b/v3/integrations/nrb3/go.mod index ce6630230..32e1315aa 100644 --- a/v3/integrations/nrb3/go.mod +++ b/v3/integrations/nrb3/go.mod @@ -1,8 +1,8 @@ module github.com/newrelic/go-agent/v3/integrations/nrb3 -go 1.21 +go 1.22 -require github.com/newrelic/go-agent/v3 v3.36.0 +require github.com/newrelic/go-agent/v3 v3.37.0 replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrecho-v3/go.mod b/v3/integrations/nrecho-v3/go.mod index 7ff8e4847..9764f69f3 100644 --- a/v3/integrations/nrecho-v3/go.mod +++ b/v3/integrations/nrecho-v3/go.mod @@ -2,13 +2,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrecho-v3 // 1.7 is the earliest version of Go tested by v3.1.0: // https://github.com/labstack/echo/blob/v3.1.0/.travis.yml -go 1.21 +go 1.22 require ( // v3.1.0 is the earliest v3 version of Echo that works with modules due // to the github.com/rsc/letsencrypt import of v3.0.0. github.com/labstack/echo v3.1.0+incompatible - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrecho-v4/go.mod b/v3/integrations/nrecho-v4/go.mod index ba2ff683d..d020c76b7 100644 --- a/v3/integrations/nrecho-v4/go.mod +++ b/v3/integrations/nrecho-v4/go.mod @@ -2,11 +2,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrecho-v4 // As of Jun 2022, the echo go.mod file uses 1.17: // https://github.com/labstack/echo/blob/master/go.mod -go 1.21 +go 1.22 require ( github.com/labstack/echo/v4 v4.9.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrelasticsearch-v7/go.mod b/v3/integrations/nrelasticsearch-v7/go.mod index 827249907..478a3f8f6 100644 --- a/v3/integrations/nrelasticsearch-v7/go.mod +++ b/v3/integrations/nrelasticsearch-v7/go.mod @@ -2,11 +2,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7 // As of Jan 2020, the v7 elasticsearch go.mod uses 1.11: // https://github.com/elastic/go-elasticsearch/blob/7.x/go.mod -go 1.21 +go 1.22 require ( github.com/elastic/go-elasticsearch/v7 v7.17.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod index b716c3d98..2e502a3a8 100644 --- a/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod +++ b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod @@ -1,9 +1,9 @@ module client-example -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 github.com/valyala/fasthttp v1.49.0 ) diff --git a/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod index be1cde658..d305a3166 100644 --- a/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod +++ b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod @@ -1,9 +1,9 @@ module server-example -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 github.com/valyala/fasthttp v1.49.0 ) diff --git a/v3/integrations/nrfasthttp/go.mod b/v3/integrations/nrfasthttp/go.mod index 2b2c9ce4a..a1d60bb34 100644 --- a/v3/integrations/nrfasthttp/go.mod +++ b/v3/integrations/nrfasthttp/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/nrfasthttp -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/valyala/fasthttp v1.49.0 ) diff --git a/v3/integrations/nrgin/go.mod b/v3/integrations/nrgin/go.mod index 9f5ae53ed..e35d61b0f 100644 --- a/v3/integrations/nrgin/go.mod +++ b/v3/integrations/nrgin/go.mod @@ -2,11 +2,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrgin // As of Dec 2019, the gin go.mod file uses 1.12: // https://github.com/gin-gonic/gin/blob/master/go.mod -go 1.21 +go 1.22 require ( github.com/gin-gonic/gin v1.9.1 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrgorilla/go.mod b/v3/integrations/nrgorilla/go.mod index 5dfb080dc..521f62344 100644 --- a/v3/integrations/nrgorilla/go.mod +++ b/v3/integrations/nrgorilla/go.mod @@ -2,12 +2,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrgorilla // As of Dec 2019, the gorilla/mux go.mod file uses 1.12: // https://github.com/gorilla/mux/blob/master/go.mod -go 1.21 +go 1.22 require ( // v1.7.0 is the earliest version of Gorilla using modules. github.com/gorilla/mux v1.7.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrgraphgophers/go.mod b/v3/integrations/nrgraphgophers/go.mod index 46a99e03d..1f08378aa 100644 --- a/v3/integrations/nrgraphgophers/go.mod +++ b/v3/integrations/nrgraphgophers/go.mod @@ -2,12 +2,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrgraphgophers // As of Jan 2020, the graphql-go go.mod file uses 1.13: // https://github.com/graph-gophers/graphql-go/blob/master/go.mod -go 1.21 +go 1.22 require ( // graphql-go has no tagged releases as of Jan 2020. github.com/graph-gophers/graphql-go v1.3.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrgraphqlgo/example/go.mod b/v3/integrations/nrgraphqlgo/example/go.mod index d753e7d06..9b2cd9e31 100644 --- a/v3/integrations/nrgraphqlgo/example/go.mod +++ b/v3/integrations/nrgraphqlgo/example/go.mod @@ -1,11 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo/example -go 1.21 +go 1.22 require ( github.com/graphql-go/graphql v0.8.1 github.com/graphql-go/graphql-go-handler v0.2.3 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo v1.0.0 ) diff --git a/v3/integrations/nrgraphqlgo/go.mod b/v3/integrations/nrgraphqlgo/go.mod index 1200e64cd..5e368653c 100644 --- a/v3/integrations/nrgraphqlgo/go.mod +++ b/v3/integrations/nrgraphqlgo/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo -go 1.21 +go 1.22 require ( github.com/graphql-go/graphql v0.8.1 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrgrpc/go.mod b/v3/integrations/nrgrpc/go.mod index c41413267..89962496f 100644 --- a/v3/integrations/nrgrpc/go.mod +++ b/v3/integrations/nrgrpc/go.mod @@ -1,12 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrgrpc -go 1.21 +go 1.22 require ( // protobuf v1.3.0 is the earliest version using modules, we use v1.3.1 // because all dependencies were removed in this version. github.com/golang/protobuf v1.5.4 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrsecurityagent v1.1.0 // v1.15.0 is the earliest version of grpc using modules. google.golang.org/grpc v1.65.0 diff --git a/v3/integrations/nrhttprouter/go.mod b/v3/integrations/nrhttprouter/go.mod index 413647f1b..223de1d19 100644 --- a/v3/integrations/nrhttprouter/go.mod +++ b/v3/integrations/nrhttprouter/go.mod @@ -2,12 +2,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrhttprouter // As of Dec 2019, the httprouter go.mod file uses 1.7: // https://github.com/julienschmidt/httprouter/blob/master/go.mod -go 1.21 +go 1.22 require ( // v1.3.0 is the earliest version of httprouter using modules. github.com/julienschmidt/httprouter v1.3.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrlambda/go.mod b/v3/integrations/nrlambda/go.mod index 66a780928..519f444db 100644 --- a/v3/integrations/nrlambda/go.mod +++ b/v3/integrations/nrlambda/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrlambda -go 1.21 +go 1.22 require ( github.com/aws/aws-lambda-go v1.41.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrlogrus/go.mod b/v3/integrations/nrlogrus/go.mod index 635cb207d..5e84e0c0a 100644 --- a/v3/integrations/nrlogrus/go.mod +++ b/v3/integrations/nrlogrus/go.mod @@ -2,10 +2,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrlogrus // As of Dec 2019, the logrus go.mod file uses 1.13: // https://github.com/sirupsen/logrus/blob/master/go.mod -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus v1.0.0 // v1.1.0 is required for the Logger.GetLevel method, and is the earliest // version of logrus using modules. diff --git a/v3/integrations/nrlogxi/go.mod b/v3/integrations/nrlogxi/go.mod index 62df3aa91..547ee712b 100644 --- a/v3/integrations/nrlogxi/go.mod +++ b/v3/integrations/nrlogxi/go.mod @@ -2,12 +2,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrlogxi // As of Dec 2019, logxi requires 1.3+: // https://github.com/mgutz/logxi#requirements -go 1.21 +go 1.22 require ( // 'v1', at commit aebf8a7d67ab, is the only logxi release. github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrmicro/go.mod b/v3/integrations/nrmicro/go.mod index 198e947ae..3c8c4316f 100644 --- a/v3/integrations/nrmicro/go.mod +++ b/v3/integrations/nrmicro/go.mod @@ -2,15 +2,15 @@ module github.com/newrelic/go-agent/v3/integrations/nrmicro // As of Dec 2019, the go-micro go.mod file uses 1.13: // https://github.com/micro/go-micro/blob/master/go.mod -go 1.21 +go 1.22 -toolchain go1.23.4 +toolchain go1.24.0 require ( github.com/golang/protobuf v1.5.4 github.com/micro/go-micro v1.8.0 - github.com/newrelic/go-agent/v3 v3.36.0 - google.golang.org/protobuf v1.36.2 + github.com/newrelic/go-agent/v3 v3.37.0 + google.golang.org/protobuf v1.36.4 ) diff --git a/v3/integrations/nrmongo/go.mod b/v3/integrations/nrmongo/go.mod index 40d3f7e55..f8940127a 100644 --- a/v3/integrations/nrmongo/go.mod +++ b/v3/integrations/nrmongo/go.mod @@ -2,10 +2,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrmongo // As of Dec 2019, 1.10 is the mongo-driver requirement: // https://github.com/mongodb/mongo-go-driver#requirements -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 // mongo-driver does not support modules as of Nov 2019. go.mongodb.org/mongo-driver v1.10.2 ) diff --git a/v3/integrations/nrmssql/go.mod b/v3/integrations/nrmssql/go.mod index 9dc94301f..7fd42ce37 100644 --- a/v3/integrations/nrmssql/go.mod +++ b/v3/integrations/nrmssql/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrmssql -go 1.21 +go 1.22 require ( github.com/microsoft/go-mssqldb v0.19.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrmysql/go.mod b/v3/integrations/nrmysql/go.mod index b138567a1..0ec3bb555 100644 --- a/v3/integrations/nrmysql/go.mod +++ b/v3/integrations/nrmysql/go.mod @@ -1,13 +1,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrmysql // 1.10 is the Go version in mysql's go.mod -go 1.21 +go 1.22 require ( // v1.5.0 is the first mysql version to support gomod github.com/go-sql-driver/mysql v1.6.0 // v3.3.0 includes the new location of ParseQuery - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrnats/go.mod b/v3/integrations/nrnats/go.mod index 1d4a43bb5..1db1142f1 100644 --- a/v3/integrations/nrnats/go.mod +++ b/v3/integrations/nrnats/go.mod @@ -2,14 +2,14 @@ module github.com/newrelic/go-agent/v3/integrations/nrnats // As of Jun 2023, 1.19 is the earliest version of Go tested by nats: // https://github.com/nats-io/nats.go/blob/master/.travis.yml -go 1.21 +go 1.22 toolchain go1.23.4 require ( github.com/nats-io/nats-server v1.4.1 github.com/nats-io/nats.go v1.36.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrnats/test/go.mod b/v3/integrations/nrnats/test/go.mod index dd4ba151f..edb347588 100644 --- a/v3/integrations/nrnats/test/go.mod +++ b/v3/integrations/nrnats/test/go.mod @@ -1,14 +1,14 @@ module github.com/newrelic/go-agent/v3/integrations/test // This module exists to avoid having extra nrnats module dependencies. -go 1.21 +go 1.22 replace github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 => ../ require ( github.com/nats-io/nats-server v1.4.1 github.com/nats-io/nats.go v1.17.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrnats v1.0.0 ) diff --git a/v3/integrations/nropenai/go.mod b/v3/integrations/nropenai/go.mod index 2d23bf859..8815609cb 100644 --- a/v3/integrations/nropenai/go.mod +++ b/v3/integrations/nropenai/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nropenai -go 1.21 +go 1.22 require ( github.com/google/uuid v1.6.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/sashabaranov/go-openai v1.20.2 ) diff --git a/v3/integrations/nrpgx/example/sqlx/go.mod b/v3/integrations/nrpgx/example/sqlx/go.mod index 20f20a6b7..bf14c707e 100644 --- a/v3/integrations/nrpgx/example/sqlx/go.mod +++ b/v3/integrations/nrpgx/example/sqlx/go.mod @@ -1,10 +1,10 @@ // This sqlx example is a separate module to avoid adding sqlx dependency to the // nrpgx go.mod file. module github.com/newrelic/go-agent/v3/integrations/nrpgx/example/sqlx -go 1.21 +go 1.22 require ( github.com/jmoiron/sqlx v1.2.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrpgx v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrpgx => ../../ diff --git a/v3/integrations/nrpgx/go.mod b/v3/integrations/nrpgx/go.mod index c032bb1d8..13154d423 100644 --- a/v3/integrations/nrpgx/go.mod +++ b/v3/integrations/nrpgx/go.mod @@ -1,11 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrpgx -go 1.21 +go 1.22 require ( github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx/v4 v4.18.2 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrpgx5/go.mod b/v3/integrations/nrpgx5/go.mod index 06763870c..a819200e8 100644 --- a/v3/integrations/nrpgx5/go.mod +++ b/v3/integrations/nrpgx5/go.mod @@ -1,13 +1,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrpgx5 -go 1.21 +go 1.22 -toolchain go1.23.4 +toolchain go1.24.0 require ( github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 github.com/jackc/pgx/v5 v5.5.4 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/stretchr/testify v1.8.1 ) diff --git a/v3/integrations/nrpkgerrors/go.mod b/v3/integrations/nrpkgerrors/go.mod index b2d258022..e8a391566 100644 --- a/v3/integrations/nrpkgerrors/go.mod +++ b/v3/integrations/nrpkgerrors/go.mod @@ -2,10 +2,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrpkgerrors // As of Dec 2019, 1.11 is the earliest version of Go tested by pkg/errors: // https://github.com/pkg/errors/blob/master/.travis.yml -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 // v0.8.0 was the last release in 2016, and when // major development on pkg/errors stopped. github.com/pkg/errors v0.8.0 diff --git a/v3/integrations/nrpq/example/sqlx/go.mod b/v3/integrations/nrpq/example/sqlx/go.mod index 074017c9f..012e71a6f 100644 --- a/v3/integrations/nrpq/example/sqlx/go.mod +++ b/v3/integrations/nrpq/example/sqlx/go.mod @@ -1,11 +1,11 @@ // This sqlx example is a separate module to avoid adding sqlx dependency to the // nrpq go.mod file. module github.com/newrelic/go-agent/v3/integrations/nrpq/example/sqlx -go 1.21 +go 1.22 require ( github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.1.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrpq v0.0.0 ) replace github.com/newrelic/go-agent/v3/integrations/nrpq => ../../ diff --git a/v3/integrations/nrpq/go.mod b/v3/integrations/nrpq/go.mod index f167ccaad..b2e26c944 100644 --- a/v3/integrations/nrpq/go.mod +++ b/v3/integrations/nrpq/go.mod @@ -1,12 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrpq -go 1.21 +go 1.22 require ( // NewConnector dsn parsing tests expect v1.1.0 error return behavior. github.com/lib/pq v1.1.0 // v3.3.0 includes the new location of ParseQuery - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrredis-v7/go.mod b/v3/integrations/nrredis-v7/go.mod index 85c860dd5..4b390a173 100644 --- a/v3/integrations/nrredis-v7/go.mod +++ b/v3/integrations/nrredis-v7/go.mod @@ -1,11 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrredis-v7 // https://github.com/go-redis/redis/blob/master/go.mod -go 1.21 +go 1.22 require ( github.com/go-redis/redis/v7 v7.0.0-beta.5 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrredis-v8/go.mod b/v3/integrations/nrredis-v8/go.mod index 268b9dd83..9b14aceb3 100644 --- a/v3/integrations/nrredis-v8/go.mod +++ b/v3/integrations/nrredis-v8/go.mod @@ -1,11 +1,11 @@ module github.com/newrelic/go-agent/v3/integrations/nrredis-v8 // https://github.com/go-redis/redis/blob/master/go.mod -go 1.21 +go 1.22 require ( github.com/go-redis/redis/v8 v8.4.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrredis-v9/go.mod b/v3/integrations/nrredis-v9/go.mod index b9dd9cab5..6e128104b 100644 --- a/v3/integrations/nrredis-v9/go.mod +++ b/v3/integrations/nrredis-v9/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrredis-v9 // https://github.com/redis/go-redis/blob/a38f75b640398bd709ee46c778a23e80e09d48b5/go.mod#L3 -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/redis/go-redis/v9 v9.0.2 ) diff --git a/v3/integrations/nrsarama/go.mod b/v3/integrations/nrsarama/go.mod index e1ba449cc..c7e9a5bc5 100644 --- a/v3/integrations/nrsarama/go.mod +++ b/v3/integrations/nrsarama/go.mod @@ -1,12 +1,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrsarama -go 1.21 +go 1.22 -toolchain go1.23.4 +toolchain go1.24.0 require ( github.com/Shopify/sarama v1.38.1 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/stretchr/testify v1.8.1 ) diff --git a/v3/integrations/nrsecurityagent/go.mod b/v3/integrations/nrsecurityagent/go.mod index a2eca34e3..d21b617e6 100644 --- a/v3/integrations/nrsecurityagent/go.mod +++ b/v3/integrations/nrsecurityagent/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent -go 1.21 +go 1.22 require ( github.com/newrelic/csec-go-agent v1.6.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/v3/integrations/nrslog/go.mod b/v3/integrations/nrslog/go.mod index 9477b0517..c4a543ff9 100644 --- a/v3/integrations/nrslog/go.mod +++ b/v3/integrations/nrslog/go.mod @@ -1,10 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrslog // The new log/slog package in Go 1.21 brings structured logging to the standard library. -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/stretchr/testify v1.9.0 ) diff --git a/v3/integrations/nrsnowflake/go.mod b/v3/integrations/nrsnowflake/go.mod index f91663a7f..8c352d40a 100644 --- a/v3/integrations/nrsnowflake/go.mod +++ b/v3/integrations/nrsnowflake/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/nrsnowflake -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/snowflakedb/gosnowflake v1.6.19 ) diff --git a/v3/integrations/nrsqlite3/go.mod b/v3/integrations/nrsqlite3/go.mod index f0f596a61..352ef32ce 100644 --- a/v3/integrations/nrsqlite3/go.mod +++ b/v3/integrations/nrsqlite3/go.mod @@ -2,12 +2,12 @@ module github.com/newrelic/go-agent/v3/integrations/nrsqlite3 // As of Dec 2019, 1.9 is the oldest version of Go tested by go-sqlite3: // https://github.com/mattn/go-sqlite3/blob/master/.travis.yml -go 1.21 +go 1.22 require ( github.com/mattn/go-sqlite3 v1.0.0 // v3.3.0 includes the new location of ParseQuery - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrstan/examples/go.mod b/v3/integrations/nrstan/examples/go.mod index c2152ab2a..fd71967fc 100644 --- a/v3/integrations/nrstan/examples/go.mod +++ b/v3/integrations/nrstan/examples/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan/examples // This module exists to avoid a dependency on nrnrats. -go 1.21 +go 1.22 require ( github.com/nats-io/stan.go v0.5.0 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrnats v0.0.0 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) diff --git a/v3/integrations/nrstan/go.mod b/v3/integrations/nrstan/go.mod index f9efe8d18..9a4329e6e 100644 --- a/v3/integrations/nrstan/go.mod +++ b/v3/integrations/nrstan/go.mod @@ -2,13 +2,13 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan // As of Dec 2019, 1.11 is the earliest Go version tested by Stan: // https://github.com/nats-io/stan.go/blob/master/.travis.yml -go 1.21 +go 1.22 toolchain go1.23.4 require ( github.com/nats-io/stan.go v0.10.4 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 ) diff --git a/v3/integrations/nrstan/test/go.mod b/v3/integrations/nrstan/test/go.mod index 5494f8611..cd32affa0 100644 --- a/v3/integrations/nrstan/test/go.mod +++ b/v3/integrations/nrstan/test/go.mod @@ -2,14 +2,14 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan/test // This module exists to avoid a dependency on // github.com/nats-io/nats-streaming-server in nrstan. -go 1.21 +go 1.22 toolchain go1.23.4 require ( github.com/nats-io/nats-streaming-server v0.25.6 github.com/nats-io/stan.go v0.10.4 - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) diff --git a/v3/integrations/nrzap/go.mod b/v3/integrations/nrzap/go.mod index ead8500ac..dac36ebbb 100644 --- a/v3/integrations/nrzap/go.mod +++ b/v3/integrations/nrzap/go.mod @@ -2,10 +2,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrzap // As of Dec 2019, zap has 1.13 in their go.mod file: // https://github.com/uber-go/zap/blob/master/go.mod -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 // v1.12.0 is the earliest version of zap using modules. go.uber.org/zap v1.12.0 ) diff --git a/v3/integrations/nrzerolog/go.mod b/v3/integrations/nrzerolog/go.mod index 5c01e5398..d87298451 100644 --- a/v3/integrations/nrzerolog/go.mod +++ b/v3/integrations/nrzerolog/go.mod @@ -1,9 +1,9 @@ module github.com/newrelic/go-agent/v3/integrations/nrzerolog -go 1.21 +go 1.22 require ( - github.com/newrelic/go-agent/v3 v3.36.0 + github.com/newrelic/go-agent/v3 v3.37.0 github.com/rs/zerolog v1.28.0 ) replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index ccdf274ac..c229246e5 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -333,11 +333,7 @@ func (txn *Transaction) startSegmentAt(at time.Time) SegmentStartTime { // // ... code you want to time here ... // segment.End() func (txn *Transaction) StartSegment(name string) *Segment { - if nilTransaction(txn) { - return &Segment{} // return a non-nil Segment to avoid nil dereference - } - - if IsSecurityAgentPresent() && txn.thread.thread != nil && txn.thread.thread.threadID > 0 { + if IsSecurityAgentPresent() && !nilTransaction(txn) && txn.thread.thread != nil && txn.thread.thread.threadID > 0 { // async segment start secureAgent.SendEvent("NEW_GOROUTINE_LINKER", txn.thread.getCsecData()) } diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index 0d80ae73b..8469401eb 100644 --- a/v3/newrelic/version.go +++ b/v3/newrelic/version.go @@ -11,7 +11,7 @@ import ( const ( // Version is the full string version of this Go Agent. - Version = "3.36.0" + Version = "3.37.0" ) var ( From 2f2a7fee89c889afb4c3eb31ef8a65a9ea0e8f10 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Mon, 24 Feb 2025 17:03:06 -0800 Subject: [PATCH 14/14] typo in changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee2e790b..0108d8249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,13 @@ - Labels are defined via `ConfigLabels(...)` or `NEW_RELIC_LABELS` - Added memory allocation limit detection/response mechanism to facilitate calling custom functions to perform application-specific resource management functionality, report custom metrics or events, or take other appropriate actions, in response to rising heap memory size. +### Fixed + - Added protection around transaction methods to gracefully return when the transaction object is `nil`. + ### Support statement We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. See the [Go agent EOL Policy](/docs/apm/agents/go-agent/get-started/go-agent-eol-policy) for details about supported versions of the Go agent and third-party components. -### Fixed - - Added protection around transaction methods to gracefully return when the transaction object is `nil`. ## 3.36.0 ### Enhanced