Skip to content
2 changes: 1 addition & 1 deletion v3/newrelic/app_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (run *appRun) LoggingConfig() (config loggingConfig) {
// which will be the default or the user's configured size (if any), but
// may be capped to the maximum allowed by the collector.
func (run *appRun) MaxSpanEvents() int {
return run.limit(internal.MaxSpanEvents, run.ptrSpanEvents)
return run.limit(run.Config.SpanEvents.MaxSamplesStored, run.ptrSpanEvents)
}

func (run *appRun) limit(dflt int, field func() *uint) int {
Expand Down
175 changes: 175 additions & 0 deletions v3/newrelic/app_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,178 @@ func TestCreateTransactionName(t *testing.T) {
t.Error("wanted:", want, "got:", out)
}
}

func testMockConnectReply(t *testing.T, retVal string) *internal.ConnectReply {
h, err := internal.UnmarshalConnectReply([]byte(retVal), internal.PreconnectReply{})
if nil != err {
t.Fatal(err)
}
return h
}

func Test_appRun_MaxSpanEvents(t *testing.T) {
// this test assumes the default max is 2000 span events
tests := []struct {
name string // description of this test case
// Named input parameters for receiver constructor.
config config
reply *internal.ConnectReply
want int
}{
{
name: "MaxSamplesStored is default and Reply limit is max span events",
config: config{Config: defaultConfig()},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 2000
}
}}`),
want: 2000,
},
{
name: "MaxSamplesStored is default and Reply limit is nil",
config: config{Config: defaultConfig()},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": null
}}`),
want: 2000,
},
{
name: "MaxSamplesStored is greater than response from harvester",
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 1500},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 1400
}
}}`),
want: 1400,
},
{
name: "MaxSamplesStored is less than 2000 and response is greater", // we don't expect this case to happen, but we are showing that we will use whatever the harvester response is
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 1999},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 2000
}
}}`),
want: 2000,
},
{
name: "MaxSamplesStored is greater than 2000 and response is max span events",
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 20000},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 2000
}
}}`),
want: 2000,
},
{
name: "MaxSamplesStored is greater than 2000 and response is less than max span events",
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 20000},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 50
}
}}`),
want: 50,
},
{
name: "MaxSamplesStored is 2000 and response is greater than our coded max span events", // we don't expect this case to happen, but we are showing that we will use whatever the harvester response is
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 2000},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 4455
}
}}`),
want: 4455,
},
{
name: "MaxSamplesStored is greater than 2000 and response is greater than our coded max span events", // we don't expect this case to happen, but we are showing that we will use whatever the harvester response is
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 5000},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 5000
}
}}`),
want: 5000,
},
{
name: "MaxSamplesStored is 0 and response is 0",
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 0},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": 0
}
}}`),
want: 0,
},
{
name: "MaxSamplesStored is 0 and response is nil",
config: config{Config: Config{
SpanEvents: struct {
Enabled bool
Attributes AttributeDestinationConfig
MaxSamplesStored int
}{MaxSamplesStored: 0},
}},
reply: testMockConnectReply(t, `{"return_value":{
"span_event_harvest_config": {
"harvest_limit": null
}
}}`),
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
run := newAppRun(tt.config, tt.reply)
got := run.MaxSpanEvents()
if got != tt.want {
t.Errorf("MaxSpanEvents() = %v, want %v", got, tt.want)
}
})
}
}
16 changes: 15 additions & 1 deletion v3/newrelic/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ type Config struct {
Enabled bool
// Attributes controls the attributes included on Spans.
Attributes AttributeDestinationConfig
// MaxSamplesStored allows you to limit the number of Span
// Events stored/reported in a given 60-second period
MaxSamplesStored int
}

// InfiniteTracing controls behavior related to Infinite Tracing tail based
Expand Down Expand Up @@ -660,6 +663,7 @@ func defaultConfig() Config {
c.TransactionEvents.Enabled = true
c.TransactionEvents.Attributes.Enabled = true
c.TransactionEvents.MaxSamplesStored = internal.MaxTxnEvents

c.HighSecurity = false
c.ErrorCollector.Enabled = true
c.ErrorCollector.CaptureEvents = true
Expand Down Expand Up @@ -707,6 +711,7 @@ func defaultConfig() Config {
c.DistributedTracer.Sampler.RemoteParentNotSampled = Default.String()
c.SpanEvents.Enabled = true
c.SpanEvents.Attributes.Enabled = true
c.SpanEvents.MaxSamplesStored = internal.MaxSpanEvents

c.DatastoreTracer.InstanceReporting.Enabled = true
c.DatastoreTracer.DatabaseNameReporting.Enabled = true
Expand Down Expand Up @@ -812,6 +817,15 @@ func (c Config) maxTxnEvents() int {
return configured
}

// maxConfigEvents returns the configured maximum number of events if it has been configured
// and is less than the default maximum; otherwise it returns the default max.
func maxConfigEvents(configured int, max int) int {
if configured < 0 || configured > max {
return max
}
return configured
}

// maxCustomEvents returns the configured maximum number of Custom Events if it has been configured
// and is less than the default maximum; otherwise it returns the default max.
func (c Config) maxCustomEvents() int {
Expand Down Expand Up @@ -1014,7 +1028,7 @@ func configConnectJSONInternal(c Config, pid int, util *utilization.Data, e envi
Util: util,
SecurityPolicies: securityPolicies,
Metadata: metadata,
EventData: internal.DefaultEventHarvestConfigWithDT(c.TransactionEvents.MaxSamplesStored, c.ApplicationLogging.Forwarding.MaxSamplesStored, c.CustomInsightsEvents.MaxSamplesStored, c.DistributedTracer.ReservoirLimit, c.DistributedTracer.Enabled),
EventData: internal.DefaultEventHarvestConfigWithDT(c.TransactionEvents.MaxSamplesStored, c.ApplicationLogging.Forwarding.MaxSamplesStored, c.CustomInsightsEvents.MaxSamplesStored, c.SpanEvents.MaxSamplesStored, c.DistributedTracer.Enabled),
}})
}

Expand Down
32 changes: 31 additions & 1 deletion v3/newrelic/config_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"unicode/utf8"

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

// ConfigOption configures the Config when provided to NewApplication.
Expand Down Expand Up @@ -98,17 +100,32 @@ func ConfigCustomInsightsEventsMaxSamplesStored(limit int) ConfigOption {
return func(cfg *Config) { cfg.CustomInsightsEvents.MaxSamplesStored = limit }
}

// ConfigSpanEventsMaxSamplesStored alters the sample size allowing control
// of how many span events are stored in an agent for a given harvest cycle.
// Alters the SpanEvents.MaxSamplesStored setting.
// Note: As of Oct 2025, the absolute maximum span events that can be sent each minute is 2000.
func ConfigSpanEventsMaxSamplesStored(limit int) ConfigOption {
return func(cfg *Config) { cfg.SpanEvents.MaxSamplesStored = maxConfigEvents(limit, internal.MaxSpanEvents) }
}

// ConfigSpanEventsEnabled enables or disables the collection of span events.
func ConfigSpanEventsEnabled(enabled bool) ConfigOption {
return func(cfg *Config) { cfg.SpanEvents.Enabled = enabled }
}

// ConfigCustomInsightsEventsEnabled enables or disables the collection of custom insight events.
func ConfigCustomInsightsEventsEnabled(enabled bool) ConfigOption {
return func(cfg *Config) { cfg.CustomInsightsEvents.Enabled = enabled }
}

// Deprecated: ConfigDistributedTracerReservoirLimit is deprecated in favor of ConfigSpanEventsMaxSamplesStored
// ConfigDistributedTracerReservoirLimit alters the sample reservoir size (maximum
// number of span events to be collected) for distributed tracing instead of
// using the built-in default.
// Alters the DistributedTracer.ReservoirLimit setting.
func ConfigDistributedTracerReservoirLimit(limit int) ConfigOption {
return func(cfg *Config) { cfg.DistributedTracer.ReservoirLimit = limit }
// will add some logging logic here to notify that this option is deprectated
return ConfigSpanEventsMaxSamplesStored(limit)
}

// ConfigAIMonitoringStreamingEnabled turns on or off the collection of AI Monitoring streaming mode metrics.
Expand Down Expand Up @@ -531,6 +548,15 @@ func configFromEnvironment(getenv func(string) string) ConfigOption {
}
}
}
assignIntWithMax := func(field *int, name string, max int) {
if env := getenv(name); env != "" {
if i, err := strconv.Atoi(env); nil != err {
cfg.Error = fmt.Errorf("invalid %s value: %s", name, env)
} else {
*field = maxConfigEvents(i, max) // should send a warning back if the value is set to default
}
}
}
assignString := func(field *string, name string) {
if env := getenv(name); env != "" {
*field = env
Expand Down Expand Up @@ -577,6 +603,10 @@ func configFromEnvironment(getenv func(string) string) ConfigOption {
assignBool(&cfg.AIMonitoring.RecordContent.Enabled, "NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED")
assignBool(&cfg.CustomInsightsEvents.CustomAttributesEnabled, "NEW_RELIC_APPLICATION_LOGGING_FORWARDING_CUSTOM_ATTRIBUTES_ENABLED")

// Span Event Env Variables
assignBool(&cfg.SpanEvents.Enabled, "NEW_RELIC_SPAN_EVENTS_ENABLED")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mirackara I added in this line to allow for control for enabling span events through env vars. Should I add a function for this as well?

assignIntWithMax(&cfg.SpanEvents.MaxSamplesStored, "NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED", internal.MaxSpanEvents)

if env := getenv("NEW_RELIC_LABELS"); env != "" {
labels, err := getLabels(getenv("NEW_RELIC_LABELS"))
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions v3/newrelic/config_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,62 @@ func TestConfigRemoteParentNotSampledOff(t *testing.T) {
t.Error("incorrect config value for DistributedTracer.Sampler.RemoteParentNotSampled:", cfg.DistributedTracer.Sampler.RemoteParentNotSampled)
}
}

func TestConfigSpanEventsMaxSamplesStored(t *testing.T) {
// these tests assume internal.MaxSpanEvents = 2000
tests := []struct {
name string // description of this test case
limit int // limit that is being passed in
want int
}{
{
name: "MaxSamplesStored is less than 0",
limit: -1,
want: 2000,
},
{
name: "MaxSamplesStored is greater than 2000",
limit: 2001,
want: 2000,
},
{
name: "MaxSamplesStored is much greater than 2000",
limit: 100000,
want: 2000,
},
{
name: "MaxSamplesStored is between 0 and 2000",
limit: 500,
want: 500,
},
{
name: "MaxSamplesStored is 0",
limit: 0,
want: 0,
},
{
name: "MaxSamplesStored is 2000",
limit: 2000,
want: 2000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfgOpt := ConfigSpanEventsMaxSamplesStored(tt.limit)
cfg := defaultConfig()
cfgOpt(&cfg)
if cfg.SpanEvents.MaxSamplesStored != tt.want {
t.Errorf("cfg.SpanEvents.MaxSamplesStored = %v, want %v", cfg.SpanEvents.MaxSamplesStored, tt.want)
}
})
// Should be the same result if using the wrapped function
t.Run(tt.name, func(t *testing.T) {
cfgOpt := ConfigDistributedTracerReservoirLimit(tt.limit)
cfg := defaultConfig()
cfgOpt(&cfg)
if cfg.SpanEvents.MaxSamplesStored != tt.want {
t.Errorf("cfg.SpanEvents.MaxSamplesStored = %v, want %v", cfg.SpanEvents.MaxSamplesStored, tt.want)
}
})
}
}
Loading
Loading