Skip to content

Commit 18613dc

Browse files
Merge pull request #997 from iamemilio/slog-2.0
Slog 2.0
2 parents dea6291 + e9de240 commit 18613dc

File tree

10 files changed

+1260
-293
lines changed

10 files changed

+1260
-293
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package nrslog
2+
3+
import (
4+
"log/slog"
5+
"maps"
6+
"strings"
7+
)
8+
9+
type attributeCache struct {
10+
preCompiledAttributes map[string]interface{}
11+
prefix string
12+
}
13+
14+
func newAttributeCache() *attributeCache {
15+
return &attributeCache{
16+
preCompiledAttributes: make(map[string]interface{}),
17+
prefix: "",
18+
}
19+
}
20+
21+
func (c *attributeCache) clone() *attributeCache {
22+
return &attributeCache{
23+
preCompiledAttributes: maps.Clone(c.preCompiledAttributes),
24+
prefix: c.prefix,
25+
}
26+
}
27+
28+
func (c *attributeCache) copyPreCompiledAttributes() map[string]interface{} {
29+
return maps.Clone(c.preCompiledAttributes)
30+
}
31+
32+
func (c *attributeCache) getPrefix() string {
33+
return c.prefix
34+
}
35+
36+
// precompileGroup sets the group prefix for the cache created by a handler
37+
// precompileGroup call. This is used to avoid re-computing the group prefix
38+
// and should only ever be called on newly created caches and handlers.
39+
func (c *attributeCache) precompileGroup(group string) {
40+
if c.prefix != "" {
41+
c.prefix += "."
42+
}
43+
c.prefix += group
44+
}
45+
46+
// precompileAttributes appends attributes to the cache created by a handler
47+
// WithAttrs call. This is used to avoid re-computing the with Attrs attributes
48+
// and should only ever be called on newly created caches and handlers.
49+
func (c *attributeCache) precompileAttributes(attrs []slog.Attr) {
50+
if len(attrs) == 0 {
51+
return
52+
}
53+
54+
for _, a := range attrs {
55+
c.appendAttr(c.preCompiledAttributes, a, c.prefix)
56+
}
57+
}
58+
59+
func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) {
60+
// Resolve the Attr's value before doing anything else.
61+
a.Value = a.Value.Resolve()
62+
// Ignore empty Attrs.
63+
if a.Equal(slog.Attr{}) {
64+
return
65+
}
66+
67+
// majority of runtime spent allocating and copying strings
68+
group := strings.Builder{}
69+
group.Grow(len(groupPrefix) + len(a.Key) + 1)
70+
group.WriteString(groupPrefix)
71+
72+
if a.Key != "" {
73+
if group.Len() > 0 {
74+
group.WriteByte('.')
75+
}
76+
group.WriteString(a.Key)
77+
}
78+
79+
key := group.String()
80+
81+
// If the Attr is a group, append its attributes
82+
if a.Value.Kind() == slog.KindGroup {
83+
attrs := a.Value.Group()
84+
// Ignore empty groups.
85+
if len(attrs) == 0 {
86+
return
87+
}
88+
89+
for _, ga := range attrs {
90+
c.appendAttr(nrAttrs, ga, key)
91+
}
92+
return
93+
}
94+
95+
// attr is an attribute
96+
nrAttrs[key] = a.Value.Any()
97+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package nrslog
2+
3+
import (
4+
"time"
5+
6+
"github.com/newrelic/go-agent/v3/newrelic"
7+
)
8+
9+
const updateFrequency = 1 * time.Minute // check infrequently because the go agent config is not expected to change --> cost 50-100 uS
10+
11+
// 44% faster than checking the config on every log message
12+
type configCache struct {
13+
lastCheck time.Time
14+
15+
// true if we have successfully gotten the config at least once to verify the agent is connected
16+
gotStartupConfig bool
17+
// true if the logs in context feature is enabled as well as either local decorating or forwarding
18+
enabled bool
19+
enrichLogs bool
20+
forwardLogs bool
21+
}
22+
23+
func newConfigCache() *configCache {
24+
return &configCache{}
25+
}
26+
27+
func (c *configCache) clone() *configCache {
28+
return &configCache{
29+
lastCheck: c.lastCheck,
30+
gotStartupConfig: c.gotStartupConfig,
31+
enabled: c.enabled,
32+
enrichLogs: c.enrichLogs,
33+
forwardLogs: c.forwardLogs,
34+
}
35+
}
36+
37+
func (c *configCache) shouldEnrichLog(app *newrelic.Application) bool {
38+
c.update(app)
39+
return c.enrichLogs
40+
}
41+
42+
func (c *configCache) shouldForwardLogs(app *newrelic.Application) bool {
43+
c.update(app)
44+
return c.forwardLogs
45+
}
46+
47+
// isEnabled returns true if the logs in context feature is enabled
48+
// as well as either local decorating or forwarding.
49+
func (c *configCache) isEnabled(app *newrelic.Application) bool {
50+
c.update(app)
51+
return c.enabled
52+
}
53+
54+
// Note: this has a data race in async use cases, but it does not
55+
// cause logical errors, only cache misses. This is acceptable in
56+
// comparison to the cost of synchronization.
57+
func (c *configCache) update(app *newrelic.Application) {
58+
// do not get the config from agent if we have successfully gotten it before
59+
// and it has been less than updateFrequency since the last check. This is
60+
// because on startup, the agent will return a dummy config until it has
61+
// connected and received the real config.
62+
if c.gotStartupConfig && time.Since(c.lastCheck) < updateFrequency {
63+
return
64+
}
65+
66+
config, ok := app.Config()
67+
if !ok {
68+
c.enrichLogs = false
69+
c.forwardLogs = false
70+
c.enabled = false
71+
return
72+
}
73+
74+
c.gotStartupConfig = true
75+
c.enrichLogs = config.ApplicationLogging.LocalDecorating.Enabled && config.ApplicationLogging.Enabled
76+
c.forwardLogs = config.ApplicationLogging.Forwarding.Enabled && config.ApplicationLogging.Enabled
77+
c.enabled = config.ApplicationLogging.Enabled && (c.enrichLogs || c.forwardLogs)
78+
79+
c.lastCheck = time.Now()
80+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ go 1.21
44

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

7-
87
replace github.com/newrelic/go-agent/v3 => ../../..

0 commit comments

Comments
 (0)