diff --git a/CHANGELOG.md b/CHANGELOG.md index c04cb8e1d..0108d8249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 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. + +### 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. + + ## 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/examples/oom/main.go b/v3/examples/oom/main.go new file mode 100644 index 000000000..396e041da --- /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); err != nil { + 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..588aede53 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -1,6 +1,6 @@ module github.com/newrelic/go-agent/v3 -go 1.21 +go 1.22 require ( google.golang.org/grpc v1.65.0 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/attributes.go b/v3/integrations/logcontext-v2/nrslog/attributes.go new file mode 100644 index 000000000..582069ca1 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/attributes.go @@ -0,0 +1,97 @@ +package nrslog + +import ( + "log/slog" + "maps" + "strings" +) + +type attributeCache struct { + preCompiledAttributes map[string]interface{} + prefix string +} + +func newAttributeCache() *attributeCache { + return &attributeCache{ + preCompiledAttributes: make(map[string]interface{}), + prefix: "", + } +} + +func (c *attributeCache) clone() *attributeCache { + return &attributeCache{ + preCompiledAttributes: maps.Clone(c.preCompiledAttributes), + prefix: c.prefix, + } +} + +func (c *attributeCache) copyPreCompiledAttributes() map[string]interface{} { + return maps.Clone(c.preCompiledAttributes) +} + +func (c *attributeCache) getPrefix() string { + return c.prefix +} + +// 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 +} + +// 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 + } + + for _, a := range attrs { + c.appendAttr(c.preCompiledAttributes, a, c.prefix) + } +} + +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 + } + + // majority of runtime spent allocating and copying strings + group := strings.Builder{} + group.Grow(len(groupPrefix) + len(a.Key) + 1) + group.WriteString(groupPrefix) + + if a.Key != "" { + 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.go b/v3/integrations/logcontext-v2/nrslog/config.go new file mode 100644 index 000000000..24e69a796 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/config.go @@ -0,0 +1,80 @@ +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 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 +} + +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/go.mod b/v3/integrations/logcontext-v2/nrslog/go.mod index 0dc5ec35e..4e2dfc63a 100644 --- a/v3/integrations/logcontext-v2/nrslog/go.mod +++ b/v3/integrations/logcontext-v2/nrslog/go.mod @@ -1,8 +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.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/nrslog/handler.go b/v3/integrations/logcontext-v2/nrslog/handler.go index 9c476910f..bad4b5153 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler.go +++ b/v3/integrations/logcontext-v2/nrslog/handler.go @@ -2,150 +2,180 @@ package nrslog import ( "context" - "io" + "errors" "log/slog" + "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 - w *LogWriter app *newrelic.Application 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 +// newHandler is an internal helper function to create a new NRHandler +func newHandler(app *newrelic.Application, handler slog.Handler) *NRHandler { + return &NRHandler{ + handler: handler, + attributeCache: newAttributeCache(), + configCache: newConfigCache(), + linkingCache: newLinkingCache(), + 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 +// 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 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 + } + if handler == nil { + return handler } - h := logger.Handler() - switch nrHandler := h.(type) { - case NRHandler: - txnHandler := nrHandler.WithTransaction(txn) - return slog.New(txnHandler) + switch handler.(type) { + case *NRHandler: + return handler default: - return logger + return newHandler(app, handler) } } -// 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) -} +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 WrapHandler(app *newrelic.Application, handler slog.Handler) NRHandler { - return NRHandler{ - handler: handler, - app: app, +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 } -} -// addWriter is an internal helper function to append an io.Writer to the NRHandler object -func (h *NRHandler) addWriter(w *LogWriter) { - h.w = w + return newHandler(app, handler), nil } -// 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{ - handler: h.handler, - app: h.app, - txn: txn, - } +// 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)) +} - if h.w != nil { - writer := h.w.WithTransaction(txn) - handler.addWriter(&writer) +// 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(), + configCache: h.configCache.clone(), + linkingCache: h.linkingCache.clone(), + app: h.app, + txn: h.txn, } +} - 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. // 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) } // 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.) -// -// 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 { - attrs := map[string]interface{}{} - - record.Attrs(func(attr slog.Attr) bool { - attrs[attr.Key] = attr.Value.Any() - return true - }) - - data := newrelic.LogData{ - Severity: record.Level.String(), - Timestamp: record.Time.UnixMilli(), - Message: record.Message, - Attributes: attrs, - } - if h.txn != nil { - h.txn.RecordLog(data) +func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error { + // 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 + } + + // if no app or txn, invoke underlying handler + if h.app == nil && nrTxn == nil { + return h.handler.Handle(ctx, record) + } + + // timestamp must be sent to newrelic + var timestamp int64 + if record.Time.IsZero() { + timestamp = time.Now().UnixMilli() } else { - h.app.RecordLog(data) + timestamp = record.Time.UnixMilli() + } + + if h.shouldForwardLogs(h.app) { + attrs := h.copyPreCompiledAttributes() // coppies cached attribute map, todo: optimize to avoid map coppies + prefix := h.getPrefix() + + record.Attrs(func(attr slog.Attr) bool { + h.appendAttr(attrs, attr, prefix) + return true + }) + + data := newrelic.LogData{ + Severity: record.Level.String(), + Timestamp: timestamp, + Message: record.Message, + Attributes: attrs, + } + if nrTxn != nil { + nrTxn.RecordLog(data) + } else { + h.app.RecordLog(data) + } + } + + // 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) @@ -153,79 +183,60 @@ 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 { - handler := h.handler.WithAttrs(attrs) - return NRHandler{ - handler: handler, - app: h.app, - txn: h.txn, +// +// 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 { + if len(attrs) == 0 { + return h } + 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 // 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 { - handler := h.handler.WithGroup(name) - return NRHandler{ - handler: handler, - app: h.app, - txn: h.txn, +func (h *NRHandler) WithGroup(name string) slog.Handler { + if name == "" { + return h } -} -// 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 + 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 // transaction from context. -func WithTransactionFromContext(handler NRHandler) TransactionFromContextHandler { - return TransactionFromContextHandler{handler} +// +// Deprecated: this is a no-op +func WithTransactionFromContext(handler slog.Handler) slog.Handler { + return 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) +const newrelicAttributeKey = "newrelic" + +func (h *NRHandler) enrichRecord(app *newrelic.Application, record *slog.Record) { + str := nrLinkingString(h.getAgentLinkingMetadata(app)) + if str == "" { + return + } + + record.AddAttrs(slog.String(newrelicAttributeKey, str)) +} + +func (h *NRHandler) enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) { + str := nrLinkingString(h.getTransactionLinkingMetadata(txn)) + if str == "" { + return + } + + 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 4e3d6d774..8f21b6fce 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -3,10 +3,11 @@ package nrslog import ( "bytes" "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 +44,92 @@ func TestHandler(t *testing.T) { }) } +func TestWrap(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + handler := slog.NewTextHandler(io.Discard, &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{})) + 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), @@ -89,12 +176,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{ { @@ -206,34 +292,95 @@ 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) { 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, + }) + + // purge the buffer + writer.Reset() txn := app.StartTransaction("example transaction") ctx := newrelic.NewContext(context.Background(), txn) @@ -241,11 +388,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(), @@ -257,49 +416,26 @@ 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, }, }, }) - } -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!" - log = log.With(slog.Group("test group", slog.String("string key", "val"))) - 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) - } - -} - -func TestTransactionFromContextHandler(t *testing.T) { +// Ensure deprecation compatibility +func TestDeprecatedWithTransactionFromContext(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), @@ -335,3 +471,267 @@ func TestTransactionFromContextHandler(t *testing.T) { }, }) } + +func TestWithComplexAttributeOrGroup(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) + + log.With(attr).WithGroup("group3").With(slog.String("key3", "val3")).Info(message) + + 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", + }, + }, + { + Severity: slog.LevelInfo.String(), + Message: message, + Timestamp: internal.MatchAnyUnixMilli, + Attributes: map[string]interface{}{ + "group.key": "val", + "group.group2.key2": "val2", + "group3.key3": "val3", + }, + }, + }) +} + +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) + } +} + +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{} + 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), + 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), + ) + + h, _ := Wrap(app.Application, slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + txn := app.Application.StartTransaction("my txn") + defer txn.End() + record := slog.Record{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + h.enrichRecord(app.Application, &record) + } +} + +func BenchmarkLinkingString(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++ { + nrLinkingString(md) + } +} + +func BenchmarkShouldEnrichLog(b *testing.B) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + 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() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + h.shouldEnrichLog(app.Application) + } +} diff --git a/v3/integrations/logcontext-v2/nrslog/linking.go b/v3/integrations/logcontext-v2/nrslog/linking.go new file mode 100644 index 000000000..e93064789 --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/linking.go @@ -0,0 +1,112 @@ +package nrslog + +import ( + "strings" + + "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, + } +} + +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() +} 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() + } +} diff --git a/v3/integrations/logcontext-v2/nrslog/wrap.go b/v3/integrations/logcontext-v2/nrslog/wrap.go new file mode 100644 index 000000000..ef35ab7be --- /dev/null +++ b/v3/integrations/logcontext-v2/nrslog/wrap.go @@ -0,0 +1,65 @@ +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. +// +// Deprecated: Use WrapHandler() instead. +func TextHandler(app *newrelic.Application, w io.Writer, opts *slog.HandlerOptions) slog.Handler { + 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) slog.Handler { + return WrapHandler(app, slog.NewJSONHandler(w, opts)) +} + +// 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 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. +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 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. +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 + } +} 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)) -} 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/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/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 { 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/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/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 diff --git a/v3/newrelic/oom_monitor.go b/v3/newrelic/oom_monitor.go new file mode 100644 index 000000000..fcae5ff9e --- /dev/null +++ b/v3/newrelic/oom_monitor.go @@ -0,0 +1,156 @@ +// 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 + } + a.app.heapHighWaterMarkAlarms.sampleTicker = 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) +} diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 16a384faa..c229246e5 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,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 IsSecurityAgentPresent() && txn != nil && txn.thread != nil && 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()) } @@ -346,7 +355,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 +376,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 +388,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 +478,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 +497,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 +519,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 +532,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 +541,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 +552,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") +} 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 (