Skip to content

Commit

Permalink
Merge pull request #997 from iamemilio/slog-2.0
Browse files Browse the repository at this point in the history
Slog 2.0
  • Loading branch information
nr-swilloughby authored Feb 19, 2025
2 parents dea6291 + e9de240 commit 18613dc
Show file tree
Hide file tree
Showing 10 changed files with 1,260 additions and 293 deletions.
97 changes: 97 additions & 0 deletions v3/integrations/logcontext-v2/nrslog/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package nrslog

import (
"log/slog"
"maps"
"strings"
)

type attributeCache struct {
preCompiledAttributes map[string]interface{}
prefix string
}

func newAttributeCache() *attributeCache {
return &attributeCache{
preCompiledAttributes: make(map[string]interface{}),
prefix: "",
}
}

func (c *attributeCache) clone() *attributeCache {
return &attributeCache{
preCompiledAttributes: maps.Clone(c.preCompiledAttributes),
prefix: c.prefix,
}
}

func (c *attributeCache) copyPreCompiledAttributes() map[string]interface{} {
return maps.Clone(c.preCompiledAttributes)
}

func (c *attributeCache) getPrefix() string {
return c.prefix
}

// precompileGroup sets the group prefix for the cache created by a handler
// precompileGroup call. This is used to avoid re-computing the group prefix
// and should only ever be called on newly created caches and handlers.
func (c *attributeCache) precompileGroup(group string) {
if c.prefix != "" {
c.prefix += "."
}
c.prefix += group
}

// precompileAttributes appends attributes to the cache created by a handler
// WithAttrs call. This is used to avoid re-computing the with Attrs attributes
// and should only ever be called on newly created caches and handlers.
func (c *attributeCache) precompileAttributes(attrs []slog.Attr) {
if len(attrs) == 0 {
return
}

for _, a := range attrs {
c.appendAttr(c.preCompiledAttributes, a, c.prefix)
}
}

func (c *attributeCache) appendAttr(nrAttrs map[string]interface{}, a slog.Attr, groupPrefix string) {
// Resolve the Attr's value before doing anything else.
a.Value = a.Value.Resolve()
// Ignore empty Attrs.
if a.Equal(slog.Attr{}) {
return
}

// majority of runtime spent allocating and copying strings
group := strings.Builder{}
group.Grow(len(groupPrefix) + len(a.Key) + 1)
group.WriteString(groupPrefix)

if a.Key != "" {
if group.Len() > 0 {
group.WriteByte('.')
}
group.WriteString(a.Key)
}

key := group.String()

// If the Attr is a group, append its attributes
if a.Value.Kind() == slog.KindGroup {
attrs := a.Value.Group()
// Ignore empty groups.
if len(attrs) == 0 {
return
}

for _, ga := range attrs {
c.appendAttr(nrAttrs, ga, key)
}
return
}

// attr is an attribute
nrAttrs[key] = a.Value.Any()
}
80 changes: 80 additions & 0 deletions v3/integrations/logcontext-v2/nrslog/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package nrslog

import (
"time"

"github.com/newrelic/go-agent/v3/newrelic"
)

const updateFrequency = 1 * time.Minute // check infrequently because the go agent config is not expected to change --> cost 50-100 uS

// 44% faster than checking the config on every log message
type configCache struct {
lastCheck time.Time

// true if we have successfully gotten the config at least once to verify the agent is connected
gotStartupConfig bool
// true if the logs in context feature is enabled as well as either local decorating or forwarding
enabled bool
enrichLogs bool
forwardLogs bool
}

func newConfigCache() *configCache {
return &configCache{}
}

func (c *configCache) clone() *configCache {
return &configCache{
lastCheck: c.lastCheck,
gotStartupConfig: c.gotStartupConfig,
enabled: c.enabled,
enrichLogs: c.enrichLogs,
forwardLogs: c.forwardLogs,
}
}

func (c *configCache) shouldEnrichLog(app *newrelic.Application) bool {
c.update(app)
return c.enrichLogs
}

func (c *configCache) shouldForwardLogs(app *newrelic.Application) bool {
c.update(app)
return c.forwardLogs
}

// isEnabled returns true if the logs in context feature is enabled
// as well as either local decorating or forwarding.
func (c *configCache) isEnabled(app *newrelic.Application) bool {
c.update(app)
return c.enabled
}

// Note: this has a data race in async use cases, but it does not
// cause logical errors, only cache misses. This is acceptable in
// comparison to the cost of synchronization.
func (c *configCache) update(app *newrelic.Application) {
// do not get the config from agent if we have successfully gotten it before
// and it has been less than updateFrequency since the last check. This is
// because on startup, the agent will return a dummy config until it has
// connected and received the real config.
if c.gotStartupConfig && time.Since(c.lastCheck) < updateFrequency {
return
}

config, ok := app.Config()
if !ok {
c.enrichLogs = false
c.forwardLogs = false
c.enabled = false
return
}

c.gotStartupConfig = true
c.enrichLogs = config.ApplicationLogging.LocalDecorating.Enabled && config.ApplicationLogging.Enabled
c.forwardLogs = config.ApplicationLogging.Forwarding.Enabled && config.ApplicationLogging.Enabled
c.enabled = config.ApplicationLogging.Enabled && (c.enrichLogs || c.forwardLogs)

c.lastCheck = time.Now()
}
1 change: 0 additions & 1 deletion v3/integrations/logcontext-v2/nrslog/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ go 1.21

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


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

0 comments on commit 18613dc

Please sign in to comment.