Skip to content

Commit

Permalink
Add Zap logger field attribute encoding option
Browse files Browse the repository at this point in the history
  • Loading branch information
mirackara committed Apr 24, 2024
1 parent 4f155e0 commit 4dbcccb
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 11 deletions.
2 changes: 2 additions & 0 deletions v3/integrations/logcontext-v2/nrzap/example/main.go
Expand Up @@ -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)
Expand Down
53 changes: 51 additions & 2 deletions v3/integrations/logcontext-v2/nrzap/nrzap.go
Expand Up @@ -2,6 +2,7 @@ package nrzap

import (
"errors"
"math"
"time"

"github.com/newrelic/go-agent/v3/internal"
Expand All @@ -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()
Expand All @@ -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 {
Expand Down
71 changes: 63 additions & 8 deletions v3/integrations/logcontext-v2/nrzap/nrzap_test.go
@@ -1,7 +1,6 @@
package nrzap

import (
"encoding/json"
"errors"
"io"
"os"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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}),
})

}
}

Expand Down
6 changes: 5 additions & 1 deletion v3/newrelic/config.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions v3/newrelic/config_options.go
Expand Up @@ -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 {
Expand Down

0 comments on commit 4dbcccb

Please sign in to comment.