Skip to content

Commit

Permalink
Merge pull request #908 from newrelic/develop
Browse files Browse the repository at this point in the history
Release 3.33.0
  • Loading branch information
nr-swilloughby committed May 1, 2024
2 parents 0f93238 + 668533d commit 424d175
Show file tree
Hide file tree
Showing 20 changed files with 519 additions and 76 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 5 additions & 3 deletions v3/integrations/logcontext-v2/nrslog/example/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"log/slog"
"os"
"time"
Expand All @@ -24,12 +25,13 @@ 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)

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!")
Expand Down
23 changes: 20 additions & 3 deletions v3/integrations/logcontext-v2/nrzap/example/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"os"
"time"

Expand All @@ -14,7 +15,10 @@ func main() {
app, err := newrelic.NewApplication(
newrelic.ConfigAppName("nrzerolog example"),
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)
Expand All @@ -29,16 +33,29 @@ 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)
if err != nil && err != nrzap.ErrNilTxn {
panic(err)
}

txnLogger := zap.New(txnCore)
txnLogger.Info("this is a transaction log message")
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}),
)

err = errors.New("OW! an error occurred")
txnLogger.Error("this is an error log message", zap.Error(err))

txn.End()

Expand Down
75 changes: 72 additions & 3 deletions v3/integrations/logcontext-v2/nrzap/nrzap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package nrzap

import (
"errors"
"math"
"time"

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

// Helper function that converts zap fields to a map of string interface
func convertFieldWithMapEncoder(fields []zap.Field) map[string]interface{} {
attributes := make(map[string]interface{})
for _, field := range fields {
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
}

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,
Timestamp: entry.Time.UnixMilli(),
Severity: entry.Level.String(),
Message: entry.Message,
Attributes: attributes,
}

if nr.txn != nil {
Expand Down
143 changes: 143 additions & 0 deletions v3/integrations/logcontext-v2/nrzap/nrzap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"testing"
"time"

"github.com/newrelic/go-agent/v3/internal"
"github.com/newrelic/go-agent/v3/internal/integrationsupport"
Expand Down Expand Up @@ -131,6 +132,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,
Expand All @@ -140,6 +144,110 @@ func TestTransactionLogger(t *testing.T) {
})
}

func TestTransactionLoggerWithFields(t *testing.T) {
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
newrelic.ConfigAppLogDecoratingEnabled(true),
newrelic.ConfigAppLogForwardingEnabled(true),
newrelic.ConfigZapAttributesEncoder(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 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),
Expand Down Expand Up @@ -204,6 +312,41 @@ func BenchmarkZapBaseline(b *testing.B) {
}
}

func BenchmarkFieldConversion(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()

for i := 0; i < b.N; i++ {
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()
for i := 0; i < b.N; i++ {
convertFieldsAtHarvestTime([]zap.Field{
zap.String("test-key", "test-val"),
zap.Any("test-key", map[string]interface{}{"pi": 3.14, "duration": 2 * time.Second}),
})

}
}

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),
Expand Down
18 changes: 17 additions & 1 deletion v3/integrations/nrecho-v3/nrecho.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
}
}
}

0 comments on commit 424d175

Please sign in to comment.