diff --git a/v3/integrations/logcontext-v2/nrslog/handler_test.go b/v3/integrations/logcontext-v2/nrslog/handler_test.go index 8f21b6fce..726b9fa9f 100644 --- a/v3/integrations/logcontext-v2/nrslog/handler_test.go +++ b/v3/integrations/logcontext-v2/nrslog/handler_test.go @@ -294,7 +294,7 @@ func TestWithAttributes(t *testing.T) { txn := app.StartTransaction("hi") txnLog := WithTransaction(txn, log) - txnLog.Info(message) + txnLog.Info(message, slog.Duration("duration", 3*time.Second)) data := txn.GetLinkingMetadata() txn.End() @@ -309,11 +309,13 @@ func TestWithAttributes(t *testing.T) { log = log.With(additionalAttrs) log.Info(message) + expectInt := int64(1) + app.ExpectLogEvents(t, []internal.WantLog{ { Attributes: map[string]interface{}{ "string key": "val", - "int key": 1, + "int key": expectInt, }, Severity: slog.LevelInfo.String(), Message: message, @@ -322,7 +324,8 @@ func TestWithAttributes(t *testing.T) { { Attributes: map[string]interface{}{ "string key": "val", - "int key": 1, + "int key": expectInt, + "duration": time.Duration(3 * time.Second), }, Severity: slog.LevelInfo.String(), Message: message, @@ -333,7 +336,7 @@ func TestWithAttributes(t *testing.T) { { Attributes: map[string]interface{}{ "string key": "val", - "int key": 1, + "int key": expectInt, "group1.additional": "attr", }, Severity: slog.LevelInfo.String(), @@ -343,7 +346,7 @@ func TestWithAttributes(t *testing.T) { { Attributes: map[string]interface{}{ "string key": "val", - "int key": 1, + "int key": expectInt, "group1.group2.additional": "attr", }, Severity: slog.LevelInfo.String(), @@ -353,7 +356,7 @@ func TestWithAttributes(t *testing.T) { { Attributes: map[string]interface{}{ "string key": "val", - "int key": 1, + "int key": expectInt, "group1.group2.additional": "attr", }, Severity: slog.LevelInfo.String(), @@ -417,7 +420,7 @@ func TestWithAttributesFromContext(t *testing.T) { Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "foo": "bar", - "answer": 42, + "answer": int64(42), }, TraceID: metadata.TraceID, SpanID: metadata.SpanID, @@ -428,7 +431,7 @@ func TestWithAttributesFromContext(t *testing.T) { Timestamp: internal.MatchAnyUnixMilli, Attributes: map[string]interface{}{ "group1.foo": "bar", - "group1.answer": 42, + "group1.answer": int64(42), }, }, }) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index eb1ef14b5..c5d613d8c 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -14,6 +14,7 @@ import ( "sort" "strconv" "strings" + "time" ) const ( @@ -507,6 +508,16 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { w.floatField(key, float64(v)) case float64: w.floatField(key, v) + case time.Time: + writeAttributeValueJSON(w, key, v.String()) + case time.Duration: + writeAttributeValueJSON(w, key, v.String()) + case time.Weekday: + writeAttributeValueJSON(w, key, v.String()) + case *time.Location: + writeAttributeValueJSON(w, key, v.String()) + case time.Month: + writeAttributeValueJSON(w, key, v.String()) default: // attempt to construct a JSON string kind := reflect.ValueOf(v).Kind() diff --git a/v3/newrelic/attributes_test.go b/v3/newrelic/attributes_test.go index 466c53729..f9cf4e88e 100644 --- a/v3/newrelic/attributes_test.go +++ b/v3/newrelic/attributes_test.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/newrelic/go-agent/v3/internal/crossagent" ) @@ -170,6 +171,11 @@ func TestWriteAttributeValueJSON(t *testing.T) { writeAttributeValueJSON(&w, "a", int(-5)) writeAttributeValueJSON(&w, "a", float32(1.5)) writeAttributeValueJSON(&w, "a", float64(4.56)) + writeAttributeValueJSON(&w, "duration", time.Duration(7)) + writeAttributeValueJSON(&w, "time", time.Time{}.AddDate(2000, 1, 1)) + writeAttributeValueJSON(&w, "weekday", time.Wednesday) + writeAttributeValueJSON(&w, "month", time.April) + writeAttributeValueJSON(&w, "location", time.FixedZone("LOC", 0)) buf.WriteByte('}') expect := compactJSONString(`{ @@ -188,7 +194,12 @@ func TestWriteAttributeValueJSON(t *testing.T) { "a":-4, "a":-5, "a":1.5, - "a":4.56 + "a":4.56, + "duration":"7ns", + "time":"2001-02-02 00:00:00 +0000 UTC", + "weekday":"Wednesday", + "month":"April", + "location":"LOC" }`) js := buf.String() if js != expect { diff --git a/v3/newrelic/expect_implementation.go b/v3/newrelic/expect_implementation.go index 122d588ff..869160c68 100644 --- a/v3/newrelic/expect_implementation.go +++ b/v3/newrelic/expect_implementation.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "reflect" "time" "github.com/newrelic/go-agent/v3/internal" @@ -251,73 +252,33 @@ func expectLogEvent(v internal.Validator, actual logEvent, want internal.WantLog } if actual.attributes != nil && want.Attributes != nil { - for k, val := range want.Attributes { - actualVal, actualOk := actual.attributes[k] - if !actualOk { - v.Error(fmt.Sprintf("expected log attribute for key %v is missing", k)) - return + if len(actual.attributes) != len(want.Attributes) { + skippedAttributes := []string{} + for k := range actual.attributes { + if _, ok := want.Attributes[k]; !ok { + skippedAttributes = append(skippedAttributes, fmt.Sprintf("an expected attribute is missing: {\"%s\":%v}", k, actual.attributes[k])) + } } - - // 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 + for k := range want.Attributes { + if _, ok := actual.attributes[k]; !ok { + skippedAttributes = append(skippedAttributes, fmt.Sprintf("unexpected attribute: {\"%s\":%v}", k, want.Attributes[k])) } } - } - } - -} - -// 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 - } - for k, v := range a { - if bv, ok := b[k]; !ok { - return false + v.Error(fmt.Sprintf("unexpected number of log attributes: got %d, want %d; %s", len(actual.attributes), len(want.Attributes), skippedAttributes)) + return } 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 - } - // 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 + for k, wantVal := range want.Attributes { + actualVal := actual.attributes[k] + ok := reflect.DeepEqual(wantVal, actualVal) + if !ok { + v.Error(fmt.Sprintf("unexpected log attribute for key \"%s\": got value: %+v, type: %T; want value: %+v, type: %T", k, actualVal, actualVal, wantVal, wantVal)) + return } - default: - return false } } } - return true } + func expectEvent(v internal.Validator, e json.Marshaler, expect internal.WantEvent) { js, err := e.MarshalJSON() if nil != err {