Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c1d3317
feat: add configurable transmission delay for metrics
Angith Apr 20, 2026
90fdce5
test: add unit tests for metrics transmission delay
Angith Apr 20, 2026
fb07a49
doc: update README, add example for metrics transmission delay
Angith Apr 20, 2026
9698ebd
Merge branch 'main' into configurable-metrics-interval
Angith Apr 20, 2026
3ccdd57
Merge branch 'main' into configurable-metrics-interval
Angith Apr 20, 2026
7bfc771
fix: use constanats for min, max, default transmission intervals
Angith Apr 21, 2026
a328ffb
Merge branch 'configurable-metrics-interval' of https://github.com/in…
Angith Apr 21, 2026
e0744fd
Merge branch 'main' into configurable-metrics-interval
Angith Apr 21, 2026
aec05a0
Merge branch 'main' into configurable-metrics-interval
Angith May 4, 2026
e234dad
feat: make poll rate retrieval from agent and metrics transmission co…
Angith May 4, 2026
bd8f1e2
feat: handle metrics collection logic on agent reset
Angith May 4, 2026
4f5c4f3
chore: code cleanup
Angith May 4, 2026
b68cba7
fix: cuncurrent access of stop and run
Angith May 5, 2026
81afc08
fix: resolve metrics collection issue for serverless agent
Angith May 5, 2026
578cb74
Merge branch 'main' of https://github.com/instana/go-sensor into conf…
Angith Jul 17, 2026
d531608
refactor: change the upper limit of the poll_rate configuration
Angith Jul 20, 2026
466987a
Merge branch 'main' of https://github.com/instana/go-sensor into conf…
Angith Jul 22, 2026
0720776
refactor: ensure the metric collection run function is executed only …
Angith Jul 22, 2026
9d59857
Merge branch 'main' into configurable-metrics-interval
Angith Jul 22, 2026
54784a2
fix: resolve race condition in collectMemoryMetrics
Angith Jul 24, 2026
d8b59d8
Merge branch 'configurable-metrics-interval' of https://github.com/in…
Angith Jul 24, 2026
a90035a
Merge branch 'main' into configurable-metrics-interval
Angith Jul 24, 2026
520caec
fix: resolve unit test failures
Angith Jul 24, 2026
f4c5888
Merge branch 'configurable-metrics-interval' of https://github.com/in…
Angith Jul 24, 2026
e5c3986
doc: update README.md for serverless agent
Angith Jul 28, 2026
f7133b8
Merge branch 'main' into configurable-metrics-interval
Angith Jul 28, 2026
9bbceb3
fix: address review comments
Angith Jul 28, 2026
de4c50d
Merge branch 'configurable-metrics-interval' of https://github.com/in…
Angith Jul 28, 2026
08717bf
fix: removed transmissionInterval fallback logic from getTransmission…
Angith Jul 29, 2026
6d987b0
refactor: code cleanup
Angith Jul 29, 2026
fe37b10
Merge branch 'main' into configurable-metrics-interval
Angith Jul 30, 2026
a336e7a
test: add unit tests to increase coverage
Angith Jul 31, 2026
1fe4e2b
Merge branch 'configurable-metrics-interval' of https://github.com/in…
Angith Jul 31, 2026
84bfc6f
refactor: replace poll_rate range clamping with canonical-set validation
Angith Aug 2, 2026
e60ec05
fix: corrected format of fsm_test
Angith Aug 3, 2026
f4f3d5e
Merge branch 'main' into configurable-metrics-interval
sanojsubran Aug 3, 2026
27854cd
Merge branch 'main' into configurable-metrics-interval
Angith Aug 6, 2026
7ea2885
fix: remove unwanted debug logs
Angith Aug 6, 2026
8ba2822
Merge branch 'main' into configurable-metrics-interval
Angith Aug 13, 2026
fac269b
Merge branch 'main' into configurable-metrics-interval
nithinputhenveettil Aug 17, 2026
8951f54
Merge branch 'main' into configurable-metrics-interval
nithinputhenveettil Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,41 @@ func init() {
### Collecting Metrics

Once the collector has been initialized with `instana.InitCollector`, application metrics such as memory, CPU consumption, active goroutine count etc will be automatically collected and reported to the Agent without further actions or configurations to the SDK.
This data is then already available in the dashboard.

#### Metrics Transmission Interval

Metrics are transmitted to the Instana Agent at a configurable interval. The interval depends on the deployment environment.

##### Standard (Host Agent) Deployments

The interval is configured through the Instana Agent's `configuration.yaml` file.

**Configuration:**

In the agent's `configuration.yaml`:
```yaml
# Configure metrics transmission interval for Go applications
com.instana.plugin.golang:
poll_rate: 5 # seconds
```

**Valid Values:**

The accepted values are: `1`, `5`, `10`, `20`, `30`, `60`, `120`, `180`, `240`, `300`, `360`, `420`, `480`, `540`, `600` (seconds).

- Default: `1` second (if not configured or if an invalid value is provided)

**Behavior:**
- If `poll_rate` is not configured or is `<= 0`, defaults to `1` second.
- If `poll_rate` is a positive value not in the canonical set above, a warning is logged and the value is used as-is. Range enforcement is the responsibility of the Instana Agent.
- Configuration is read from the agent once, during the initial handshake when the Go tracer starts up.

> [!IMPORTANT]
> The `poll_rate` value is applied **only at Go tracer startup**. If you change `poll_rate` in the agent's `configuration.yaml` after the tracer is already running, the new value will **not** take effect until the Go application is restarted. This applies even if the Instana Agent itself is restarted — the tracer will continue using the interval it received during its own initial handshake.

##### Serverless Deployments (AWS Fargate/ECS, AWS Lambda, Google Cloud Run, Azure Functions)

In serverless environments, the Go tracer communicates directly with the Instana Serverless Acceptor and does not perform the host agent handshake. As a result, the `poll_rate` setting in `configuration.yaml` has no effect. The metrics transmission interval is fixed at **1 second** and cannot be configured.

### Tracing Calls

Expand Down
3 changes: 3 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type agentResponse struct {
ExtraHTTPHeaders []string `json:"extra-http-headers"`
Disable []map[string]bool `json:"disable"`
} `json:"tracing"`
PluginConfig struct {
PollRate int `json:"poll_rate"` // Poll rate in seconds
} `json:"plugin.golang"`
}

func (a *agentResponse) getExtraHTTPHeaders() []string {
Expand Down
73 changes: 73 additions & 0 deletions agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -744,3 +744,76 @@ func TestAgent_IPv4vsIPv6(t *testing.T) {
})
}
}

// TestNoopAgent_Methods verifies that all noopAgent methods return expected zero/nil
// values and do not panic. These are the fallback implementations before sensor init.
func TestNoopAgent_Methods(t *testing.T) {
tests := []struct {
name string
call func(noopAgent) error
wantErr bool
}{
{
name: "SendMetrics returns nil",
call: func(a noopAgent) error { return a.SendMetrics(acceptor.Metrics{}) },
wantErr: false,
},
{
name: "SendEvent returns nil",
call: func(a noopAgent) error { return a.SendEvent(&EventData{}) },
wantErr: false,
},
{
name: "SendSpans returns nil",
call: func(a noopAgent) error { return a.SendSpans(nil) },
wantErr: false,
},
{
name: "SendProfiles returns nil",
call: func(a noopAgent) error { return a.SendProfiles(nil) },
wantErr: false,
},
{
name: "Flush returns nil",
call: func(a noopAgent) error { return a.Flush(context.Background()) },
wantErr: false,
},
}

var a noopAgent
assert.False(t, a.Ready(), "noopAgent.Ready() must always return false")

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.call(a)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

// TestAgentS_Flush verifies that agentS.Flush is a no-op that always returns nil.
func TestAgentS_Flush(t *testing.T) {
agent := &agentS{logger: defaultLogger}
assert.NoError(t, agent.Flush(context.Background()))
}

// TestAgentS_SetLogger verifies that setLogger replaces the agent logger.
func TestAgentS_SetLogger(t *testing.T) {
agent := &agentS{logger: defaultLogger}
newLogger := &testLogger{}
agent.setLogger(newLogger)
assert.Equal(t, newLogger, agent.logger)
}

// TestAgentS_SendMetrics_Error verifies that SendMetrics propagates a connection error
// and triggers a reset when the underlying agentComm cannot reach the host.
func TestAgentS_SendMetrics_Error(t *testing.T) {
agent := newAgent("test-service", "127.0.0.1", 1, defaultLogger)
agent.agentComm = newAgentCommunicator("127.0.0.1", "1", &fromS{EntityID: "123"}, defaultLogger)

assert.Error(t, agent.SendMetrics(acceptor.Metrics{}))
}
54 changes: 54 additions & 0 deletions fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ const (
maximumRetries = 3
)

// validPollRates is the canonical set of accepted poll_rate values (in seconds) as
// defined by the Instana Agent configuration schema. The go tracer does not enforce
// this set — it only warns when an unexpected value is received.
var validPollRates = []int{1, 5, 10, 20, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600}

// isValidPollRate reports whether seconds is a member of the canonical validPollRates set.
func isValidPollRate(seconds int) bool {
for _, v := range validPollRates {
if v == seconds {
return true
}
}
return false
}

type fsmS struct {
agentComm *agentCommunicator
fsm *f.FSM
Expand Down Expand Up @@ -268,10 +283,38 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) {
}

r.applyDisableTracingConfig(resp)
r.applyMetricsPollRateConfig(resp)

r.logger.Debug("CollectableHTTPHeaders used: ", sensor.options.Tracer.CollectableHTTPHeaders)
}

// applyMetricsPollRateConfig applies the metrics poll rate configuration from agent response.
// If the received poll_rate is not a member of the canonical set defined by validPollRates,
// a warning is logged but the value is still applied — range enforcement is the
// responsibility of the Instana Agent.
func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) {
s, err := getSensor()
if err != nil {
r.logger.Debug("Sensor not initialized, skipping poll_rate configuration")
return
}

// If no poll rate is provided by agent, use default (1 second)
if resp.PluginConfig.PollRate <= 0 {
r.logger.Debug("No poll_rate configuration received from agent, using default 1 second")
s.options.Metrics.setTransmissionInterval(defaultTransmissionInterval)
return
}

if !isValidPollRate(resp.PluginConfig.PollRate) {
r.logger.Warn("poll_rate value from agent (", resp.PluginConfig.PollRate, ") is not in the canonical set ",
validPollRates, ". The value will be used as-is; ensure the Instana Agent configuration is correct.")
}

r.logger.Debug("Applying metrics poll_rate configuration from agent: ", resp.PluginConfig.PollRate, " second(s)")
s.options.Metrics.setTransmissionInterval(resp.PluginConfig.PollRate)
}

func (r *fsmS) applyDisableTracingConfig(resp agentResponse) {
// Do nothing if we have no configuration from the agent
if len(resp.Tracing.Disable) == 0 {
Expand Down Expand Up @@ -420,6 +463,17 @@ func (r *fsmS) reset() {

func (r *fsmS) ready(_ context.Context, e *f.Event) {
go delayed.flush()
s, err := getSensor()
if err != nil {
r.logger.Error(err.Error())
return
}
interval := s.options.Metrics.getTransmissionInterval()
if interval <= 0 {
s.options.Metrics.setTransmissionInterval(defaultTransmissionInterval)
interval = s.options.Metrics.getTransmissionInterval()
}
s.meter.Run(interval)
}

func (r *fsmS) cpuSetFileContent(pid int) string {
Expand Down
154 changes: 153 additions & 1 deletion fsm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ import (

type testLogger struct {
infoMsg string
warnMsg string
errMsg string
}

func (tl *testLogger) Debug(v ...interface{}) {}
func (tl *testLogger) Info(v ...interface{}) {
tl.infoMsg = fmt.Sprint(v...)
}
func (tl *testLogger) Warn(v ...interface{}) {}
func (tl *testLogger) Warn(v ...interface{}) {
tl.warnMsg = fmt.Sprint(v...)
}
func (tl *testLogger) Error(v ...interface{}) {
tl.errMsg = fmt.Sprint(v...)
}
Expand Down Expand Up @@ -635,3 +638,152 @@ func TestApplyDisableTracingConfig(t *testing.T) {
})
}
}

func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) {
tests := []struct {
name string
pollRate int
expectedSecs int
expectWarn bool
}{
{
name: "Canonical 1 second — no warning",
pollRate: 1,
expectedSecs: 1,
expectWarn: false,
},
{
name: "Canonical 5 seconds — no warning",
pollRate: 5,
expectedSecs: 5,
expectWarn: false,
},
{
name: "Canonical 10 seconds — no warning",
pollRate: 10,
expectedSecs: 10,
expectWarn: false,
},
{
name: "Canonical 60 seconds — no warning",
pollRate: 60,
expectedSecs: 60,
expectWarn: false,
},
{
name: "Canonical 600 seconds — no warning",
pollRate: 600,
expectedSecs: 600,
expectWarn: false,
},
{
name: "Non-canonical positive value (7s) — applied as-is with warning",
pollRate: 7,
expectedSecs: 7,
expectWarn: true,
},
{
name: "Large positive value (5000s) — applied as-is with warning",
pollRate: 5000,
expectedSecs: 5000,
expectWarn: true,
},
{
name: "Zero — uses default (1s), no warning",
pollRate: 0,
expectedSecs: 1,
expectWarn: false,
},
{
name: "Negative value (-5) — uses default (1s), no warning",
pollRate: -5,
expectedSecs: 1,
expectWarn: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize sensor with default options
sensor = newSensor(DefaultOptions())
defer func() { sensor = nil }()

tLogger := &testLogger{}
fsm := &fsmS{
logger: tLogger,
}

resp := agentResponse{
PluginConfig: struct {
PollRate int `json:"poll_rate"`
}{
PollRate: tt.pollRate,
},
}

fsm.applyMetricsPollRateConfig(resp)

interval := sensor.options.Metrics.getTransmissionInterval()
assert.Equal(t, time.Duration(tt.expectedSecs)*time.Second, interval)

if tt.expectWarn {
assert.NotEmpty(t, tLogger.warnMsg, "expected a warning to be logged for non-canonical poll_rate %d", tt.pollRate)
} else {
assert.Empty(t, tLogger.warnMsg, "expected no warning for poll_rate %d", tt.pollRate)
}
})
}
}

// Test_fsmS_applyMetricsPollRateConfig_NoSensor verifies that applyMetricsPollRateConfig
// is a no-op (does not panic) when the global sensor has not been initialized.
func Test_fsmS_applyMetricsPollRateConfig_NoSensor(t *testing.T) {
// Ensure no global sensor is set.
origSensor := sensor
sensor = nil
defer func() { sensor = origSensor }()

fsm := &fsmS{logger: &testLogger{}}
resp := agentResponse{}

// Must not panic even though getSensor() will return an error.
assert.NotPanics(t, func() {
fsm.applyMetricsPollRateConfig(resp)
})
}

// Test_fsmS_ready_NoSensor verifies that ready() logs an error and returns early
// when the global sensor has not been initialized (getSensor returns an error).
func Test_fsmS_ready_NoSensor(t *testing.T) {
origSensor := sensor
sensor = nil
defer func() { sensor = origSensor }()

tLogger := &testLogger{}
fsm := &fsmS{logger: tLogger}

assert.NotPanics(t, func() {
fsm.ready(context.Background(), nil)
})
assert.NotEmpty(t, tLogger.errMsg, "expected error to be logged when sensor is nil")
}

// Test_fsmS_ready_IntervalZero verifies that ready() applies the default interval
// when the sensor's transmission interval has not been set (zero value).
func Test_fsmS_ready_IntervalZero(t *testing.T) {
sensor = newSensor(DefaultOptions())
// Interval is zero by default (not set by FSM/agent yet).
assert.Equal(t, time.Duration(0), sensor.options.Metrics.getTransmissionInterval())
defer func() {
sensor.meter.Stop()
sensor = nil
}()

fsm := &fsmS{logger: &testLogger{}}
assert.NotPanics(t, func() {
fsm.ready(context.Background(), nil)
})

// After ready(), the interval must have been set to the default.
assert.Equal(t, defaultTransmissionInterval*time.Second, sensor.options.Metrics.getTransmissionInterval())
}
Loading
Loading