From 8890896f5d91acfbd641c0bf903eb392e9acc670 Mon Sep 17 00:00:00 2001 From: Aayush garg Date: Thu, 11 Apr 2024 17:44:37 +0530 Subject: [PATCH 01/21] Fix for correct struct field name for echo version v3.2.2+ --- v3/integrations/nrecho-v3/nrecho.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/v3/integrations/nrecho-v3/nrecho.go b/v3/integrations/nrecho-v3/nrecho.go index 9fe29f54b..06de72784 100644 --- a/v3/integrations/nrecho-v3/nrecho.go +++ b/v3/integrations/nrecho-v3/nrecho.go @@ -35,6 +35,22 @@ func handlerPointer(handler echo.HandlerFunc) uintptr { return reflect.ValueOf(handler).Pointer() } +func handlerName(router interface{}) string { + val := reflect.ValueOf(router) + if val.Kind() == reflect.Ptr { // for echo version v3.2.2+ + val = val.Elem() + } else { + val = reflect.ValueOf(&router).Elem().Elem() + } + if name := val.FieldByName("Name"); name.IsValid() { // for echo version v3.2.2+ + return name.String() + } else if handler := val.FieldByName("Handler"); handler.IsValid() { + return handler.String() + } else { + return "" + } +} + func transactionName(c echo.Context) string { ptr := handlerPointer(c.Handler()) if ptr == handlerPointer(echo.NotFoundHandler) { @@ -108,7 +124,7 @@ func WrapRouter(engine *echo.Echo) { if engine != nil && newrelic.IsSecurityAgentPresent() { router := engine.Routes() for _, r := range router { - newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, r.Handler) + newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, handlerName(r)) } } } From 2ff793ada089406f3f0d56b23ecf6673bacb8539 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 12 Apr 2024 13:43:47 -0400 Subject: [PATCH 02/21] working first pass at log attribute emission --- v3/newrelic/attributes_from_internal.go | 50 ++++++++++ v3/newrelic/harvest_test.go | 2 + v3/newrelic/log_event.go | 20 +++- v3/newrelic/log_events.go | 2 +- v3/newrelic/log_events_test.go | 117 ++++++++++++++++-------- 5 files changed, 150 insertions(+), 41 deletions(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index fca46d9cf..45928becd 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -5,10 +5,12 @@ package newrelic import ( "bytes" + "encoding/json" "fmt" "math" "net/http" "net/url" + "reflect" "sort" "strconv" "strings" @@ -490,6 +492,54 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { } } +// This is capable of consuming maps and structs, but this is expensive. +// If possible, pass them already stringified. +func writeLogAttributeJSON(w *jsonFieldsWriter, key string, val any) { + switch v := val.(type) { + case string: + w.stringField(key, v) + case bool: + if v { + w.rawField(key, `true`) + } else { + w.rawField(key, `false`) + } + case uint8: + w.intField(key, int64(v)) + case uint16: + w.intField(key, int64(v)) + case uint32: + w.intField(key, int64(v)) + case uint64: + w.intField(key, int64(v)) + case uint: + w.intField(key, int64(v)) + case uintptr: + w.intField(key, int64(v)) + case int8: + w.intField(key, int64(v)) + case int16: + w.intField(key, int64(v)) + case int32: + w.intField(key, int64(v)) + case int64: + w.intField(key, v) + case int: + w.intField(key, int64(v)) + case float32: + w.floatField(key, float64(v)) + case float64: + w.floatField(key, v) + default: + if reflect.ValueOf(v).Kind() == reflect.Struct || reflect.ValueOf(v).Kind() == reflect.Map { + bytes, _ := json.Marshal(v) + w.rawField(key, jsonString(bytes)) + } else { + w.stringField(key, fmt.Sprintf("%T", v)) + } + } +} + func agentAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet, additionalAttributes ...map[string]string) { if a == nil { buf.WriteString("{}") diff --git a/v3/newrelic/harvest_test.go b/v3/newrelic/harvest_test.go index 73fdd9b71..9e9f00314 100644 --- a/v3/newrelic/harvest_test.go +++ b/v3/newrelic/harvest_test.go @@ -315,6 +315,7 @@ func TestHarvestLogEventsReady(t *testing.T) { }) logEvent := logEvent{ + nil, 0.5, 123456, "INFO", @@ -576,6 +577,7 @@ func TestMergeFailedHarvest(t *testing.T) { }, 0) logEvent := logEvent{ + nil, 0.5, 123456, "INFO", diff --git a/v3/newrelic/log_event.go b/v3/newrelic/log_event.go index 5fd705ef0..14ba5299c 100644 --- a/v3/newrelic/log_event.go +++ b/v3/newrelic/log_event.go @@ -19,6 +19,7 @@ const ( ) type logEvent struct { + atributes map[string]any priority priority timestamp int64 severity string @@ -28,10 +29,14 @@ type logEvent struct { } // LogData contains data fields that are needed to generate log events. +// Note: if you are passing a struct or map as an attribute, try to pass it as a string. The collector can parse that on New Relic's side. +// This is preferable because the json.Marshal method used to create the string log JSON is less efficient than the tools built into +// logging products for creating stringified json for complex objects and data structures. type LogData struct { - Timestamp int64 // Optional: Unix Millisecond Timestamp; A timestamp will be generated if unset - Severity string // Optional: Severity of log being consumed - Message string // Optional: Message of log being consumed; Maximum size: 32768 Bytes. + Timestamp int64 // Optional: Unix Millisecond Timestamp; A timestamp will be generated if unset + Severity string // Optional: Severity of log being consumed + Message string // Optional: Message of log being consumed; Maximum size: 32768 Bytes. + Attributes map[string]any // Optional: a key value pair with a string key, and any value. This can be used for categorizing logs in the UI. } // writeJSON prepares JSON in the format expected by the collector. @@ -51,6 +56,14 @@ func (e *logEvent) WriteJSON(buf *bytes.Buffer) { w.needsComma = false buf.WriteByte(',') w.intField(logcontext.LogTimestampFieldName, e.timestamp) + if e.atributes != nil && len(e.atributes) > 0 { + buf.WriteString(`,"attributes":{`) + w := jsonFieldsWriter{buf: buf} + for key, val := range e.atributes { + writeLogAttributeJSON(&w, key, val) + } + buf.WriteByte('}') + } buf.WriteByte('}') } @@ -88,6 +101,7 @@ func (data *LogData) toLogEvent() (logEvent, error) { message: data.Message, severity: data.Severity, timestamp: data.Timestamp, + atributes: data.Attributes, } return event, nil diff --git a/v3/newrelic/log_events.go b/v3/newrelic/log_events.go index c02b76058..df3861570 100644 --- a/v3/newrelic/log_events.go +++ b/v3/newrelic/log_events.go @@ -60,7 +60,7 @@ type logEventHeap []logEvent // TODO: when go 1.18 becomes the minimum supported version, re-write to make a generic heap implementation // for all event heaps, to de-duplicate this code -//func (events *logEvents) +// func (events *logEvents) func (h logEventHeap) Len() int { return len(h) } func (h logEventHeap) Less(i, j int) bool { return h[i].priority.isLowerPriority(h[j].priority) } func (h logEventHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } diff --git a/v3/newrelic/log_events_test.go b/v3/newrelic/log_events_test.go index 2a796d06a..978c833f7 100644 --- a/v3/newrelic/log_events_test.go +++ b/v3/newrelic/log_events_test.go @@ -36,19 +36,20 @@ func loggingConfigEnabled(limit int) loggingConfig { } } -func sampleLogEvent(priority priority, severity, message string) *logEvent { +func sampleLogEvent(priority priority, severity, message string, attributes map[string]any) *logEvent { return &logEvent{ priority: priority, severity: severity, message: message, + atributes: attributes, timestamp: 123456, } } func TestBasicLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(5)) - events.Add(sampleLogEvent(0.5, infoLevel, "message1")) - events.Add(sampleLogEvent(0.5, infoLevel, "message2")) + events.Add(sampleLogEvent(0.5, infoLevel, "message1", nil)) + events.Add(sampleLogEvent(0.5, infoLevel, "message2", nil)) json, err := events.CollectorJSON(agentRunID) if nil != err { @@ -70,6 +71,47 @@ func TestBasicLogEvents(t *testing.T) { } } +type testStruct struct { + A string + B int + C c +} + +type c struct { + D string +} + +func TestBasicLogEventWithAttributes(t *testing.T) { + st := testStruct{ + A: "a", + B: 1, + C: c{"hello"}, + } + + events := newLogEvents(testCommonAttributes, loggingConfigEnabled(5)) + events.Add(sampleLogEvent(0.5, infoLevel, "message1", map[string]any{"one": 1, "two": "hi"})) + events.Add(sampleLogEvent(0.5, infoLevel, "message2", map[string]any{"complex": st, "map": map[string]string{"hi": "hello"}})) + + json, err := events.CollectorJSON(agentRunID) + if nil != err { + t.Fatal(err) + } + + expected := commonJSON + + `{"level":"INFO","message":"message1","timestamp":123456,"attributes":{"one":1,"two":"hi"}},` + + `{"level":"INFO","message":"message2","timestamp":123456,"attributes":{"complex":{"A":"a","B":1,"C":{"D":"hello"}},"map":{"hi":"hello"}}}]}]` + + if string(json) != expected { + t.Error(string(json), expected) + } + if events.numSeen != 2 { + t.Error(events.numSeen) + } + if events.NumSaved() != 2 { + t.Error(events.NumSaved()) + } +} + func TestEmptyLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) json, err := events.CollectorJSON(agentRunID) @@ -79,10 +121,10 @@ func TestEmptyLogEvents(t *testing.T) { if nil != json { t.Error(string(json)) } - if 0 != events.numSeen { + if events.numSeen != 0 { t.Error(events.numSeen) } - if 0 != events.NumSaved() { + if events.NumSaved() != 0 { t.Error(events.NumSaved()) } } @@ -91,12 +133,12 @@ func TestEmptyLogEvents(t *testing.T) { func TestSamplingLogEvents(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - events.Add(sampleLogEvent(0.999999, infoLevel, "a")) - events.Add(sampleLogEvent(0.1, infoLevel, "b")) - events.Add(sampleLogEvent(0.9, infoLevel, "c")) - events.Add(sampleLogEvent(0.2, infoLevel, "d")) - events.Add(sampleLogEvent(0.8, infoLevel, "e")) - events.Add(sampleLogEvent(0.3, infoLevel, "f")) + events.Add(sampleLogEvent(0.999999, infoLevel, "a", nil)) + events.Add(sampleLogEvent(0.1, infoLevel, "b", nil)) + events.Add(sampleLogEvent(0.9, infoLevel, "c", nil)) + events.Add(sampleLogEvent(0.2, infoLevel, "d", nil)) + events.Add(sampleLogEvent(0.8, infoLevel, "e", nil)) + events.Add(sampleLogEvent(0.3, infoLevel, "f", nil)) json, err := events.CollectorJSON(agentRunID) if nil != err { @@ -141,14 +183,14 @@ func TestMergeFullLogEvents(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) + e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) + e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) - e2.Add(sampleLogEvent(0.06, infoLevel, "d")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) + e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) + e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) + e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e1.Merge(e2) json, err := e1.CollectorJSON(agentRunID) @@ -176,14 +218,14 @@ func TestLogEventMergeFailedSuccess(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) + e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) + e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) - e2.Add(sampleLogEvent(0.06, infoLevel, "d")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) + e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) + e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) + e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e1.mergeFailed(e2) @@ -214,14 +256,14 @@ func TestLogEventMergeFailedLimitReached(t *testing.T) { e1 := newLogEvents(testCommonAttributes, loggingConfigEnabled(2)) e2 := newLogEvents(testCommonAttributes, loggingConfigEnabled(3)) - e1.Add(sampleLogEvent(0.1, infoLevel, "a")) - e1.Add(sampleLogEvent(0.15, infoLevel, "b")) - e1.Add(sampleLogEvent(0.25, infoLevel, "c")) + e1.Add(sampleLogEvent(0.1, infoLevel, "a", nil)) + e1.Add(sampleLogEvent(0.15, infoLevel, "b", nil)) + e1.Add(sampleLogEvent(0.25, infoLevel, "c", nil)) - e2.Add(sampleLogEvent(0.06, infoLevel, "d")) - e2.Add(sampleLogEvent(0.12, infoLevel, "e")) - e2.Add(sampleLogEvent(0.18, infoLevel, "f")) - e2.Add(sampleLogEvent(0.24, infoLevel, "g")) + e2.Add(sampleLogEvent(0.06, infoLevel, "d", nil)) + e2.Add(sampleLogEvent(0.12, infoLevel, "e", nil)) + e2.Add(sampleLogEvent(0.18, infoLevel, "f", nil)) + e2.Add(sampleLogEvent(0.24, infoLevel, "g", nil)) e2.failedHarvests = failedEventsAttemptsLimit @@ -253,7 +295,7 @@ func TestLogEventsSplitFull(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 15; i++ { priority := priority(float32(i) / 10.0) - events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } // Test that the capacity cannot exceed the max. if 10 != events.capacity() { @@ -292,7 +334,7 @@ func TestLogEventsSplitNotFullOdd(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 7; i++ { priority := priority(float32(i) / 10.0) - events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) @@ -322,7 +364,7 @@ func TestLogEventsSplitNotFullEven(t *testing.T) { events := newLogEvents(testCommonAttributes, loggingConfigEnabled(10)) for i := 0; i < 8; i++ { priority := priority(float32(i) / 10.0) - events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority))) + events.Add(sampleLogEvent(priority, "INFO", fmt.Sprint(priority), nil)) } e1, e2 := events.split() j1, err1 := e1.CollectorJSON(agentRunID) @@ -356,7 +398,7 @@ func TestLogEventsZeroCapacity(t *testing.T) { if 0 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } - events.Add(sampleLogEvent(0.5, "INFO", "TEST")) + events.Add(sampleLogEvent(0.5, "INFO", "TEST", nil)) if 1 != events.NumSeen() || 0 != events.NumSaved() || 0 != events.capacity() { t.Error(events.NumSeen(), events.NumSaved(), events.capacity()) } @@ -375,7 +417,7 @@ func TestLogEventCollectionDisabled(t *testing.T) { if 0 != events.NumSeen() || 0 != len(events.severityCount) || 0 != events.NumSaved() || 5 != events.capacity() { t.Error(events.NumSeen(), len(events.severityCount), events.NumSaved(), events.capacity()) } - events.Add(sampleLogEvent(0.5, "INFO", "TEST")) + events.Add(sampleLogEvent(0.5, "INFO", "TEST", nil)) if 1 != events.NumSeen() || 1 != len(events.severityCount) || 0 != events.NumSaved() || 5 != events.capacity() { t.Error(events.NumSeen(), len(events.severityCount), events.NumSaved(), events.capacity()) } @@ -467,6 +509,7 @@ func BenchmarkRecordLoggingMetrics(b *testing.B) { for i := 0; i < internal.MaxLogEvents; i++ { logEvent := logEvent{ + nil, newPriority(), 123456, "INFO", From 13aa082ad0683504c207478ea3828e0bc9188dc8 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 17 Apr 2024 13:10:04 -0400 Subject: [PATCH 03/21] create json string that is truncated --- v3/newrelic/attributes_from_internal.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index 45928becd..f04e086db 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -494,6 +494,7 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { // This is capable of consuming maps and structs, but this is expensive. // If possible, pass them already stringified. +// note that other than the additional support for complex structs, this is a 1:1 clone of writeAgentAttributeValues() func writeLogAttributeJSON(w *jsonFieldsWriter, key string, val any) { switch v := val.(type) { case string: @@ -531,9 +532,13 @@ func writeLogAttributeJSON(w *jsonFieldsWriter, key string, val any) { case float64: w.floatField(key, v) default: + // attempt to construct a JSON string if reflect.ValueOf(v).Kind() == reflect.Struct || reflect.ValueOf(v).Kind() == reflect.Map { bytes, _ := json.Marshal(v) - w.rawField(key, jsonString(bytes)) + if len(bytes) > 254 { + bytes = bytes[:254] + } + w.stringField(key, string(bytes)) } else { w.stringField(key, fmt.Sprintf("%T", v)) } From 559e3ce0f0a080d5f2e1dc5495c5d9c1887fbaaf Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 17 Apr 2024 14:14:17 -0400 Subject: [PATCH 04/21] better tests --- v3/newrelic/attributes_from_internal.go | 2 +- v3/newrelic/log_events_test.go | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index f04e086db..af0a36b3a 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -533,7 +533,7 @@ func writeLogAttributeJSON(w *jsonFieldsWriter, key string, val any) { w.floatField(key, v) default: // attempt to construct a JSON string - if reflect.ValueOf(v).Kind() == reflect.Struct || reflect.ValueOf(v).Kind() == reflect.Map { + if reflect.ValueOf(v).Kind() == reflect.Struct || reflect.ValueOf(v).Kind() == reflect.Map || reflect.ValueOf(v).Kind() == reflect.Slice || reflect.ValueOf(v).Kind() == reflect.Array { bytes, _ := json.Marshal(v) if len(bytes) > 254 { bytes = bytes[:254] diff --git a/v3/newrelic/log_events_test.go b/v3/newrelic/log_events_test.go index 978c833f7..d820998c7 100644 --- a/v3/newrelic/log_events_test.go +++ b/v3/newrelic/log_events_test.go @@ -89,8 +89,11 @@ func TestBasicLogEventWithAttributes(t *testing.T) { } events := newLogEvents(testCommonAttributes, loggingConfigEnabled(5)) - events.Add(sampleLogEvent(0.5, infoLevel, "message1", map[string]any{"one": 1, "two": "hi"})) - events.Add(sampleLogEvent(0.5, infoLevel, "message2", map[string]any{"complex": st, "map": map[string]string{"hi": "hello"}})) + events.Add(sampleLogEvent(0.5, infoLevel, "message1", map[string]any{"two": "hi"})) + events.Add(sampleLogEvent(0.5, infoLevel, "message2", map[string]any{"struct": st})) + events.Add(sampleLogEvent(0.5, infoLevel, "message3", map[string]any{"map": map[string]string{"hi": "hello"}})) + events.Add(sampleLogEvent(0.5, infoLevel, "message4", map[string]any{"slice": []string{"hi", "hello", "test"}})) + events.Add(sampleLogEvent(0.5, infoLevel, "message5", map[string]any{"array": [2]int{1, 2}})) json, err := events.CollectorJSON(agentRunID) if nil != err { @@ -98,16 +101,19 @@ func TestBasicLogEventWithAttributes(t *testing.T) { } expected := commonJSON + - `{"level":"INFO","message":"message1","timestamp":123456,"attributes":{"one":1,"two":"hi"}},` + - `{"level":"INFO","message":"message2","timestamp":123456,"attributes":{"complex":{"A":"a","B":1,"C":{"D":"hello"}},"map":{"hi":"hello"}}}]}]` + `{"level":"INFO","message":"message1","timestamp":123456,"attributes":{"two":"hi"}},` + + `{"level":"INFO","message":"message2","timestamp":123456,"attributes":{"struct":"{\"A\":\"a\",\"B\":1,\"C\":{\"D\":\"hello\"}}"}},` + + `{"level":"INFO","message":"message3","timestamp":123456,"attributes":{"map":"{\"hi\":\"hello\"}"}},` + + `{"level":"INFO","message":"message4","timestamp":123456,"attributes":{"slice":"[\"hi\",\"hello\",\"test\"]"}},` + + `{"level":"INFO","message":"message5","timestamp":123456,"attributes":{"array":"[1,2]"}}]}]` if string(json) != expected { - t.Error(string(json), expected) + t.Error("actual not equal to expected:\n", string(json), "\n", expected) } - if events.numSeen != 2 { + if events.numSeen != 5 { t.Error(events.numSeen) } - if events.NumSaved() != 2 { + if events.NumSaved() != 5 { t.Error(events.NumSaved()) } } From 8659a499fe9b61e19e837db644c7a9388f1cb890 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 17 Apr 2024 14:21:14 -0400 Subject: [PATCH 05/21] better doc text --- v3/newrelic/log_event.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v3/newrelic/log_event.go b/v3/newrelic/log_event.go index 14ba5299c..5bb388075 100644 --- a/v3/newrelic/log_event.go +++ b/v3/newrelic/log_event.go @@ -29,9 +29,10 @@ type logEvent struct { } // LogData contains data fields that are needed to generate log events. -// Note: if you are passing a struct or map as an attribute, try to pass it as a string. The collector can parse that on New Relic's side. -// This is preferable because the json.Marshal method used to create the string log JSON is less efficient than the tools built into -// logging products for creating stringified json for complex objects and data structures. +// Note: if you are passing a struct, map, slice, or array as an attribute, try to pass it as a JSON string generated by the logging framework if possible. +// The collector can parse that into an object on New Relic's side. +// This is preferable because the json.Marshal method used in the agent to create the string log JSON is usually less efficient than the tools built into +// logging products for creating stringified JSON for complex objects and data structures. type LogData struct { Timestamp int64 // Optional: Unix Millisecond Timestamp; A timestamp will be generated if unset Severity string // Optional: Severity of log being consumed From ef701fd46854199d508fda3d8191ff2a73774a97 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 17 Apr 2024 15:16:22 -0400 Subject: [PATCH 06/21] consolidate code for attribute JSON creation --- v3/newrelic/attributes_from_internal.go | 44 ------------------------- v3/newrelic/log_event.go | 2 +- 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index af0a36b3a..cf45f3499 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -452,50 +452,6 @@ func addUserAttribute(a *attributes, key string, val interface{}, d destinationS } func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { - switch v := val.(type) { - case string: - w.stringField(key, v) - case bool: - if v { - w.rawField(key, `true`) - } else { - w.rawField(key, `false`) - } - case uint8: - w.intField(key, int64(v)) - case uint16: - w.intField(key, int64(v)) - case uint32: - w.intField(key, int64(v)) - case uint64: - w.intField(key, int64(v)) - case uint: - w.intField(key, int64(v)) - case uintptr: - w.intField(key, int64(v)) - case int8: - w.intField(key, int64(v)) - case int16: - w.intField(key, int64(v)) - case int32: - w.intField(key, int64(v)) - case int64: - w.intField(key, v) - case int: - w.intField(key, int64(v)) - case float32: - w.floatField(key, float64(v)) - case float64: - w.floatField(key, v) - default: - w.stringField(key, fmt.Sprintf("%T", v)) - } -} - -// This is capable of consuming maps and structs, but this is expensive. -// If possible, pass them already stringified. -// note that other than the additional support for complex structs, this is a 1:1 clone of writeAgentAttributeValues() -func writeLogAttributeJSON(w *jsonFieldsWriter, key string, val any) { switch v := val.(type) { case string: w.stringField(key, v) diff --git a/v3/newrelic/log_event.go b/v3/newrelic/log_event.go index 5bb388075..18f79e595 100644 --- a/v3/newrelic/log_event.go +++ b/v3/newrelic/log_event.go @@ -61,7 +61,7 @@ func (e *logEvent) WriteJSON(buf *bytes.Buffer) { buf.WriteString(`,"attributes":{`) w := jsonFieldsWriter{buf: buf} for key, val := range e.atributes { - writeLogAttributeJSON(&w, key, val) + writeAttributeValueJSON(&w, key, val) } buf.WriteByte('}') } From 15fac4b2064dd2eeca2c036199e5db5c236fda20 Mon Sep 17 00:00:00 2001 From: mirackara Date: Thu, 18 Apr 2024 12:04:25 -0500 Subject: [PATCH 07/21] Zap Field Attributes Support --- .../logcontext-v2/nrzap/example/main.go | 12 ++++- v3/integrations/logcontext-v2/nrzap/nrzap.go | 44 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrzap/example/main.go b/v3/integrations/logcontext-v2/nrzap/example/main.go index fb7912403..c7e696a9d 100644 --- a/v3/integrations/logcontext-v2/nrzap/example/main.go +++ b/v3/integrations/logcontext-v2/nrzap/example/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "time" @@ -29,7 +30,7 @@ func main() { } backgroundLogger := zap.New(backgroundCore) - backgroundLogger.Info("this is a background log message") + backgroundLogger.Info("this is a background log message with fields test", zap.Any("foo", 3.14)) txn := app.StartTransaction("nrzap example transaction") txnCore, err := nrzap.WrapTransactionCore(core, txn) @@ -38,7 +39,14 @@ func main() { } txnLogger := zap.New(txnCore) - txnLogger.Info("this is a transaction log message") + txnLogger.Info("this is a transaction log message", + zap.String("region", "nr-east"), + zap.Int("int", 123), + zap.Duration("duration", 1*time.Second), + ) + + err = errors.New("this is an error") + txnLogger.Error("this is an error log message", zap.Error(err)) txn.End() diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap.go b/v3/integrations/logcontext-v2/nrzap/nrzap.go index cf4823f3b..b6eb55e8b 100644 --- a/v3/integrations/logcontext-v2/nrzap/nrzap.go +++ b/v3/integrations/logcontext-v2/nrzap/nrzap.go @@ -2,6 +2,9 @@ package nrzap import ( "errors" + "fmt" + "math" + "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/newrelic" @@ -24,12 +27,47 @@ type newrelicApplicationState struct { txn *newrelic.Transaction } +func convertField(fields []zap.Field) map[string]interface{} { + attributes := make(map[string]interface{}) + for _, field := range fields { + switch field.Type { + + case zapcore.BoolType: + attributes[field.Key] = field.Integer == 1 + case zapcore.Float32Type: + attributes[field.Key] = math.Float32frombits(uint32(field.Integer)) + case zapcore.Float64Type: + attributes[field.Key] = math.Float64frombits(uint64(field.Integer)) + case zapcore.Int64Type, zapcore.Int32Type, zapcore.Int16Type, zapcore.Int8Type: + attributes[field.Key] = field.Integer + case zapcore.StringType: + attributes[field.Key] = field.String + case zapcore.Uint64Type, zapcore.Uint32Type, zapcore.Uint16Type, zapcore.Uint8Type: + attributes[field.Key] = uint64(field.Integer) + case zapcore.DurationType: + attributes[field.Key] = time.Duration(field.Integer) + case zapcore.TimeType: + attributes[field.Key] = time.Unix(0, field.Integer) + case zapcore.TimeFullType: + attributes[field.Key] = time.Unix(0, field.Integer).UTC() + case zapcore.ErrorType: + attributes[field.Key] = field.Interface.(error).Error() + case zapcore.BinaryType: + attributes[field.Key] = field.Interface + default: + attributes[field.Key] = fmt.Sprintf("%v", field.Interface) + } + } + return attributes +} + // internal handler function to manage writing a log to the new relic application func (nr *newrelicApplicationState) recordLog(entry zapcore.Entry, fields []zap.Field) { data := newrelic.LogData{ - Timestamp: entry.Time.UnixMilli(), - Severity: entry.Level.String(), - Message: entry.Message, + Timestamp: entry.Time.UnixMilli(), + Severity: entry.Level.String(), + Message: entry.Message, + Attributes: convertField(fields), } if nr.txn != nil { From ba742216f0afc8ad64c2c2b81f658db44df68c5b Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Thu, 18 Apr 2024 13:53:33 -0400 Subject: [PATCH 08/21] truncate strings to 256 bytes --- v3/newrelic/attributes_from_internal.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index cf45f3499..430c5752a 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -21,6 +21,9 @@ const ( // listed as span attributes to simplify code. It is not listed in the // public attributes.go file for this reason to prevent confusion. spanAttributeQueryParameters = "query_parameters" + + // The collector can only allow attributes to be a maximum of 256 bytes + maxAttributeLengthBytes = 256 ) var ( @@ -454,6 +457,9 @@ func addUserAttribute(a *attributes, key string, val interface{}, d destinationS func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { switch v := val.(type) { case string: + if len(v) > maxAttributeLengthBytes { + v = v[:maxAttributeLengthBytes] + } w.stringField(key, v) case bool: if v { @@ -489,10 +495,11 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { w.floatField(key, v) default: // attempt to construct a JSON string - if reflect.ValueOf(v).Kind() == reflect.Struct || reflect.ValueOf(v).Kind() == reflect.Map || reflect.ValueOf(v).Kind() == reflect.Slice || reflect.ValueOf(v).Kind() == reflect.Array { + kind := reflect.ValueOf(v).Kind() + if kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice || kind == reflect.Array { bytes, _ := json.Marshal(v) - if len(bytes) > 254 { - bytes = bytes[:254] + if len(bytes) > maxAttributeLengthBytes { + bytes = bytes[:maxAttributeLengthBytes] } w.stringField(key, string(bytes)) } else { From 4da86dd49178910bd6848a296c1d151669f65bd7 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Thu, 18 Apr 2024 16:30:07 -0700 Subject: [PATCH 09/21] updated dependency --- v3/integrations/nrsecurityagent/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/integrations/nrsecurityagent/go.mod b/v3/integrations/nrsecurityagent/go.mod index 321e774b6..78f9814bc 100644 --- a/v3/integrations/nrsecurityagent/go.mod +++ b/v3/integrations/nrsecurityagent/go.mod @@ -3,7 +3,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent go 1.20 require ( - github.com/newrelic/csec-go-agent v1.1.0 + github.com/newrelic/csec-go-agent v1.2.0 github.com/newrelic/go-agent/v3 v3.32.0 github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0 gopkg.in/yaml.v2 v2.4.0 From 3f71008ae7cecfeea1c87a3f371b6951d75b6ade Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 19 Apr 2024 10:48:59 -0400 Subject: [PATCH 10/21] Context Driven Handler slog --- v3/integrations/logcontext-v2/nrslog/example/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrslog/example/main.go b/v3/integrations/logcontext-v2/nrslog/example/main.go index 57a7d11d1..9ee09ff8e 100644 --- a/v3/integrations/logcontext-v2/nrslog/example/main.go +++ b/v3/integrations/logcontext-v2/nrslog/example/main.go @@ -4,6 +4,7 @@ import ( "log/slog" "os" "time" + "context" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog" "github.com/newrelic/go-agent/v3/newrelic" @@ -24,8 +25,9 @@ func main() { log.Info("I am a log message") txn := app.StartTransaction("example transaction") - txnLogger := nrslog.WithTransaction(txn, log) - txnLogger.Info("I am a log inside a transaction") + ctx := newrelic.NewContext(context.Background(), txn) + + txnLogger.InfoContext(ctx, "I am a log inside a transaction") // pretend to do some work time.Sleep(500 * time.Millisecond) From ecd7e2223da1cf7a02218429b5d74a90f3f74330 Mon Sep 17 00:00:00 2001 From: mirackara Date: Mon, 22 Apr 2024 11:31:06 -0500 Subject: [PATCH 11/21] Zap Attributes Testing + Refactor --- .../logcontext-v2/nrzap/example/main.go | 9 +- v3/integrations/logcontext-v2/nrzap/nrzap.go | 38 +++----- .../logcontext-v2/nrzap/nrzap_test.go | 88 +++++++++++++++++++ v3/internal/expect.go | 11 +-- v3/newrelic/expect_implementation.go | 71 +++++++++++++++ 5 files changed, 180 insertions(+), 37 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrzap/example/main.go b/v3/integrations/logcontext-v2/nrzap/example/main.go index c7e696a9d..57569fb1f 100644 --- a/v3/integrations/logcontext-v2/nrzap/example/main.go +++ b/v3/integrations/logcontext-v2/nrzap/example/main.go @@ -15,6 +15,7 @@ func main() { app, err := newrelic.NewApplication( newrelic.ConfigAppName("nrzerolog example"), newrelic.ConfigInfoLogger(os.Stdout), + newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigFromEnvironment(), ) if err != nil { @@ -37,15 +38,15 @@ func main() { if err != nil && err != nrzap.ErrNilTxn { panic(err) } - txnLogger := zap.New(txnCore) txnLogger.Info("this is a transaction log message", - zap.String("region", "nr-east"), + zap.String("region", "region-test-2"), zap.Int("int", 123), - zap.Duration("duration", 1*time.Second), + zap.Duration("duration", 200*time.Millisecond), + zap.Any("zapmap", map[string]any{"pi": 3.14, "duration": 2 * time.Second}), ) - err = errors.New("this is an error") + err = errors.New("OW! an error occurred") txnLogger.Error("this is an error log message", zap.Error(err)) txn.End() diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap.go b/v3/integrations/logcontext-v2/nrzap/nrzap.go index b6eb55e8b..894a6b09f 100644 --- a/v3/integrations/logcontext-v2/nrzap/nrzap.go +++ b/v3/integrations/logcontext-v2/nrzap/nrzap.go @@ -2,8 +2,6 @@ package nrzap import ( "errors" - "fmt" - "math" "time" "github.com/newrelic/go-agent/v3/internal" @@ -27,35 +25,19 @@ type newrelicApplicationState struct { txn *newrelic.Transaction } +// Helper function that converts zap fields to a map of string interface func convertField(fields []zap.Field) map[string]interface{} { attributes := make(map[string]interface{}) for _, field := range fields { - switch field.Type { - - case zapcore.BoolType: - attributes[field.Key] = field.Integer == 1 - case zapcore.Float32Type: - attributes[field.Key] = math.Float32frombits(uint32(field.Integer)) - case zapcore.Float64Type: - attributes[field.Key] = math.Float64frombits(uint64(field.Integer)) - case zapcore.Int64Type, zapcore.Int32Type, zapcore.Int16Type, zapcore.Int8Type: - attributes[field.Key] = field.Integer - case zapcore.StringType: - attributes[field.Key] = field.String - case zapcore.Uint64Type, zapcore.Uint32Type, zapcore.Uint16Type, zapcore.Uint8Type: - attributes[field.Key] = uint64(field.Integer) - case zapcore.DurationType: - attributes[field.Key] = time.Duration(field.Integer) - case zapcore.TimeType: - attributes[field.Key] = time.Unix(0, field.Integer) - case zapcore.TimeFullType: - attributes[field.Key] = time.Unix(0, field.Integer).UTC() - case zapcore.ErrorType: - attributes[field.Key] = field.Interface.(error).Error() - case zapcore.BinaryType: - attributes[field.Key] = field.Interface - default: - attributes[field.Key] = fmt.Sprintf("%v", field.Interface) + enc := zapcore.NewMapObjectEncoder() + field.AddTo(enc) + for key, value := range enc.Fields { + // Format time.Duration values as strings + if durationVal, ok := value.(time.Duration); ok { + attributes[key] = durationVal.String() + } else { + attributes[key] = value + } } } return attributes diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap_test.go b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go index 33adfcd26..32448b2fe 100644 --- a/v3/integrations/logcontext-v2/nrzap/nrzap_test.go +++ b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go @@ -1,10 +1,12 @@ package nrzap import ( + "encoding/json" "errors" "io" "os" "testing" + "time" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" @@ -131,6 +133,9 @@ func TestTransactionLogger(t *testing.T) { app.ExpectLogEvents(t, []internal.WantLog{ { + Attributes: map[string]interface{}{ + "test-key": "test-val", + }, Severity: zap.ErrorLevel.String(), Message: msg, Timestamp: internal.MatchAnyUnixMilli, @@ -140,6 +145,56 @@ func TestTransactionLogger(t *testing.T) { }) } +func TestTransactionLoggerWithFields(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + ) + + txn := app.StartTransaction("test transaction") + txnMetadata := txn.GetTraceMetadata() + + core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) + wrappedCore, err := WrapTransactionCore(core, txn) + if err != nil { + t.Error(err) + } + + logger := zap.New(wrappedCore) + + msg := "this is a test info message" + + // for background logging: + logger.Info(msg, + zap.String("region", "region-test-2"), + zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), + zap.Duration("duration", 1*time.Second), + zap.Int("int", 123), + zap.Bool("bool", true), + ) + + logger.Sync() + + // ensure txn gets written to an event and logs get released + txn.End() + + app.ExpectLogEvents(t, []internal.WantLog{ + { + Attributes: map[string]interface{}{ + "region": "region-test-2", + "anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}, + "duration": 1 * time.Second, + "int": 123, + "bool": true, + }, + Severity: zap.InfoLevel.String(), + Message: msg, + Timestamp: internal.MatchAnyUnixMilli, + TraceID: txnMetadata.TraceID, + SpanID: txnMetadata.SpanID, + }, + }) +} func TestTransactionLoggerNilTxn(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), @@ -204,6 +259,39 @@ func BenchmarkZapBaseline(b *testing.B) { } } +func BenchmarkFieldConversion(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + convertField([]zap.Field{zap.String("test-key", "test-val")}) + } +} + +func BenchmarkFieldUnmarshalling(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + fields := []zap.Field{zap.String("test-key", "test-val")} + for i := 0; i < b.N; i++ { + attributes := make(map[string]interface{}) + for _, field := range fields { + jsonBytes, _ := json.Marshal(field.Interface) + attributes[field.Key] = jsonBytes + } + } +} + +func BenchmarkZapWithAttribute(b *testing.B) { + core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(io.Discard), zap.InfoLevel) + logger := zap.New(core) + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + logger.Info("this is a test message", zap.Any("test-key", "test-val")) + } +} + func BenchmarkZapWrappedCore(b *testing.B) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), diff --git a/v3/internal/expect.go b/v3/internal/expect.go index edb92bdc2..6ed3d4f46 100644 --- a/v3/internal/expect.go +++ b/v3/internal/expect.go @@ -29,11 +29,12 @@ type WantError struct { // WantLog is a traced log event expectation type WantLog struct { - Severity string - Message string - SpanID string - TraceID string - Timestamp int64 + Attributes map[string]interface{} + Severity string + Message string + SpanID string + TraceID string + Timestamp int64 } func uniquePointer() *struct{} { diff --git a/v3/newrelic/expect_implementation.go b/v3/newrelic/expect_implementation.go index 970ad4d07..0c688a878 100644 --- a/v3/newrelic/expect_implementation.go +++ b/v3/newrelic/expect_implementation.go @@ -249,8 +249,79 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog v.Error(fmt.Sprintf("unexpected log timestamp: got %d, want %d", actual.timestamp, want.Timestamp)) return } + + if actual.atributes != nil && want.Attributes != nil { + for k, val := range want.Attributes { + actualVal, actualOk := actual.atributes[k] + if !actualOk { + v.Error(fmt.Sprintf("expected log attribute for key %v is missing", k)) + return + } + // TO:DO -- Correct handling of maps. Currently, we're just checking for the presences of a specific key + if k == "anyValue" { + // Special handling for map comparison + expectedMap, ok := val.(map[string]interface{}) + if !ok { + v.Error(fmt.Sprintf("type assertion to map[string]interface{} failed for key %v", k)) + return + } + actualMap, ok := actualVal.(map[string]interface{}) + if !ok { + v.Error(fmt.Sprintf("type assertion to map[string]interface{} failed for actual value of key %v", k)) + return + } + if !expectLogEventAttributesMaps(expectedMap, actualMap) { + v.Error(fmt.Sprintf("unexpected log attribute for key %v: got %v, want %v", k, actualMap, expectedMap)) + return + } + } + } + } + } +// Helper function that compares two maps for equality. This is used to compare the attribute fields of log events. +func expectLogEventAttributesMaps(a, b map[string]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v := range a { + if bv, ok := b[k]; !ok { + return false + } else { + switch v := v.(type) { + case float64: + if bv, ok := bv.(float64); !ok || v != bv { + return false + } + + case int: + if bv, ok := bv.(int); !ok || v != bv { + return false + } + case time.Duration: + if bv, ok := bv.(time.Duration); ok { + return v == bv + } + case string: + if bv, ok := bv.(string); !ok || v != bv { + return false + } + case int64: + if bv, ok := bv.(int64); !ok || v != bv { + return false + } + case map[string]interface{}: + if bv, ok := bv.(map[string]interface{}); !ok || !expectLogEventAttributesMaps(v, bv) { + return false + } + default: + return false + } + } + } + return true +} func expectEvent(v internal.Validator, e json.Marshaler, expect internal.WantEvent) { js, err := e.MarshalJSON() if nil != err { From 3d728611bc2599b86eb4de94fedba8af36500225 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 22 Apr 2024 13:31:55 -0400 Subject: [PATCH 12/21] typo in internal log attribute field name --- v3/newrelic/log_event.go | 28 ++++++++++++++-------------- v3/newrelic/log_events_test.go | 10 +++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/v3/newrelic/log_event.go b/v3/newrelic/log_event.go index 18f79e595..700be9d24 100644 --- a/v3/newrelic/log_event.go +++ b/v3/newrelic/log_event.go @@ -19,13 +19,13 @@ const ( ) type logEvent struct { - atributes map[string]any - priority priority - timestamp int64 - severity string - message string - spanID string - traceID string + attributes map[string]any + priority priority + timestamp int64 + severity string + message string + spanID string + traceID string } // LogData contains data fields that are needed to generate log events. @@ -57,10 +57,10 @@ func (e *logEvent) WriteJSON(buf *bytes.Buffer) { w.needsComma = false buf.WriteByte(',') w.intField(logcontext.LogTimestampFieldName, e.timestamp) - if e.atributes != nil && len(e.atributes) > 0 { + if e.attributes != nil && len(e.attributes) > 0 { buf.WriteString(`,"attributes":{`) w := jsonFieldsWriter{buf: buf} - for key, val := range e.atributes { + for key, val := range e.attributes { writeAttributeValueJSON(&w, key, val) } buf.WriteByte('}') @@ -98,11 +98,11 @@ func (data *LogData) toLogEvent() (logEvent, error) { data.Severity = strings.TrimSpace(data.Severity) event := logEvent{ - priority: newPriority(), - message: data.Message, - severity: data.Severity, - timestamp: data.Timestamp, - atributes: data.Attributes, + priority: newPriority(), + message: data.Message, + severity: data.Severity, + timestamp: data.Timestamp, + attributes: data.Attributes, } return event, nil diff --git a/v3/newrelic/log_events_test.go b/v3/newrelic/log_events_test.go index d820998c7..e359ef55a 100644 --- a/v3/newrelic/log_events_test.go +++ b/v3/newrelic/log_events_test.go @@ -38,11 +38,11 @@ func loggingConfigEnabled(limit int) loggingConfig { func sampleLogEvent(priority priority, severity, message string, attributes map[string]any) *logEvent { return &logEvent{ - priority: priority, - severity: severity, - message: message, - atributes: attributes, - timestamp: 123456, + priority: priority, + severity: severity, + message: message, + attributes: attributes, + timestamp: 123456, } } From 3baa6146722042f70b542df761fb166fc57a58c5 Mon Sep 17 00:00:00 2001 From: mirackara Date: Mon, 22 Apr 2024 13:27:40 -0500 Subject: [PATCH 13/21] Better tests/more zap field examples --- .../logcontext-v2/nrzap/example/main.go | 14 +++++++--- v3/newrelic/expect_implementation.go | 28 ++++++++----------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrzap/example/main.go b/v3/integrations/logcontext-v2/nrzap/example/main.go index 57569fb1f..7a45532cc 100644 --- a/v3/integrations/logcontext-v2/nrzap/example/main.go +++ b/v3/integrations/logcontext-v2/nrzap/example/main.go @@ -39,10 +39,16 @@ func main() { panic(err) } txnLogger := zap.New(txnCore) - txnLogger.Info("this is a transaction log message", - zap.String("region", "region-test-2"), - zap.Int("int", 123), - zap.Duration("duration", 200*time.Millisecond), + txnLogger.Info("this is a transaction log message with custom fields", + zap.String("zapstring", "region-test-2"), + zap.Int("zapint", 123), + zap.Duration("zapduration", 200*time.Millisecond), + zap.Bool("zapbool", true), + zap.Object("zapobject", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error { + enc.AddString("foo", "bar") + return nil + })), + zap.Any("zapmap", map[string]any{"pi": 3.14, "duration": 2 * time.Second}), ) diff --git a/v3/newrelic/expect_implementation.go b/v3/newrelic/expect_implementation.go index 0c688a878..8ce3dd305 100644 --- a/v3/newrelic/expect_implementation.go +++ b/v3/newrelic/expect_implementation.go @@ -257,21 +257,16 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog v.Error(fmt.Sprintf("expected log attribute for key %v is missing", k)) return } - // TO:DO -- Correct handling of maps. Currently, we're just checking for the presences of a specific key - if k == "anyValue" { - // Special handling for map comparison - expectedMap, ok := val.(map[string]interface{}) - if !ok { - v.Error(fmt.Sprintf("type assertion to map[string]interface{} failed for key %v", k)) - return - } - actualMap, ok := actualVal.(map[string]interface{}) - if !ok { - v.Error(fmt.Sprintf("type assertion to map[string]interface{} failed for actual value of key %v", k)) - return - } - if !expectLogEventAttributesMaps(expectedMap, actualMap) { - v.Error(fmt.Sprintf("unexpected log attribute for key %v: got %v, want %v", k, actualMap, expectedMap)) + + // Check if both values are maps, and if so, compare them recursively + if expectedMap, ok := val.(map[string]interface{}); ok { + if actualMap, ok := actualVal.(map[string]interface{}); ok { + if !expectLogEventAttributesMaps(expectedMap, actualMap) { + v.Error(fmt.Sprintf("unexpected log attribute for key %v: got %v, want %v", k, actualMap, expectedMap)) + return + } + } else { + v.Error(fmt.Sprintf("actual value for key %v is not a map", k)) return } } @@ -280,7 +275,7 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog } -// Helper function that compares two maps for equality. This is used to compare the attribute fields of log events. +// Helper function that compares two maps for equality. This is used to compare the attribute fields of log events expected vs received func expectLogEventAttributesMaps(a, b map[string]interface{}) bool { if len(a) != len(b) { return false @@ -311,6 +306,7 @@ func expectLogEventAttributesMaps(a, b map[string]interface{}) bool { if bv, ok := bv.(int64); !ok || v != bv { return false } + // if the type of the field is a map, recursively compare the maps case map[string]interface{}: if bv, ok := bv.(map[string]interface{}); !ok || !expectLogEventAttributesMaps(v, bv) { return false From 4f155e01e1647ccae2dccc9934e5a9982577b2c7 Mon Sep 17 00:00:00 2001 From: mirackara Date: Mon, 22 Apr 2024 13:35:03 -0500 Subject: [PATCH 14/21] updated expect implementation --- v3/newrelic/expect_implementation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/newrelic/expect_implementation.go b/v3/newrelic/expect_implementation.go index 8ce3dd305..122d588ff 100644 --- a/v3/newrelic/expect_implementation.go +++ b/v3/newrelic/expect_implementation.go @@ -250,9 +250,9 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog return } - if actual.atributes != nil && want.Attributes != nil { + if actual.attributes != nil && want.Attributes != nil { for k, val := range want.Attributes { - actualVal, actualOk := actual.atributes[k] + actualVal, actualOk := actual.attributes[k] if !actualOk { v.Error(fmt.Sprintf("expected log attribute for key %v is missing", k)) return From 4dbcccb1a3cdd7e063e532d3992fbb61e92ed6b7 Mon Sep 17 00:00:00 2001 From: mirackara Date: Wed, 24 Apr 2024 14:33:01 -0500 Subject: [PATCH 15/21] Add Zap logger field attribute encoding option --- .../logcontext-v2/nrzap/example/main.go | 2 + v3/integrations/logcontext-v2/nrzap/nrzap.go | 53 +++++++++++++- .../logcontext-v2/nrzap/nrzap_test.go | 71 ++++++++++++++++--- v3/newrelic/config.go | 6 +- v3/newrelic/config_options.go | 7 ++ 5 files changed, 128 insertions(+), 11 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrzap/example/main.go b/v3/integrations/logcontext-v2/nrzap/example/main.go index 7a45532cc..84b1f0eda 100644 --- a/v3/integrations/logcontext-v2/nrzap/example/main.go +++ b/v3/integrations/logcontext-v2/nrzap/example/main.go @@ -17,6 +17,8 @@ func main() { newrelic.ConfigInfoLogger(os.Stdout), newrelic.ConfigDebugLogger(os.Stdout), newrelic.ConfigFromEnvironment(), + // This is enabled by default. if disabled, the attributes will be marshalled at harvest time. + newrelic.ConfigZapAttributesEncoder(false), ) if err != nil { panic(err) diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap.go b/v3/integrations/logcontext-v2/nrzap/nrzap.go index 894a6b09f..ae5a7ff8f 100644 --- a/v3/integrations/logcontext-v2/nrzap/nrzap.go +++ b/v3/integrations/logcontext-v2/nrzap/nrzap.go @@ -2,6 +2,7 @@ package nrzap import ( "errors" + "math" "time" "github.com/newrelic/go-agent/v3/internal" @@ -26,7 +27,7 @@ type newrelicApplicationState struct { } // Helper function that converts zap fields to a map of string interface -func convertField(fields []zap.Field) map[string]interface{} { +func convertFieldWithMapEncoder(fields []zap.Field) map[string]interface{} { attributes := make(map[string]interface{}) for _, field := range fields { enc := zapcore.NewMapObjectEncoder() @@ -43,13 +44,61 @@ func convertField(fields []zap.Field) map[string]interface{} { return attributes } +func convertFieldsAtHarvestTime(fields []zap.Field) map[string]interface{} { + attributes := make(map[string]interface{}) + for _, field := range fields { + if field.Interface != nil { + + // Handles ErrorType fields + if field.Type == zapcore.ErrorType { + attributes[field.Key] = field.Interface.(error).Error() + } else { + // Handles all interface types + attributes[field.Key] = field.Interface + } + + } else if field.String != "" { // Check if the field is a string and doesn't contain an interface + attributes[field.Key] = field.String + + } else { + // Float Types + if field.Type == zapcore.Float32Type { + attributes[field.Key] = math.Float32frombits(uint32(field.Integer)) + continue + } else if field.Type == zapcore.Float64Type { + attributes[field.Key] = math.Float64frombits(uint64(field.Integer)) + continue + } + // Bool Type + if field.Type == zapcore.BoolType { + field.Interface = field.Integer == 1 + attributes[field.Key] = field.Interface + } else { + // Integer Types + attributes[field.Key] = field.Integer + + } + } + } + return attributes +} + // internal handler function to manage writing a log to the new relic application func (nr *newrelicApplicationState) recordLog(entry zapcore.Entry, fields []zap.Field) { + attributes := map[string]interface{}{} + cfg, _ := nr.app.Config() + + // Check if the attributes should be frontloaded or marshalled at harvest time + if cfg.ApplicationLogging.ZapLogger.AttributesFrontloaded { + attributes = convertFieldWithMapEncoder(fields) + } else { + attributes = convertFieldsAtHarvestTime(fields) + } data := newrelic.LogData{ Timestamp: entry.Time.UnixMilli(), Severity: entry.Level.String(), Message: entry.Message, - Attributes: convertField(fields), + Attributes: attributes, } if nr.txn != nil { diff --git a/v3/integrations/logcontext-v2/nrzap/nrzap_test.go b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go index 32448b2fe..34100166b 100644 --- a/v3/integrations/logcontext-v2/nrzap/nrzap_test.go +++ b/v3/integrations/logcontext-v2/nrzap/nrzap_test.go @@ -1,7 +1,6 @@ package nrzap import ( - "encoding/json" "errors" "io" "os" @@ -149,6 +148,7 @@ func TestTransactionLoggerWithFields(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), newrelic.ConfigAppLogForwardingEnabled(true), + newrelic.ConfigZapAttributesEncoder(true), ) txn := app.StartTransaction("test transaction") @@ -195,6 +195,59 @@ func TestTransactionLoggerWithFields(t *testing.T) { }, }) } + +func TestTransactionLoggerWithFieldsAtHarvestTime(t *testing.T) { + app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, + newrelic.ConfigAppLogDecoratingEnabled(true), + newrelic.ConfigAppLogForwardingEnabled(true), + newrelic.ConfigZapAttributesEncoder(false), + ) + + txn := app.StartTransaction("test transaction") + txnMetadata := txn.GetTraceMetadata() + + core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel) + wrappedCore, err := WrapTransactionCore(core, txn) + if err != nil { + t.Error(err) + } + + logger := zap.New(wrappedCore) + + msg := "this is a test info message" + + // for background logging: + logger.Info(msg, + zap.String("region", "region-test-2"), + zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), + zap.Duration("duration", 1*time.Second), + zap.Int("int", 123), + zap.Bool("bool", true), + ) + + logger.Sync() + + // ensure txn gets written to an event and logs get released + txn.End() + + app.ExpectLogEvents(t, []internal.WantLog{ + { + Attributes: map[string]interface{}{ + "region": "region-test-2", + "anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}, + "duration": 1 * time.Second, + "int": 123, + "bool": true, + }, + Severity: zap.InfoLevel.String(), + Message: msg, + Timestamp: internal.MatchAnyUnixMilli, + TraceID: txnMetadata.TraceID, + SpanID: txnMetadata.SpanID, + }, + }) +} + func TestTransactionLoggerNilTxn(t *testing.T) { app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn, newrelic.ConfigAppLogDecoratingEnabled(true), @@ -264,20 +317,22 @@ func BenchmarkFieldConversion(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - convertField([]zap.Field{zap.String("test-key", "test-val")}) + convertFieldWithMapEncoder([]zap.Field{ + zap.String("test-key", "test-val"), + zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), + }) } } func BenchmarkFieldUnmarshalling(b *testing.B) { b.ResetTimer() b.ReportAllocs() - fields := []zap.Field{zap.String("test-key", "test-val")} for i := 0; i < b.N; i++ { - attributes := make(map[string]interface{}) - for _, field := range fields { - jsonBytes, _ := json.Marshal(field.Interface) - attributes[field.Key] = jsonBytes - } + convertFieldsAtHarvestTime([]zap.Field{ + zap.String("test-key", "test-val"), + zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}), + }) + } } diff --git a/v3/newrelic/config.go b/v3/newrelic/config.go index d0461ca1c..42c79f809 100644 --- a/v3/newrelic/config.go +++ b/v3/newrelic/config.go @@ -582,6 +582,10 @@ type ApplicationLogging struct { // Toggles whether the agent enriches local logs printed to console so they can be sent to new relic for ingestion Enabled bool } + ZapLogger struct { + // Toggles whether zap logger field attributes are frontloaded with the zapcore.NewMapObjectEncoder or marshalled at harvest time + AttributesFrontloaded bool + } } // AttributeDestinationConfig controls the attributes sent to each destination. @@ -654,7 +658,7 @@ func defaultConfig() Config { c.ApplicationLogging.Forwarding.MaxSamplesStored = internal.MaxLogEvents c.ApplicationLogging.Metrics.Enabled = true c.ApplicationLogging.LocalDecorating.Enabled = false - + c.ApplicationLogging.ZapLogger.AttributesFrontloaded = true c.BrowserMonitoring.Enabled = true // browser monitoring attributes are disabled by default c.BrowserMonitoring.Attributes.Enabled = false diff --git a/v3/newrelic/config_options.go b/v3/newrelic/config_options.go index 082b46d83..8c3daf6f7 100644 --- a/v3/newrelic/config_options.go +++ b/v3/newrelic/config_options.go @@ -302,6 +302,13 @@ func ConfigInfoLogger(w io.Writer) ConfigOption { return ConfigLogger(NewLogger(w)) } +// ConfigZapAttributesEncoder controls whether the agent will frontload the zap logger field attributes with the zapcore.NewMapObjectEncoder or marshal at harvest time +func ConfigZapAttributesEncoder(enabled bool) ConfigOption { + return func(cfg *Config) { + cfg.ApplicationLogging.ZapLogger.AttributesFrontloaded = enabled + } +} + // ConfigModuleDependencyMetricsEnabled controls whether the agent collects and reports // the list of modules compiled into the instrumented application. func ConfigModuleDependencyMetricsEnabled(enabled bool) ConfigOption { From abd9719b606697b8ea36b0d0fb55fa3a4da78c54 Mon Sep 17 00:00:00 2001 From: mirackara Date: Fri, 26 Apr 2024 17:01:16 -0500 Subject: [PATCH 16/21] Fix Azure error handling and add AnySet method to vendors struct --- v3/internal/utilization/azure.go | 10 ++++++++-- v3/internal/utilization/utilization.go | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/v3/internal/utilization/azure.go b/v3/internal/utilization/azure.go index b2e1846be..57e733e32 100644 --- a/v3/internal/utilization/azure.go +++ b/v3/internal/utilization/azure.go @@ -28,7 +28,9 @@ func gatherAzure(util *Data, client *http.Client) error { if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running Azure about a timeout. - if _, ok := err.(unexpectedAzureErr); ok { + // If any of the other vendors have been detected, and we have an unauthorized error, we should not return the error + // If no vendors have been detected, we should return the error. + if _, ok := err.(unexpectedAzureErr); ok && !util.Vendors.AnySet() { return err } return nil @@ -59,10 +61,14 @@ func getAzure(client *http.Client) (*azure, error) { } defer response.Body.Close() - if response.StatusCode != 200 { + if response.StatusCode != 200 && response.StatusCode != 401 { return nil, unexpectedAzureErr{e: fmt.Errorf("response code %d", response.StatusCode)} } + if response.StatusCode == 401 { + return nil, unexpectedAzureErr{e: err} + } + data, err := ioutil.ReadAll(response.Body) if err != nil { return nil, unexpectedAzureErr{e: err} diff --git a/v3/internal/utilization/utilization.go b/v3/internal/utilization/utilization.go index ad2a4ea5d..e9b5085d6 100644 --- a/v3/internal/utilization/utilization.go +++ b/v3/internal/utilization/utilization.go @@ -3,7 +3,6 @@ // Package utilization implements the Utilization spec, available at // https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md -// package utilization import ( @@ -84,6 +83,9 @@ type vendors struct { Kubernetes *kubernetes `json:"kubernetes,omitempty"` } +func (v *vendors) AnySet() bool { + return v.AWS != nil || v.Azure != nil || v.GCP != nil || v.PCF != nil || v.Docker != nil || v.Kubernetes != nil +} func (v *vendors) isEmpty() bool { return nil == v || *v == vendors{} } From a884592ed50de6de2727e8b90dd6a2cea67bab7e Mon Sep 17 00:00:00 2001 From: mirackara Date: Fri, 26 Apr 2024 17:03:04 -0500 Subject: [PATCH 17/21] Refactor Azure error handling in gatherAzure function --- v3/internal/utilization/azure.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/v3/internal/utilization/azure.go b/v3/internal/utilization/azure.go index 57e733e32..5a747e2c1 100644 --- a/v3/internal/utilization/azure.go +++ b/v3/internal/utilization/azure.go @@ -28,7 +28,7 @@ func gatherAzure(util *Data, client *http.Client) error { if err != nil { // Only return the error here if it is unexpected to prevent // warning customers who aren't running Azure about a timeout. - // If any of the other vendors have been detected, and we have an unauthorized error, we should not return the error + // If any of the other vendors have already been detected and set, and we have an error, we should not return the error // If no vendors have been detected, we should return the error. if _, ok := err.(unexpectedAzureErr); ok && !util.Vendors.AnySet() { return err @@ -61,14 +61,10 @@ func getAzure(client *http.Client) (*azure, error) { } defer response.Body.Close() - if response.StatusCode != 200 && response.StatusCode != 401 { + if response.StatusCode != 200 { return nil, unexpectedAzureErr{e: fmt.Errorf("response code %d", response.StatusCode)} } - if response.StatusCode == 401 { - return nil, unexpectedAzureErr{e: err} - } - data, err := ioutil.ReadAll(response.Body) if err != nil { return nil, unexpectedAzureErr{e: err} From 2c9b0b0ec2ae0119d95a034d37a63e9c0d7c00c3 Mon Sep 17 00:00:00 2001 From: mirackara Date: Wed, 1 May 2024 11:29:21 -0500 Subject: [PATCH 18/21] Better comments --- v3/newrelic/config.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/v3/newrelic/config.go b/v3/newrelic/config.go index 42c79f809..b34537d68 100644 --- a/v3/newrelic/config.go +++ b/v3/newrelic/config.go @@ -582,6 +582,11 @@ type ApplicationLogging struct { // Toggles whether the agent enriches local logs printed to console so they can be sent to new relic for ingestion Enabled bool } + // We want to enable this when your app collects fewer logs, or if your app can afford to compile the json + // during log collection, slowing down the execution of the line of code that will write the log. If your + // application collects logs at a high frequency or volume, or it can not afford the slowdown of marshaling objects + // before sending them to new relic, we can marshal them asynchronously in the backend during harvests by setting + // this to false using ConfigZapAttributesEncoder(false). ZapLogger struct { // Toggles whether zap logger field attributes are frontloaded with the zapcore.NewMapObjectEncoder or marshalled at harvest time AttributesFrontloaded bool From 36fbaa8ee817c484cc298aa4bc0be38461cb11b0 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 1 May 2024 11:22:38 -0700 Subject: [PATCH 19/21] release 3.33.0 --- CHANGELOG.md | 18 ++++++++++++++++++ v3/newrelic/version.go | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5dc4621..2b84cf296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 3.33.0 +### Added +- Support for Zap Field Attributes +- Updated dependency on csec-go-agent in nrsecurityagent +### Fixed +- Fixed an issue where running containers on AWS would falsely flag Azure Utilization +- Fixed a typo with nrecho-v3 +- Changed nrslog example to use a context driven handler + +These changes increment the affected integration package version numbers to: +- nrsecurityagent v1.3.1 +- nrecho-v3 v1.1.1 +- logcontext-v2/nrslog v1.2.0 +- logcontext-v2/nrzap v1.2.0 + +### 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.32.0 ### Added * Updates to support for the New Relic security agent to report API endpoints. diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index 53df4c5bd..7630ad141 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.32.0" + Version = "3.33.0" ) var ( From 98f068b6d6bb293069deb139cf1e8676bf5b6dea Mon Sep 17 00:00:00 2001 From: mirackara Date: Wed, 1 May 2024 13:40:36 -0500 Subject: [PATCH 20/21] Add ZapLogger attributes frontloading to config_test.go --- v3/newrelic/config_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/v3/newrelic/config_test.go b/v3/newrelic/config_test.go index 9400a4e20..6ce06a6e6 100644 --- a/v3/newrelic/config_test.go +++ b/v3/newrelic/config_test.go @@ -151,6 +151,9 @@ func TestCopyConfigReferenceFieldsPresent(t *testing.T) { }, "Metrics": { "Enabled": true + }, + "ZapLogger": { + "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":["2"],"Include":["1"]}, @@ -356,6 +359,9 @@ func TestCopyConfigReferenceFieldsAbsent(t *testing.T) { }, "Metrics": { "Enabled": true + }, + "ZapLogger": { + "AttributesFrontloaded": true } }, "Attributes":{"Enabled":true,"Exclude":null,"Include":null}, From 58afbd4a2e883d961b2f352f14c22403bc918726 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Wed, 1 May 2024 11:43:11 -0700 Subject: [PATCH 21/21] fix example --- v3/integrations/logcontext-v2/nrslog/example/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v3/integrations/logcontext-v2/nrslog/example/main.go b/v3/integrations/logcontext-v2/nrslog/example/main.go index 9ee09ff8e..2544fb3b6 100644 --- a/v3/integrations/logcontext-v2/nrslog/example/main.go +++ b/v3/integrations/logcontext-v2/nrslog/example/main.go @@ -1,10 +1,10 @@ package main import ( + "context" "log/slog" "os" "time" - "context" "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrslog" "github.com/newrelic/go-agent/v3/newrelic" @@ -26,12 +26,12 @@ func main() { txn := app.StartTransaction("example transaction") ctx := newrelic.NewContext(context.Background(), txn) - - txnLogger.InfoContext(ctx, "I am a log inside a transaction") + + log.InfoContext(ctx, "I am a log inside a transaction") // pretend to do some work time.Sleep(500 * time.Millisecond) - txnLogger.Warn("Uh oh, something important happened!") + log.Warn("Uh oh, something important happened!") txn.End() log.Info("All Done!")