Skip to content

Commit 424d175

Browse files
Merge pull request #908 from newrelic/develop
Release 3.33.0
2 parents 0f93238 + 668533d commit 424d175

20 files changed

+519
-76
lines changed

CHANGELOG.md

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
## 3.33.0
2+
### Added
3+
- Support for Zap Field Attributes
4+
- Updated dependency on csec-go-agent in nrsecurityagent
5+
### Fixed
6+
- Fixed an issue where running containers on AWS would falsely flag Azure Utilization
7+
- Fixed a typo with nrecho-v3
8+
- Changed nrslog example to use a context driven handler
9+
10+
These changes increment the affected integration package version numbers to:
11+
- nrsecurityagent v1.3.1
12+
- nrecho-v3 v1.1.1
13+
- logcontext-v2/nrslog v1.2.0
14+
- logcontext-v2/nrzap v1.2.0
15+
16+
### Support statement
17+
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.
18+
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.
119
## 3.32.0
220
### Added
321
* Updates to support for the New Relic security agent to report API endpoints.

v3/integrations/logcontext-v2/nrslog/example/main.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"log/slog"
56
"os"
67
"time"
@@ -24,12 +25,13 @@ func main() {
2425
log.Info("I am a log message")
2526

2627
txn := app.StartTransaction("example transaction")
27-
txnLogger := nrslog.WithTransaction(txn, log)
28-
txnLogger.Info("I am a log inside a transaction")
28+
ctx := newrelic.NewContext(context.Background(), txn)
29+
30+
log.InfoContext(ctx, "I am a log inside a transaction")
2931

3032
// pretend to do some work
3133
time.Sleep(500 * time.Millisecond)
32-
txnLogger.Warn("Uh oh, something important happened!")
34+
log.Warn("Uh oh, something important happened!")
3335
txn.End()
3436

3537
log.Info("All Done!")

v3/integrations/logcontext-v2/nrzap/example/main.go

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"os"
56
"time"
67

@@ -14,7 +15,10 @@ func main() {
1415
app, err := newrelic.NewApplication(
1516
newrelic.ConfigAppName("nrzerolog example"),
1617
newrelic.ConfigInfoLogger(os.Stdout),
18+
newrelic.ConfigDebugLogger(os.Stdout),
1719
newrelic.ConfigFromEnvironment(),
20+
// This is enabled by default. if disabled, the attributes will be marshalled at harvest time.
21+
newrelic.ConfigZapAttributesEncoder(false),
1822
)
1923
if err != nil {
2024
panic(err)
@@ -29,16 +33,29 @@ func main() {
2933
}
3034

3135
backgroundLogger := zap.New(backgroundCore)
32-
backgroundLogger.Info("this is a background log message")
36+
backgroundLogger.Info("this is a background log message with fields test", zap.Any("foo", 3.14))
3337

3438
txn := app.StartTransaction("nrzap example transaction")
3539
txnCore, err := nrzap.WrapTransactionCore(core, txn)
3640
if err != nil && err != nrzap.ErrNilTxn {
3741
panic(err)
3842
}
39-
4043
txnLogger := zap.New(txnCore)
41-
txnLogger.Info("this is a transaction log message")
44+
txnLogger.Info("this is a transaction log message with custom fields",
45+
zap.String("zapstring", "region-test-2"),
46+
zap.Int("zapint", 123),
47+
zap.Duration("zapduration", 200*time.Millisecond),
48+
zap.Bool("zapbool", true),
49+
zap.Object("zapobject", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
50+
enc.AddString("foo", "bar")
51+
return nil
52+
})),
53+
54+
zap.Any("zapmap", map[string]any{"pi": 3.14, "duration": 2 * time.Second}),
55+
)
56+
57+
err = errors.New("OW! an error occurred")
58+
txnLogger.Error("this is an error log message", zap.Error(err))
4259

4360
txn.End()
4461

v3/integrations/logcontext-v2/nrzap/nrzap.go

+72-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package nrzap
22

33
import (
44
"errors"
5+
"math"
6+
"time"
57

68
"github.com/newrelic/go-agent/v3/internal"
79
"github.com/newrelic/go-agent/v3/newrelic"
@@ -24,12 +26,79 @@ type newrelicApplicationState struct {
2426
txn *newrelic.Transaction
2527
}
2628

29+
// Helper function that converts zap fields to a map of string interface
30+
func convertFieldWithMapEncoder(fields []zap.Field) map[string]interface{} {
31+
attributes := make(map[string]interface{})
32+
for _, field := range fields {
33+
enc := zapcore.NewMapObjectEncoder()
34+
field.AddTo(enc)
35+
for key, value := range enc.Fields {
36+
// Format time.Duration values as strings
37+
if durationVal, ok := value.(time.Duration); ok {
38+
attributes[key] = durationVal.String()
39+
} else {
40+
attributes[key] = value
41+
}
42+
}
43+
}
44+
return attributes
45+
}
46+
47+
func convertFieldsAtHarvestTime(fields []zap.Field) map[string]interface{} {
48+
attributes := make(map[string]interface{})
49+
for _, field := range fields {
50+
if field.Interface != nil {
51+
52+
// Handles ErrorType fields
53+
if field.Type == zapcore.ErrorType {
54+
attributes[field.Key] = field.Interface.(error).Error()
55+
} else {
56+
// Handles all interface types
57+
attributes[field.Key] = field.Interface
58+
}
59+
60+
} else if field.String != "" { // Check if the field is a string and doesn't contain an interface
61+
attributes[field.Key] = field.String
62+
63+
} else {
64+
// Float Types
65+
if field.Type == zapcore.Float32Type {
66+
attributes[field.Key] = math.Float32frombits(uint32(field.Integer))
67+
continue
68+
} else if field.Type == zapcore.Float64Type {
69+
attributes[field.Key] = math.Float64frombits(uint64(field.Integer))
70+
continue
71+
}
72+
// Bool Type
73+
if field.Type == zapcore.BoolType {
74+
field.Interface = field.Integer == 1
75+
attributes[field.Key] = field.Interface
76+
} else {
77+
// Integer Types
78+
attributes[field.Key] = field.Integer
79+
80+
}
81+
}
82+
}
83+
return attributes
84+
}
85+
2786
// internal handler function to manage writing a log to the new relic application
2887
func (nr *newrelicApplicationState) recordLog(entry zapcore.Entry, fields []zap.Field) {
88+
attributes := map[string]interface{}{}
89+
cfg, _ := nr.app.Config()
90+
91+
// Check if the attributes should be frontloaded or marshalled at harvest time
92+
if cfg.ApplicationLogging.ZapLogger.AttributesFrontloaded {
93+
attributes = convertFieldWithMapEncoder(fields)
94+
} else {
95+
attributes = convertFieldsAtHarvestTime(fields)
96+
}
2997
data := newrelic.LogData{
30-
Timestamp: entry.Time.UnixMilli(),
31-
Severity: entry.Level.String(),
32-
Message: entry.Message,
98+
Timestamp: entry.Time.UnixMilli(),
99+
Severity: entry.Level.String(),
100+
Message: entry.Message,
101+
Attributes: attributes,
33102
}
34103

35104
if nr.txn != nil {

v3/integrations/logcontext-v2/nrzap/nrzap_test.go

+143
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"testing"
8+
"time"
89

910
"github.com/newrelic/go-agent/v3/internal"
1011
"github.com/newrelic/go-agent/v3/internal/integrationsupport"
@@ -131,6 +132,9 @@ func TestTransactionLogger(t *testing.T) {
131132

132133
app.ExpectLogEvents(t, []internal.WantLog{
133134
{
135+
Attributes: map[string]interface{}{
136+
"test-key": "test-val",
137+
},
134138
Severity: zap.ErrorLevel.String(),
135139
Message: msg,
136140
Timestamp: internal.MatchAnyUnixMilli,
@@ -140,6 +144,110 @@ func TestTransactionLogger(t *testing.T) {
140144
})
141145
}
142146

147+
func TestTransactionLoggerWithFields(t *testing.T) {
148+
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
149+
newrelic.ConfigAppLogDecoratingEnabled(true),
150+
newrelic.ConfigAppLogForwardingEnabled(true),
151+
newrelic.ConfigZapAttributesEncoder(true),
152+
)
153+
154+
txn := app.StartTransaction("test transaction")
155+
txnMetadata := txn.GetTraceMetadata()
156+
157+
core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel)
158+
wrappedCore, err := WrapTransactionCore(core, txn)
159+
if err != nil {
160+
t.Error(err)
161+
}
162+
163+
logger := zap.New(wrappedCore)
164+
165+
msg := "this is a test info message"
166+
167+
// for background logging:
168+
logger.Info(msg,
169+
zap.String("region", "region-test-2"),
170+
zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}),
171+
zap.Duration("duration", 1*time.Second),
172+
zap.Int("int", 123),
173+
zap.Bool("bool", true),
174+
)
175+
176+
logger.Sync()
177+
178+
// ensure txn gets written to an event and logs get released
179+
txn.End()
180+
181+
app.ExpectLogEvents(t, []internal.WantLog{
182+
{
183+
Attributes: map[string]interface{}{
184+
"region": "region-test-2",
185+
"anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second},
186+
"duration": 1 * time.Second,
187+
"int": 123,
188+
"bool": true,
189+
},
190+
Severity: zap.InfoLevel.String(),
191+
Message: msg,
192+
Timestamp: internal.MatchAnyUnixMilli,
193+
TraceID: txnMetadata.TraceID,
194+
SpanID: txnMetadata.SpanID,
195+
},
196+
})
197+
}
198+
199+
func TestTransactionLoggerWithFieldsAtHarvestTime(t *testing.T) {
200+
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
201+
newrelic.ConfigAppLogDecoratingEnabled(true),
202+
newrelic.ConfigAppLogForwardingEnabled(true),
203+
newrelic.ConfigZapAttributesEncoder(false),
204+
)
205+
206+
txn := app.StartTransaction("test transaction")
207+
txnMetadata := txn.GetTraceMetadata()
208+
209+
core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), os.Stdout, zap.InfoLevel)
210+
wrappedCore, err := WrapTransactionCore(core, txn)
211+
if err != nil {
212+
t.Error(err)
213+
}
214+
215+
logger := zap.New(wrappedCore)
216+
217+
msg := "this is a test info message"
218+
219+
// for background logging:
220+
logger.Info(msg,
221+
zap.String("region", "region-test-2"),
222+
zap.Any("anyValue", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}),
223+
zap.Duration("duration", 1*time.Second),
224+
zap.Int("int", 123),
225+
zap.Bool("bool", true),
226+
)
227+
228+
logger.Sync()
229+
230+
// ensure txn gets written to an event and logs get released
231+
txn.End()
232+
233+
app.ExpectLogEvents(t, []internal.WantLog{
234+
{
235+
Attributes: map[string]interface{}{
236+
"region": "region-test-2",
237+
"anyValue": map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second},
238+
"duration": 1 * time.Second,
239+
"int": 123,
240+
"bool": true,
241+
},
242+
Severity: zap.InfoLevel.String(),
243+
Message: msg,
244+
Timestamp: internal.MatchAnyUnixMilli,
245+
TraceID: txnMetadata.TraceID,
246+
SpanID: txnMetadata.SpanID,
247+
},
248+
})
249+
}
250+
143251
func TestTransactionLoggerNilTxn(t *testing.T) {
144252
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
145253
newrelic.ConfigAppLogDecoratingEnabled(true),
@@ -204,6 +312,41 @@ func BenchmarkZapBaseline(b *testing.B) {
204312
}
205313
}
206314

315+
func BenchmarkFieldConversion(b *testing.B) {
316+
b.ResetTimer()
317+
b.ReportAllocs()
318+
319+
for i := 0; i < b.N; i++ {
320+
convertFieldWithMapEncoder([]zap.Field{
321+
zap.String("test-key", "test-val"),
322+
zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}),
323+
})
324+
}
325+
}
326+
327+
func BenchmarkFieldUnmarshalling(b *testing.B) {
328+
b.ResetTimer()
329+
b.ReportAllocs()
330+
for i := 0; i < b.N; i++ {
331+
convertFieldsAtHarvestTime([]zap.Field{
332+
zap.String("test-key", "test-val"),
333+
zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}),
334+
})
335+
336+
}
337+
}
338+
339+
func BenchmarkZapWithAttribute(b *testing.B) {
340+
core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(io.Discard), zap.InfoLevel)
341+
logger := zap.New(core)
342+
b.ResetTimer()
343+
b.ReportAllocs()
344+
345+
for i := 0; i < b.N; i++ {
346+
logger.Info("this is a test message", zap.Any("test-key", "test-val"))
347+
}
348+
}
349+
207350
func BenchmarkZapWrappedCore(b *testing.B) {
208351
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
209352
newrelic.ConfigAppLogDecoratingEnabled(true),

v3/integrations/nrecho-v3/nrecho.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ func handlerPointer(handler echo.HandlerFunc) uintptr {
3535
return reflect.ValueOf(handler).Pointer()
3636
}
3737

38+
func handlerName(router interface{}) string {
39+
val := reflect.ValueOf(router)
40+
if val.Kind() == reflect.Ptr { // for echo version v3.2.2+
41+
val = val.Elem()
42+
} else {
43+
val = reflect.ValueOf(&router).Elem().Elem()
44+
}
45+
if name := val.FieldByName("Name"); name.IsValid() { // for echo version v3.2.2+
46+
return name.String()
47+
} else if handler := val.FieldByName("Handler"); handler.IsValid() {
48+
return handler.String()
49+
} else {
50+
return ""
51+
}
52+
}
53+
3854
func transactionName(c echo.Context) string {
3955
ptr := handlerPointer(c.Handler())
4056
if ptr == handlerPointer(echo.NotFoundHandler) {
@@ -108,7 +124,7 @@ func WrapRouter(engine *echo.Echo) {
108124
if engine != nil && newrelic.IsSecurityAgentPresent() {
109125
router := engine.Routes()
110126
for _, r := range router {
111-
newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, r.Handler)
127+
newrelic.GetSecurityAgentInterface().SendEvent("API_END_POINTS", r.Path, r.Method, handlerName(r))
112128
}
113129
}
114130
}

0 commit comments

Comments
 (0)