Skip to content

Commit 877e3bc

Browse files
author
Emilio Garcia
committed
28% faster log enrichment
1 parent 2f9dc00 commit 877e3bc

File tree

5 files changed

+185
-46
lines changed

5 files changed

+185
-46
lines changed

v3/integrations/logcontext-v2/nrslog/go.mod

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,4 @@ go 1.21
44

55
require github.com/newrelic/go-agent/v3 v3.36.0
66

7-
require (
8-
golang.org/x/net v0.25.0 // indirect
9-
golang.org/x/sys v0.20.0 // indirect
10-
golang.org/x/text v0.15.0 // indirect
11-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
12-
google.golang.org/grpc v1.65.0 // indirect
13-
google.golang.org/protobuf v1.34.2 // indirect
14-
)
15-
167
replace github.com/newrelic/go-agent/v3 => ../../..

v3/integrations/logcontext-v2/nrslog/handler.go

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package nrslog
22

33
import (
4-
"bytes"
54
"context"
65
"log/slog"
76
"time"
@@ -36,12 +35,19 @@ func (h *NRHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
3635

3736
// Handle handles the Record.
3837
// It will only be called when Enabled returns true.
39-
// The Context argument is as for Enabled.
40-
// It is present solely to provide Handlers access to the context's values.
41-
// Canceling the context should not affect record processing.
42-
// (Among other things, log messages may be necessary to debug a
43-
// cancellation-related problem.)
4438
func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error {
39+
nrTxn := h.txn
40+
41+
ctxTxn := newrelic.FromContext(ctx)
42+
if ctxTxn != nil {
43+
nrTxn = ctxTxn
44+
}
45+
46+
// if no app or txn, do nothing
47+
if h.app == nil && nrTxn == nil {
48+
return h.handler.Handle(ctx, record)
49+
}
50+
4551
attrs := map[string]interface{}{}
4652

4753
record.Attrs(func(attr slog.Attr) bool {
@@ -67,32 +73,15 @@ func (h *NRHandler) Handle(ctx context.Context, record slog.Record) error {
6773
Attributes: attrs,
6874
}
6975

70-
nrTxn := h.txn
71-
72-
ctxTxn := newrelic.FromContext(ctx)
73-
if ctxTxn != nil {
74-
nrTxn = ctxTxn
75-
}
76-
77-
var enricherOpts newrelic.EnricherOption
7876
if nrTxn != nil {
7977
nrTxn.RecordLog(data)
80-
enricherOpts = newrelic.FromTxn(nrTxn)
78+
enrichRecordTxn(nrTxn, &record)
8179
} else {
8280
h.app.RecordLog(data)
83-
enricherOpts = newrelic.FromApp(h.app)
84-
}
85-
86-
// add linking metadata as an attribute
87-
// without disrupting normal usage of the handler
88-
nrLinking := bytes.NewBuffer([]byte{})
89-
err := newrelic.EnrichLog(nrLinking, enricherOpts)
90-
if err == nil {
91-
record.AddAttrs(slog.String("newrelic", nrLinking.String()))
81+
enrichRecord(h.app, &record)
9282
}
9383

94-
err = h.handler.Handle(ctx, record)
95-
return err
84+
return h.handler.Handle(ctx, record)
9685
}
9786

9887
// WithAttrs returns a new Handler whose attributes consist of

v3/integrations/logcontext-v2/nrslog/handler_test.go

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ func TestHandler(t *testing.T) {
4242
})
4343
}
4444

45+
func TestHandlerNilApp(t *testing.T) {
46+
out := bytes.NewBuffer([]byte{})
47+
logger := New(nil, slog.NewTextHandler(out, &slog.HandlerOptions{}))
48+
message := "Hello World!"
49+
logger.Info(message)
50+
51+
logStr := out.String()
52+
if strings.Contains(logStr, nrlinking) {
53+
t.Errorf(" %s should not contain %s", logStr, nrlinking)
54+
}
55+
if len(logStr) == 0 {
56+
t.Errorf("log string should not be empty")
57+
}
58+
}
59+
4560
func TestJSONHandler(t *testing.T) {
4661
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
4762
newrelic.ConfigAppLogDecoratingEnabled(true),
@@ -88,12 +103,11 @@ func TestHandlerTransactions(t *testing.T) {
88103
log.Debug(backgroundMsg)
89104
txn.End()
90105

91-
/*
92-
logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{
93-
EntityGUID: integrationsupport.TestEntityGUID,
94-
Hostname: host,
95-
EntityName: integrationsupport.SampleAppName,
96-
}) */
106+
logcontext.ValidateDecoratedOutput(t, out, &logcontext.DecorationExpect{
107+
EntityGUID: integrationsupport.TestEntityGUID,
108+
Hostname: host,
109+
EntityName: integrationsupport.SampleAppName,
110+
})
97111

98112
app.ExpectLogEvents(t, []internal.WantLog{
99113
{
@@ -361,11 +375,57 @@ func TestTransactionFromContextHandler(t *testing.T) {
361375
})
362376
}
363377

364-
// shockingly cheap
365-
func BenchmarkGetContextValue(b *testing.B) {
366-
var a *bytes.Buffer
378+
func BenchmarkEnrichLog(b *testing.B) {
379+
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
380+
newrelic.ConfigAppLogDecoratingEnabled(true),
381+
newrelic.ConfigAppLogForwardingEnabled(true),
382+
)
383+
txn := app.Application.StartTransaction("my txn")
384+
defer txn.End()
385+
record := slog.Record{}
386+
387+
b.ResetTimer()
388+
b.ReportAllocs()
389+
390+
for i := 0; i < b.N; i++ {
391+
nrLinking := bytes.NewBuffer([]byte{})
392+
err := newrelic.EnrichLog(nrLinking, newrelic.FromTxn(txn))
393+
if err == nil {
394+
record.AddAttrs(slog.String("newrelic", nrLinking.String()))
395+
}
396+
}
397+
}
398+
399+
func BenchmarkLinkingStringEnrichment(b *testing.B) {
400+
app := integrationsupport.NewTestApp(integrationsupport.SampleEverythingReplyFn,
401+
newrelic.ConfigAppLogDecoratingEnabled(true),
402+
newrelic.ConfigAppLogForwardingEnabled(true),
403+
)
404+
txn := app.Application.StartTransaction("my txn")
405+
defer txn.End()
406+
record := slog.Record{}
407+
408+
b.ResetTimer()
409+
b.ReportAllocs()
410+
411+
for i := 0; i < b.N; i++ {
412+
enrichRecordTxn(txn, &record)
413+
}
414+
}
415+
416+
func BenchmarkStringBuilder(b *testing.B) {
417+
md := newrelic.LinkingMetadata{
418+
EntityGUID: "entityGUID",
419+
Hostname: "hostname",
420+
TraceID: "traceID",
421+
SpanID: "spanID",
422+
EntityName: "entityName",
423+
}
424+
425+
b.ResetTimer()
426+
b.ReportAllocs()
427+
367428
for i := 0; i < b.N; i++ {
368-
a = bytes.NewBuffer([]byte{})
429+
nrLinkingString(md)
369430
}
370-
a.Reset()
371431
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package nrslog
2+
3+
import (
4+
"log/slog"
5+
"strings"
6+
7+
"github.com/newrelic/go-agent/v3/newrelic"
8+
)
9+
10+
const (
11+
nrlinking = "NR-LINKING"
12+
key = "newrelic"
13+
)
14+
15+
func enrichRecord(app *newrelic.Application, record *slog.Record) {
16+
if !shouldEnrichLog(app) {
17+
return
18+
}
19+
20+
str := nrLinkingString(app.GetLinkingMetadata())
21+
if str == "" {
22+
return
23+
}
24+
25+
record.AddAttrs(slog.String(key, str))
26+
}
27+
28+
func enrichRecordTxn(txn *newrelic.Transaction, record *slog.Record) {
29+
if !shouldEnrichLog(txn.Application()) {
30+
return
31+
}
32+
33+
str := nrLinkingString(txn.GetLinkingMetadata())
34+
if str == "" {
35+
return
36+
}
37+
38+
record.AddAttrs(slog.String(key, str))
39+
}
40+
41+
func shouldEnrichLog(app *newrelic.Application) bool {
42+
config, ok := app.Config()
43+
if !ok {
44+
return false
45+
}
46+
47+
return config.ApplicationLogging.Enabled && config.ApplicationLogging.LocalDecorating.Enabled
48+
}
49+
50+
// nrLinkingString returns a string that represents the linking metadata
51+
func nrLinkingString(data newrelic.LinkingMetadata) string {
52+
if data.EntityGUID == "" {
53+
return ""
54+
}
55+
56+
len := 16 + len(data.EntityGUID) + len(data.Hostname) + len(data.TraceID) + len(data.SpanID) + len(data.EntityName)
57+
str := strings.Builder{}
58+
str.Grow(len) // only 1 alloc
59+
60+
str.WriteString(nrlinking)
61+
str.WriteByte('|')
62+
str.WriteString(data.EntityGUID)
63+
str.WriteByte('|')
64+
str.WriteString(data.Hostname)
65+
str.WriteByte('|')
66+
str.WriteString(data.TraceID)
67+
str.WriteByte('|')
68+
str.WriteString(data.SpanID)
69+
str.WriteByte('|')
70+
str.WriteString(data.EntityName)
71+
str.WriteByte('|')
72+
73+
return str.String()
74+
}

v3/newrelic/application.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ func (app *Application) IsAIMonitoringEnabled(integration string, streaming bool
3939
}
4040
*/
4141

42+
// GetLinkingMetadata returns the fields needed to link data to
43+
// an entity. This will return an empty struct if the application
44+
// is not connected or nil.
45+
func (app *Application) GetLinkingMetadata() LinkingMetadata {
46+
if app == nil || app.app == nil {
47+
return LinkingMetadata{}
48+
}
49+
50+
reply, err := app.app.getState()
51+
if err != nil {
52+
app.app.Error("unable to record custom event", map[string]interface{}{
53+
"event-type": "AppState",
54+
"reason": err.Error(),
55+
})
56+
}
57+
58+
md := LinkingMetadata{
59+
EntityName: app.app.config.AppName,
60+
Hostname: app.app.config.hostname,
61+
EntityGUID: reply.Reply.EntityGUID,
62+
}
63+
64+
return md
65+
}
66+
4267
// StartTransaction begins a Transaction with the given name.
4368
func (app *Application) StartTransaction(name string, opts ...TraceOption) *Transaction {
4469
if app == nil {

0 commit comments

Comments
 (0)