From c1d33173b918a29d2e14b1b297c529d5ca9d6d99 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 20 Apr 2026 10:42:16 +0530 Subject: [PATCH 01/21] feat: add configurable transmission delay for metrics --- meter.go | 25 +++++++++++++++++++++++++ options.go | 40 ++++++++++++++++++++++++++++++++++++++++ sensor.go | 3 ++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/meter.go b/meter.go index ef77bca6e..498d12f19 100644 --- a/meter.go +++ b/meter.go @@ -27,6 +27,31 @@ type meterS struct { done chan struct{} } +// MetricsOptions contains configuration for metrics collection and transmission +type MetricsOptions struct { + // TransmissionDelay specifies the interval in milliseconds between metrics transmissions + // to the Instana agent. + // + // Default: 1000 (1 second) + // Minimum: 1000 (enforced via validation, values < 1000 use default) + // Maximum: 5000 (5 seconds, values above are capped with warning) + // + // This value can be configured via: + // - Environment variable: INSTANA_METRICS_TRANSMISSION_DELAY + // - Code: opts.Metrics.TransmissionDelay = 2000 + // + // Configuration precedence: ENV > code > default + // + // Example: + // opts := &instana.Options{ + // Service: "MyApp", + // Metrics: instana.MetricsOptions{ + // TransmissionDelay: 2000, // 2 seconds + // }, + // } + TransmissionDelay int +} + func newMeter(logger LeveledLogger) *meterS { logger.Debug("initializing meter") diff --git a/options.go b/options.go index 974e1a66e..c454cc0a3 100644 --- a/options.go +++ b/options.go @@ -40,6 +40,8 @@ type Options struct { MaxBufferedProfiles int // IncludeProfilerFrames is whether to include profiler calls into the profile or not IncludeProfilerFrames bool + // Metrics contains metrics collection and transmission configuration + Metrics MetricsOptions // Tracer contains tracer-specific configuration used by all tracers Tracer TracerOptions // AgentClient client to communicate with the agent. In most cases, there is no need to provide it. @@ -70,6 +72,7 @@ func (opts *Options) applyConfiguration() { opts.applyAgentConfiguration() opts.applyServiceConfiguration() opts.applyProfilingConfiguration() + opts.applyMetricsConfiguration() opts.applyTracerConfiguration() } @@ -123,6 +126,43 @@ func (opts *Options) applyProfilingConfiguration() { } } +// applyMetricsConfiguration resolves metrics collection and transmission settings +// Precedence: ENV > in-code > default +func (opts *Options) applyMetricsConfiguration() { + // Step 1: Apply default if zero + if opts.Metrics.TransmissionDelay == 0 { + opts.Metrics.TransmissionDelay = 1000 + } + + // Step 2: Check environment variable (takes precedence over code configuration) + if envDelay := os.Getenv("INSTANA_METRICS_TRANSMISSION_DELAY"); envDelay != "" { + if delay, err := strconv.Atoi(envDelay); err != nil { + // Invalid format - non-numeric value + defaultLogger.Warn("Invalid INSTANA_METRICS_TRANSMISSION_DELAY value: ", envDelay, ", using default 1000ms") + opts.Metrics.TransmissionDelay = 1000 + } else if delay <= 0 { + // Non-positive value + defaultLogger.Warn("INSTANA_METRICS_TRANSMISSION_DELAY must be positive, using default 1000ms") + opts.Metrics.TransmissionDelay = 1000 + } else { + // Valid value from ENV + opts.Metrics.TransmissionDelay = delay + } + } + + // Step 3: Enforce minimum value (1000ms) + if opts.Metrics.TransmissionDelay < 1000 { + defaultLogger.Warn("metrics transmission delay is below minimum 1000ms, using minimum 1000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") + opts.Metrics.TransmissionDelay = 1000 + } + + // Step 4: Enforce maximum cap (5000ms) + if opts.Metrics.TransmissionDelay > 5000 { + defaultLogger.Warn("metrics transmission delay exceeds maximum 5000ms, capping at 5000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") + opts.Metrics.TransmissionDelay = 5000 + } +} + // applyTracerConfiguration resolves tracer-specific settings // Precedence: ENV > in-code > agent config > default func (opts *Options) applyTracerConfiguration() { diff --git a/sensor.go b/sensor.go index 332ef95f4..75fd346b9 100644 --- a/sensor.go +++ b/sensor.go @@ -225,7 +225,8 @@ func InitSensor(options *Options) { configureAutoProfiling(options) // start collecting metrics - go sensor.meter.Run(1 * time.Second) + interval := time.Duration(options.Metrics.TransmissionDelay) * time.Millisecond + go sensor.meter.Run(interval) sensor.logger.Debug("initialized Instana sensor v", Version) } From 90fdce5097a2033bf5231cc3b06052f45f0e9920 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 20 Apr 2026 11:36:38 +0530 Subject: [PATCH 02/21] test: add unit tests for metrics transmission delay --- meter.go | 10 ++ options_test.go | 350 ++++++++++++++++++++++++++++++++++++++++++++++++ sensor.go | 3 +- 3 files changed, 361 insertions(+), 2 deletions(-) diff --git a/meter.go b/meter.go index 498d12f19..7dd476125 100644 --- a/meter.go +++ b/meter.go @@ -87,6 +87,16 @@ func (m *meterS) Stop() { m.done <- struct{}{} } +func getTransmissionDelay(options *Options) time.Duration { + interval := time.Duration(options.Metrics.TransmissionDelay) * time.Millisecond + // Safety check: fallback to default if interval becomes negative, + // possibly due to missing TransmissionDelay during sensor re-initialization. + if interval <= 0 { + interval = 1000 * time.Millisecond + } + return interval +} + func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) diff --git a/options_test.go b/options_test.go index 2488dd0ba..2518077fc 100644 --- a/options_test.go +++ b/options_test.go @@ -896,3 +896,353 @@ func TestApplyConfiguration_DefaultsOnly(t *testing.T) { assert.Nil(t, opts.Tracer.CollectableHTTPHeaders) assert.Nil(t, opts.Tracer.DisableSpans) } + +// TestApplyMetricsConfiguration_Default tests default metrics transmission delay +func TestApplyMetricsConfiguration_Default(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + + opts := &Options{} + opts.applyMetricsConfiguration() + + assert.Equal(t, 1000, opts.Metrics.TransmissionDelay, "Default should be 1000ms") +} + +// TestApplyMetricsConfiguration_ValidENV tests valid ENV override +func TestApplyMetricsConfiguration_ENVValidation(t *testing.T) { + tests := []struct { + name string + envValue string + expected int + }{ + // Valid values + { + name: "Valid 1000ms (min boundary)", + envValue: "1000", + expected: 1000, + }, + { + name: "Valid 2000ms (mid-range)", + envValue: "2000", + expected: 2000, + }, + { + name: "Valid 5000ms (max boundary)", + envValue: "5000", + expected: 5000, + }, + // Invalid values - fall back to default + { + name: "Non-numeric value", + envValue: "invalid", + expected: 1000, + }, + { + name: "Empty string", + envValue: "", + expected: 1000, + }, + { + name: "Float value", + envValue: "1500.5", + expected: 1000, + }, + // Below minimum - enforced to minimum + { + name: "Zero value", + envValue: "0", + expected: 1000, + }, + { + name: "Negative value", + envValue: "-500", + expected: 1000, + }, + { + name: "Below minimum (999ms)", + envValue: "999", + expected: 1000, + }, + // Above maximum - capped at maximum + { + name: "Above maximum (6000ms)", + envValue: "6000", + expected: 5000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + + if tt.envValue != "" { + os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) + } else { + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + } + + opts := &Options{} + opts.applyMetricsConfiguration() + + assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay) + }) + } +} + +// TestApplyMetricsConfiguration_ENVPrecedence tests ENV overrides code configuration +func TestApplyMetricsConfiguration_ENVPrecedence(t *testing.T) { + tests := []struct { + name string + inCodeValue int + envValue string + expectedCode int + expectedENV int + }{ + { + name: "ENV overrides in-code", + inCodeValue: 2000, + envValue: "3000", + expectedCode: 2000, // Without ENV + expectedENV: 3000, // With ENV + }, + { + name: "ENV overrides default", + inCodeValue: 0, // Will use default 1000 + envValue: "2500", + expectedCode: 1000, // Default + expectedENV: 2500, // ENV override + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + + // Test without ENV + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + opts := &Options{ + Metrics: MetricsOptions{ + TransmissionDelay: tt.inCodeValue, + }, + } + opts.applyMetricsConfiguration() + assert.Equal(t, tt.expectedCode, opts.Metrics.TransmissionDelay, "In-code value should be used without ENV") + + // Test with ENV + os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) + opts = &Options{ + Metrics: MetricsOptions{ + TransmissionDelay: tt.inCodeValue, + }, + } + opts.applyMetricsConfiguration() + assert.Equal(t, tt.expectedENV, opts.Metrics.TransmissionDelay, "ENV should override in-code value") + }) + } +} + +// TestApplyMetricsConfiguration_CodeConfiguration tests code-based configuration +func TestApplyMetricsConfiguration_CodeConfiguration(t *testing.T) { + tests := []struct { + name string + inCodeValue int + expected int + description string + }{ + { + name: "Valid code value 2000ms", + inCodeValue: 2000, + expected: 2000, + description: "Should use in-code value", + }, + { + name: "Code value below minimum (500ms)", + inCodeValue: 500, + expected: 1000, + description: "Should enforce minimum 1000ms", + }, + { + name: "Code value at max 5000ms", + inCodeValue: 5000, + expected: 5000, + description: "Should use max value", + }, + { + name: "Code value above max", + inCodeValue: 6000, + expected: 5000, + description: "Should cap at 5000ms", + }, + { + name: "Zero value uses default", + inCodeValue: 0, + expected: 1000, + description: "Should use default 1000ms", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + + opts := &Options{ + Metrics: MetricsOptions{ + TransmissionDelay: tt.inCodeValue, + }, + } + opts.applyMetricsConfiguration() + + assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay, tt.description) + }) + } +} + +// TestApplyMetricsConfiguration_BackwardCompatibility tests backward compatibility +func TestApplyMetricsConfiguration_BackwardCompatibility(t *testing.T) { + tests := []struct { + name string + setupOpts func() *Options + expected int + description string + }{ + { + name: "Empty Options struct", + setupOpts: func() *Options { + return &Options{} + }, + expected: 1000, + description: "Should use default 1000ms", + }, + { + name: "Options with other fields set", + setupOpts: func() *Options { + return &Options{ + AgentHost: "localhost", + AgentPort: 42699, + Service: "test-service", + } + }, + expected: 1000, + description: "Should use default 1000ms when Metrics not set", + }, + { + name: "DefaultOptions()", + setupOpts: func() *Options { + return DefaultOptions() + }, + expected: 1000, + description: "DefaultOptions should result in 1000ms", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + + opts := tt.setupOpts() + opts.applyMetricsConfiguration() + + assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay, tt.description) + }) + } +} + +// BenchmarkApplyMetricsConfiguration benchmarks the configuration overhead +func BenchmarkApplyMetricsConfiguration(b *testing.B) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + + benchmarks := []struct { + name string + envValue string + }{ + { + name: "Default (no ENV)", + envValue: "", + }, + { + name: "Valid ENV", + envValue: "2000", + }, + { + name: "Invalid ENV", + envValue: "invalid", + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + if bm.envValue != "" { + os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", bm.envValue) + } else { + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + opts := &Options{} + opts.applyMetricsConfiguration() + } + }) + } +} + +// TestApplyConfiguration_WithMetrics tests full integration with applyConfiguration +func TestApplyConfiguration_WithMetrics(t *testing.T) { + restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") + defer restore() + + tests := []struct { + name string + envValue string + inCodeValue int + expected int + }{ + { + name: "Full integration - ENV override", + envValue: "3000", + inCodeValue: 2000, + expected: 3000, + }, + { + name: "Full integration - code only", + envValue: "", + inCodeValue: 2500, + expected: 2500, + }, + { + name: "Full integration - default", + envValue: "", + inCodeValue: 0, + expected: 1000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) + } else { + os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") + } + + opts := &Options{ + Metrics: MetricsOptions{ + TransmissionDelay: tt.inCodeValue, + }, + } + + // Call the full applyConfiguration which should call applyMetricsConfiguration + opts.applyConfiguration() + + assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay) + }) + } +} diff --git a/sensor.go b/sensor.go index 75fd346b9..997de3d81 100644 --- a/sensor.go +++ b/sensor.go @@ -225,8 +225,7 @@ func InitSensor(options *Options) { configureAutoProfiling(options) // start collecting metrics - interval := time.Duration(options.Metrics.TransmissionDelay) * time.Millisecond - go sensor.meter.Run(interval) + go sensor.meter.Run(getTransmissionDelay(options)) sensor.logger.Debug("initialized Instana sensor v", Version) } From fb07a494e8ad3d893e968f4c057531f555f5fc71 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 20 Apr 2026 13:13:27 +0530 Subject: [PATCH 03/21] doc: update README, add example for metrics transmission delay --- README.md | 28 +++++ example/metrics-interval/README.md | 78 +++++++++++++ example/metrics-interval/code-config/main.go | 110 +++++++++++++++++++ example/metrics-interval/env-config/main.go | 88 +++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 example/metrics-interval/README.md create mode 100644 example/metrics-interval/code-config/main.go create mode 100644 example/metrics-interval/env-config/main.go diff --git a/README.md b/README.md index be9916e46..be26a4374 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,34 @@ 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. + +#### Configuring Metrics Transmission Interval + +By default, metrics are transmitted to the Instana Agent every 1000ms (1 second). You can customize this interval to suit your application's needs: + +**Via Environment Variable:** +```bash +export INSTANA_METRICS_TRANSMISSION_DELAY=2000 # 2 seconds +``` + +**Via Code Configuration:** +```go +col = instana.InitCollector(&instana.Options{ + Service: "My app", + Metrics: instana.MetricsOptions{ + TransmissionDelay: 3000, // 3 seconds + }, +}) +``` + +**Configuration Rules:** +- Valid range: 1000ms to 5000ms +- Default: 1000ms (if not specified or invalid) +- Values above 5000ms are automatically capped +- Environment variable takes precedence over code configuration + +For detailed examples and use cases, see [example/metrics-interval/](example/metrics-interval/). + This data is then already available in the dashboard. ### Tracing Calls diff --git a/example/metrics-interval/README.md b/example/metrics-interval/README.md new file mode 100644 index 000000000..128f92611 --- /dev/null +++ b/example/metrics-interval/README.md @@ -0,0 +1,78 @@ +# Configurable Metrics Transmission Interval Examples + +This directory contains examples demonstrating how to configure the metrics transmission interval in the Instana Go Sensor. + +## Overview + +The Instana Go Sensor allows you to configure how frequently metrics are transmitted to the Instana agent. By default, metrics are sent every 1000ms (1 second), but this can be customized based on your application's needs. + +## Configuration Methods + +### 1. Environment Variable (env-config/) + +Configure the interval using the `INSTANA_METRICS_TRANSMISSION_DELAY` environment variable. + +```bash +export INSTANA_METRICS_TRANSMISSION_DELAY=2000 +go run example/metrics-interval/env-config/main.go +``` + +**Advantages:** +- No code changes required +- Easy to adjust per environment (dev, staging, prod) +- Takes precedence over code configuration + +### 2. Code Configuration (code-config/) + +Configure the interval programmatically in your application code. + +```go +instana.InitCollector(&instana.Options{ + Service: "my-service", + Metrics: instana.MetricsOptions{ + TransmissionDelay: 3000, // 3 seconds + }, +}) +``` + +**Advantages:** +- Explicit and visible in code +- Can be set conditionally based on application logic +- Type-safe configuration + +## Configuration Rules + +- **Valid Range**: 1000ms to 5000ms +- **Default**: 1000ms (if not specified or invalid) +- **Maximum Cap**: Values above 5000ms are automatically capped at 5000ms +- **Invalid Values**: Non-numeric, zero, or negative values fall back to default 1000ms +- **Precedence**: Environment variable > Code configuration > Default + +## Running the Examples + +### Environment Variable Example +environment variable is set programmatically in the code. +```bash +cd example/metrics-interval/env-config +go run main.go +``` + +### Code Configuration Example +```bash +cd example/metrics-interval/code-config +go run main.go +``` + +## Validation and Error Handling + +The sensor validates all configuration values and provides warning logs for invalid inputs: + +- **Invalid format**: Falls back to default 1000ms with warning +- **Out of range**: Caps at 5000ms or uses default for values ≤ 1000 +- **Graceful degradation**: Application continues running with safe defaults + +## See Also + +- [Main Documentation](../../README.md) +- [Options Documentation](../../docs/options.md) +- [Instana Go Sensor API](https://pkg.go.dev/github.com/instana/go-sensor) \ No newline at end of file diff --git a/example/metrics-interval/code-config/main.go b/example/metrics-interval/code-config/main.go new file mode 100644 index 000000000..3990cba75 --- /dev/null +++ b/example/metrics-interval/code-config/main.go @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2026 IBM Corp. +// +// SPDX-License-Identifier: MIT + +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "os/signal" + "time" + + instana "github.com/instana/go-sensor" +) + +func main() { + fmt.Println("=== Instana Metrics Transmission Interval - Code Configuration ===") + fmt.Println() + + // Example 1: Configure metrics transmission interval via code + fmt.Println("Example 1: Setting custom interval to 3000ms (3 seconds)") + + opts := &instana.Options{ + Service: "metrics-interval-code-example-2", + Metrics: instana.MetricsOptions{ + TransmissionDelay: 3000, // 3000 milliseconds = 3 seconds + }, + } + + col := instana.InitCollector(opts) + + fmt.Println("✓ Instana collector initialized") + fmt.Println("✓ Metrics will be transmitted every 3000ms") + fmt.Println() + + // Example 2: Different configurations + fmt.Println("Other configuration examples:") + fmt.Println() + + // Fast interval for high-frequency monitoring + fmt.Println(" Fast interval (500ms):") + fmt.Println(" Metrics: instana.MetricsOptions{") + fmt.Println(" TransmissionDelay: 500,") + fmt.Println(" }") + fmt.Println() + + // Slow interval for resource-constrained environments + fmt.Println(" Slow interval (5000ms - maximum):") + fmt.Println(" Metrics: instana.MetricsOptions{") + fmt.Println(" TransmissionDelay: 5000,") + fmt.Println(" }") + fmt.Println() + + // Default behavior + fmt.Println(" Default interval (1000ms):") + fmt.Println(" Metrics: instana.MetricsOptions{") + fmt.Println(" TransmissionDelay: 0, // or omit the field") + fmt.Println(" }") + fmt.Println() + + fmt.Println("Configuration rules:") + fmt.Println(" - Valid range: 1ms to 5000ms") + fmt.Println(" - Values above 5000ms are automatically capped at 5000ms") + fmt.Println(" - Zero or negative values use default 1000ms") + fmt.Println(" - Environment variable INSTANA_METRICS_TRANSMISSION_DELAY takes precedence") + fmt.Println() + + // Simulate application running + go func() { + http.HandleFunc("/endpoint", instana.TracingHandlerFunc(col, "/endpoint", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + log.Fatal(http.ListenAndServe(":7070", nil)) + }() + + go func() { + ticker := time.NewTicker(5 * time.Second) + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + for range ticker.C { + url := "http://localhost:7070/endpoint" + // Create request + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + // Send request + _, err = client.Do(req) + if err != nil { + fmt.Println("Error making request:", err) + return + } + } + }() + + fmt.Println("Please go to the Instana UI to see metrics") + fmt.Println("Application running... (press Ctrl+C to exit)") + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + <-stop + fmt.Println("Application stopped.") +} diff --git a/example/metrics-interval/env-config/main.go b/example/metrics-interval/env-config/main.go new file mode 100644 index 000000000..139e1cb04 --- /dev/null +++ b/example/metrics-interval/env-config/main.go @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2026 IBM Corp. +// +// SPDX-License-Identifier: MIT + +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "os/signal" + "time" + + instana "github.com/instana/go-sensor" +) + +func main() { + // Example: Configure metrics transmission interval via environment variable + // Set INSTANA_METRICS_TRANSMISSION_DELAY before starting the application + + // For demonstration, we'll set it programmatically here + // In production, set this via your deployment configuration + os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", "2000") + + fmt.Println("=== Instana Metrics Transmission Interval - ENV Configuration ===") + fmt.Println() + fmt.Println("Environment variable INSTANA_METRICS_TRANSMISSION_DELAY=2000") + fmt.Println("This will configure metrics to be transmitted every 2000ms (2 seconds)") + fmt.Println() + + // Initialize the Instana collector with default options + // The environment variable will be automatically applied + col := instana.InitCollector(&instana.Options{ + Service: "metrics-interval-env-example", + }) + + fmt.Println("✓ Instana collector initialized") + fmt.Println("✓ Metrics will be transmitted every 2000ms") + fmt.Println() + fmt.Println("Valid values:") + fmt.Println(" - Minimum: 1ms") + fmt.Println(" - Maximum: 5000ms (values above will be capped)") + fmt.Println(" - Default: 1000ms (if not specified or invalid)") + fmt.Println() + fmt.Println("Invalid values (non-numeric, negative, zero) will fall back to default 1000ms") + fmt.Println() + + // Simulate application running + go func() { + http.HandleFunc("/endpoint", instana.TracingHandlerFunc(col, "/endpoint", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + log.Fatal(http.ListenAndServe(":7070", nil)) + }() + + go func() { + ticker := time.NewTicker(5 * time.Second) + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + for range ticker.C { + url := "http://localhost:7070/endpoint" + // Create request + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + // Send request + _, err = client.Do(req) + if err != nil { + fmt.Println("Error making request:", err) + return + } + } + }() + + fmt.Println("Please go to the Instana UI to see metrics") + fmt.Println("Application running... (press Ctrl+C to exit)") + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + <-stop + fmt.Println("Application stopped.") +} From 7bfc7710f7c373628d776ca35cbb85cb0d41e94f Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Tue, 21 Apr 2026 13:11:22 +0530 Subject: [PATCH 04/21] fix: use constanats for min, max, default transmission intervals --- meter.go | 9 ++++++++- options.go | 14 +++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/meter.go b/meter.go index 7dd476125..9b325c97d 100644 --- a/meter.go +++ b/meter.go @@ -52,6 +52,12 @@ type MetricsOptions struct { TransmissionDelay int } +const ( + defaultTransmissionDelay = 1000 + maxTransmissionDelay = 5000 + minTransmissionDelay = 1000 +) + func newMeter(logger LeveledLogger) *meterS { logger.Debug("initializing meter") @@ -92,7 +98,8 @@ func getTransmissionDelay(options *Options) time.Duration { // Safety check: fallback to default if interval becomes negative, // possibly due to missing TransmissionDelay during sensor re-initialization. if interval <= 0 { - interval = 1000 * time.Millisecond + defaultLogger.Warn("meter: safety check triggered. invalid transmission delay %d, falling back to default", options.Metrics.TransmissionDelay) + interval = defaultTransmissionDelay * time.Millisecond } return interval } diff --git a/options.go b/options.go index c454cc0a3..5e5666f2e 100644 --- a/options.go +++ b/options.go @@ -131,7 +131,7 @@ func (opts *Options) applyProfilingConfiguration() { func (opts *Options) applyMetricsConfiguration() { // Step 1: Apply default if zero if opts.Metrics.TransmissionDelay == 0 { - opts.Metrics.TransmissionDelay = 1000 + opts.Metrics.TransmissionDelay = defaultTransmissionDelay } // Step 2: Check environment variable (takes precedence over code configuration) @@ -139,11 +139,11 @@ func (opts *Options) applyMetricsConfiguration() { if delay, err := strconv.Atoi(envDelay); err != nil { // Invalid format - non-numeric value defaultLogger.Warn("Invalid INSTANA_METRICS_TRANSMISSION_DELAY value: ", envDelay, ", using default 1000ms") - opts.Metrics.TransmissionDelay = 1000 + opts.Metrics.TransmissionDelay = defaultTransmissionDelay } else if delay <= 0 { // Non-positive value defaultLogger.Warn("INSTANA_METRICS_TRANSMISSION_DELAY must be positive, using default 1000ms") - opts.Metrics.TransmissionDelay = 1000 + opts.Metrics.TransmissionDelay = defaultTransmissionDelay } else { // Valid value from ENV opts.Metrics.TransmissionDelay = delay @@ -151,15 +151,15 @@ func (opts *Options) applyMetricsConfiguration() { } // Step 3: Enforce minimum value (1000ms) - if opts.Metrics.TransmissionDelay < 1000 { + if opts.Metrics.TransmissionDelay < minTransmissionDelay { defaultLogger.Warn("metrics transmission delay is below minimum 1000ms, using minimum 1000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") - opts.Metrics.TransmissionDelay = 1000 + opts.Metrics.TransmissionDelay = minTransmissionDelay } // Step 4: Enforce maximum cap (5000ms) - if opts.Metrics.TransmissionDelay > 5000 { + if opts.Metrics.TransmissionDelay > maxTransmissionDelay { defaultLogger.Warn("metrics transmission delay exceeds maximum 5000ms, capping at 5000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") - opts.Metrics.TransmissionDelay = 5000 + opts.Metrics.TransmissionDelay = maxTransmissionDelay } } From e234dada6e997eb34f56c1e7cd047424d67fd55a Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 4 May 2026 14:59:47 +0530 Subject: [PATCH 05/21] feat: make poll rate retrieval from agent and metrics transmission configurable --- agent.go | 3 + fsm.go | 16 +++ meter.go | 74 +++++----- meter_test.go | 54 ++++++++ options.go | 41 +----- options_test.go | 350 ------------------------------------------------ sensor.go | 6 +- 7 files changed, 115 insertions(+), 429 deletions(-) diff --git a/agent.go b/agent.go index 84fbcbb0a..1eb99707f 100644 --- a/agent.go +++ b/agent.go @@ -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 from agent configuration + } `json:"plugin.golang"` } func (a *agentResponse) getExtraHTTPHeaders() []string { diff --git a/fsm.go b/fsm.go index 4601028ad..25eaf6da1 100644 --- a/fsm.go +++ b/fsm.go @@ -268,10 +268,25 @@ 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. +// The poll rate is configured in the agent's configuration.yaml under com.instana.plugin.golang.poll_rate +func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { + // 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") + sensor.options.Metrics.setTransmissionInterval(1) + return + } + + r.logger.Debug("Applying metrics poll_rate configuration from agent: ", resp.PluginConfig.PollRate, " second(s)") + sensor.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 { @@ -420,6 +435,7 @@ func (r *fsmS) reset() { func (r *fsmS) ready(_ context.Context, e *f.Event) { go delayed.flush() + go sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) } func (r *fsmS) cpuSetFileContent(pid int) string { diff --git a/meter.go b/meter.go index 9b325c97d..3d4b0eafe 100644 --- a/meter.go +++ b/meter.go @@ -4,7 +4,9 @@ package instana import ( + "fmt" "runtime" + "sync" "time" "github.com/instana/go-sensor/acceptor" @@ -27,36 +29,42 @@ type meterS struct { done chan struct{} } -// MetricsOptions contains configuration for metrics collection and transmission +// MetricsOptions contains configuration for metrics collection and transmission. +// This configuration is managed internally and populated from agent configuration. type MetricsOptions struct { - // TransmissionDelay specifies the interval in milliseconds between metrics transmissions - // to the Instana agent. - // - // Default: 1000 (1 second) - // Minimum: 1000 (enforced via validation, values < 1000 use default) - // Maximum: 5000 (5 seconds, values above are capped with warning) - // - // This value can be configured via: - // - Environment variable: INSTANA_METRICS_TRANSMISSION_DELAY - // - Code: opts.Metrics.TransmissionDelay = 2000 - // - // Configuration precedence: ENV > code > default - // - // Example: - // opts := &instana.Options{ - // Service: "MyApp", - // Metrics: instana.MetricsOptions{ - // TransmissionDelay: 2000, // 2 seconds - // }, - // } - TransmissionDelay int + mu sync.RWMutex + transmissionInterval time.Duration } -const ( - defaultTransmissionDelay = 1000 - maxTransmissionDelay = 5000 - minTransmissionDelay = 1000 -) +// GetTransmissionInterval returns the current metrics transmission interval. +// This value is configured through the agent's configuration.yaml file. +func (m *MetricsOptions) getTransmissionInterval() time.Duration { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.transmissionInterval == 0 { + return 1 * time.Second // default + } + return m.transmissionInterval +} + +// setTransmissionInterval sets the metrics transmission interval. +// This is an internal method called when agent configuration is received. +// Only 1 or 5 seconds are valid values; others default to 1 second. +func (m *MetricsOptions) setTransmissionInterval(seconds int) { + m.mu.Lock() + defer m.mu.Unlock() + + // Validate: only 1 or 5 seconds allowed + if seconds != 1 && seconds != 5 { + defaultLogger.Warn("Invalid poll_rate value from agent: ", seconds, ", using default 1 second. Valid values are 1 or 5.") + m.transmissionInterval = 1 * time.Second + return + } + + m.transmissionInterval = time.Duration(seconds) * time.Second + defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") +} func newMeter(logger LeveledLogger) *meterS { logger.Debug("initializing meter") @@ -67,6 +75,7 @@ func newMeter(logger LeveledLogger) *meterS { } func (m *meterS) Run(collectInterval time.Duration) { + fmt.Println("collectInterval: ", collectInterval) ticker := time.NewTicker(collectInterval) defer ticker.Stop() for { @@ -93,17 +102,6 @@ func (m *meterS) Stop() { m.done <- struct{}{} } -func getTransmissionDelay(options *Options) time.Duration { - interval := time.Duration(options.Metrics.TransmissionDelay) * time.Millisecond - // Safety check: fallback to default if interval becomes negative, - // possibly due to missing TransmissionDelay during sensor re-initialization. - if interval <= 0 { - defaultLogger.Warn("meter: safety check triggered. invalid transmission delay %d, falling back to default", options.Metrics.TransmissionDelay) - interval = defaultTransmissionDelay * time.Millisecond - } - return interval -} - func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) diff --git a/meter_test.go b/meter_test.go index edcbdb1d0..b05db92c3 100644 --- a/meter_test.go +++ b/meter_test.go @@ -7,6 +7,8 @@ import ( "sync" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestMeterS_Stop(t *testing.T) { @@ -130,3 +132,55 @@ func TestMeterS_NewMeter(t *testing.T) { t.Errorf("Expected initial numGC to be 0, got %d", m.numGC) } } + +func TestMetricsOptions_GetTransmissionInterval_Default(t *testing.T) { + opts := &MetricsOptions{} + + interval := opts.getTransmissionInterval() + + assert.Equal(t, 1*time.Second, interval, "Default transmission interval should be 1 second") +} + +func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { + tests := []struct { + name string + seconds int + expected time.Duration + }{ + { + name: "Valid 1 second", + seconds: 1, + expected: 1 * time.Second, + }, + { + name: "Valid 5 seconds", + seconds: 5, + expected: 5 * time.Second, + }, + { + name: "Invalid 0 seconds defaults to 1 second", + seconds: 0, + expected: 1 * time.Second, + }, + { + name: "Invalid 2 seconds defaults to 1 second", + seconds: 2, + expected: 1 * time.Second, + }, + { + name: "Invalid negative defaults to 1 second", + seconds: -1, + expected: 1 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &MetricsOptions{} + + opts.setTransmissionInterval(tt.seconds) + + assert.Equal(t, tt.expected, opts.getTransmissionInterval()) + }) + } +} diff --git a/options.go b/options.go index 5e5666f2e..60e2c0990 100644 --- a/options.go +++ b/options.go @@ -40,7 +40,8 @@ type Options struct { MaxBufferedProfiles int // IncludeProfilerFrames is whether to include profiler calls into the profile or not IncludeProfilerFrames bool - // Metrics contains metrics collection and transmission configuration + // Metrics contains metrics collection and transmission configuration. + // This is managed internally and populated from agent configuration. Metrics MetricsOptions // Tracer contains tracer-specific configuration used by all tracers Tracer TracerOptions @@ -72,7 +73,6 @@ func (opts *Options) applyConfiguration() { opts.applyAgentConfiguration() opts.applyServiceConfiguration() opts.applyProfilingConfiguration() - opts.applyMetricsConfiguration() opts.applyTracerConfiguration() } @@ -126,43 +126,6 @@ func (opts *Options) applyProfilingConfiguration() { } } -// applyMetricsConfiguration resolves metrics collection and transmission settings -// Precedence: ENV > in-code > default -func (opts *Options) applyMetricsConfiguration() { - // Step 1: Apply default if zero - if opts.Metrics.TransmissionDelay == 0 { - opts.Metrics.TransmissionDelay = defaultTransmissionDelay - } - - // Step 2: Check environment variable (takes precedence over code configuration) - if envDelay := os.Getenv("INSTANA_METRICS_TRANSMISSION_DELAY"); envDelay != "" { - if delay, err := strconv.Atoi(envDelay); err != nil { - // Invalid format - non-numeric value - defaultLogger.Warn("Invalid INSTANA_METRICS_TRANSMISSION_DELAY value: ", envDelay, ", using default 1000ms") - opts.Metrics.TransmissionDelay = defaultTransmissionDelay - } else if delay <= 0 { - // Non-positive value - defaultLogger.Warn("INSTANA_METRICS_TRANSMISSION_DELAY must be positive, using default 1000ms") - opts.Metrics.TransmissionDelay = defaultTransmissionDelay - } else { - // Valid value from ENV - opts.Metrics.TransmissionDelay = delay - } - } - - // Step 3: Enforce minimum value (1000ms) - if opts.Metrics.TransmissionDelay < minTransmissionDelay { - defaultLogger.Warn("metrics transmission delay is below minimum 1000ms, using minimum 1000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") - opts.Metrics.TransmissionDelay = minTransmissionDelay - } - - // Step 4: Enforce maximum cap (5000ms) - if opts.Metrics.TransmissionDelay > maxTransmissionDelay { - defaultLogger.Warn("metrics transmission delay exceeds maximum 5000ms, capping at 5000ms (configured: ", opts.Metrics.TransmissionDelay, "ms)") - opts.Metrics.TransmissionDelay = maxTransmissionDelay - } -} - // applyTracerConfiguration resolves tracer-specific settings // Precedence: ENV > in-code > agent config > default func (opts *Options) applyTracerConfiguration() { diff --git a/options_test.go b/options_test.go index 2518077fc..2488dd0ba 100644 --- a/options_test.go +++ b/options_test.go @@ -896,353 +896,3 @@ func TestApplyConfiguration_DefaultsOnly(t *testing.T) { assert.Nil(t, opts.Tracer.CollectableHTTPHeaders) assert.Nil(t, opts.Tracer.DisableSpans) } - -// TestApplyMetricsConfiguration_Default tests default metrics transmission delay -func TestApplyMetricsConfiguration_Default(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - - opts := &Options{} - opts.applyMetricsConfiguration() - - assert.Equal(t, 1000, opts.Metrics.TransmissionDelay, "Default should be 1000ms") -} - -// TestApplyMetricsConfiguration_ValidENV tests valid ENV override -func TestApplyMetricsConfiguration_ENVValidation(t *testing.T) { - tests := []struct { - name string - envValue string - expected int - }{ - // Valid values - { - name: "Valid 1000ms (min boundary)", - envValue: "1000", - expected: 1000, - }, - { - name: "Valid 2000ms (mid-range)", - envValue: "2000", - expected: 2000, - }, - { - name: "Valid 5000ms (max boundary)", - envValue: "5000", - expected: 5000, - }, - // Invalid values - fall back to default - { - name: "Non-numeric value", - envValue: "invalid", - expected: 1000, - }, - { - name: "Empty string", - envValue: "", - expected: 1000, - }, - { - name: "Float value", - envValue: "1500.5", - expected: 1000, - }, - // Below minimum - enforced to minimum - { - name: "Zero value", - envValue: "0", - expected: 1000, - }, - { - name: "Negative value", - envValue: "-500", - expected: 1000, - }, - { - name: "Below minimum (999ms)", - envValue: "999", - expected: 1000, - }, - // Above maximum - capped at maximum - { - name: "Above maximum (6000ms)", - envValue: "6000", - expected: 5000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - - if tt.envValue != "" { - os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) - } else { - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - } - - opts := &Options{} - opts.applyMetricsConfiguration() - - assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay) - }) - } -} - -// TestApplyMetricsConfiguration_ENVPrecedence tests ENV overrides code configuration -func TestApplyMetricsConfiguration_ENVPrecedence(t *testing.T) { - tests := []struct { - name string - inCodeValue int - envValue string - expectedCode int - expectedENV int - }{ - { - name: "ENV overrides in-code", - inCodeValue: 2000, - envValue: "3000", - expectedCode: 2000, // Without ENV - expectedENV: 3000, // With ENV - }, - { - name: "ENV overrides default", - inCodeValue: 0, // Will use default 1000 - envValue: "2500", - expectedCode: 1000, // Default - expectedENV: 2500, // ENV override - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - - // Test without ENV - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - opts := &Options{ - Metrics: MetricsOptions{ - TransmissionDelay: tt.inCodeValue, - }, - } - opts.applyMetricsConfiguration() - assert.Equal(t, tt.expectedCode, opts.Metrics.TransmissionDelay, "In-code value should be used without ENV") - - // Test with ENV - os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) - opts = &Options{ - Metrics: MetricsOptions{ - TransmissionDelay: tt.inCodeValue, - }, - } - opts.applyMetricsConfiguration() - assert.Equal(t, tt.expectedENV, opts.Metrics.TransmissionDelay, "ENV should override in-code value") - }) - } -} - -// TestApplyMetricsConfiguration_CodeConfiguration tests code-based configuration -func TestApplyMetricsConfiguration_CodeConfiguration(t *testing.T) { - tests := []struct { - name string - inCodeValue int - expected int - description string - }{ - { - name: "Valid code value 2000ms", - inCodeValue: 2000, - expected: 2000, - description: "Should use in-code value", - }, - { - name: "Code value below minimum (500ms)", - inCodeValue: 500, - expected: 1000, - description: "Should enforce minimum 1000ms", - }, - { - name: "Code value at max 5000ms", - inCodeValue: 5000, - expected: 5000, - description: "Should use max value", - }, - { - name: "Code value above max", - inCodeValue: 6000, - expected: 5000, - description: "Should cap at 5000ms", - }, - { - name: "Zero value uses default", - inCodeValue: 0, - expected: 1000, - description: "Should use default 1000ms", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - - opts := &Options{ - Metrics: MetricsOptions{ - TransmissionDelay: tt.inCodeValue, - }, - } - opts.applyMetricsConfiguration() - - assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay, tt.description) - }) - } -} - -// TestApplyMetricsConfiguration_BackwardCompatibility tests backward compatibility -func TestApplyMetricsConfiguration_BackwardCompatibility(t *testing.T) { - tests := []struct { - name string - setupOpts func() *Options - expected int - description string - }{ - { - name: "Empty Options struct", - setupOpts: func() *Options { - return &Options{} - }, - expected: 1000, - description: "Should use default 1000ms", - }, - { - name: "Options with other fields set", - setupOpts: func() *Options { - return &Options{ - AgentHost: "localhost", - AgentPort: 42699, - Service: "test-service", - } - }, - expected: 1000, - description: "Should use default 1000ms when Metrics not set", - }, - { - name: "DefaultOptions()", - setupOpts: func() *Options { - return DefaultOptions() - }, - expected: 1000, - description: "DefaultOptions should result in 1000ms", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - - opts := tt.setupOpts() - opts.applyMetricsConfiguration() - - assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay, tt.description) - }) - } -} - -// BenchmarkApplyMetricsConfiguration benchmarks the configuration overhead -func BenchmarkApplyMetricsConfiguration(b *testing.B) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - - benchmarks := []struct { - name string - envValue string - }{ - { - name: "Default (no ENV)", - envValue: "", - }, - { - name: "Valid ENV", - envValue: "2000", - }, - { - name: "Invalid ENV", - envValue: "invalid", - }, - } - - for _, bm := range benchmarks { - b.Run(bm.name, func(b *testing.B) { - if bm.envValue != "" { - os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", bm.envValue) - } else { - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - opts := &Options{} - opts.applyMetricsConfiguration() - } - }) - } -} - -// TestApplyConfiguration_WithMetrics tests full integration with applyConfiguration -func TestApplyConfiguration_WithMetrics(t *testing.T) { - restore := restoreEnvVarFunc("INSTANA_METRICS_TRANSMISSION_DELAY") - defer restore() - - tests := []struct { - name string - envValue string - inCodeValue int - expected int - }{ - { - name: "Full integration - ENV override", - envValue: "3000", - inCodeValue: 2000, - expected: 3000, - }, - { - name: "Full integration - code only", - envValue: "", - inCodeValue: 2500, - expected: 2500, - }, - { - name: "Full integration - default", - envValue: "", - inCodeValue: 0, - expected: 1000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.envValue != "" { - os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", tt.envValue) - } else { - os.Unsetenv("INSTANA_METRICS_TRANSMISSION_DELAY") - } - - opts := &Options{ - Metrics: MetricsOptions{ - TransmissionDelay: tt.inCodeValue, - }, - } - - // Call the full applyConfiguration which should call applyMetricsConfiguration - opts.applyConfiguration() - - assert.Equal(t, tt.expected, opts.Metrics.TransmissionDelay) - }) - } -} diff --git a/sensor.go b/sensor.go index 997de3d81..ad49625af 100644 --- a/sensor.go +++ b/sensor.go @@ -142,12 +142,12 @@ func newSensor(options *Options) *sensorS { agent = newServerlessAgent(s.serviceOrBinaryName(), agentEndpoint, os.Getenv("INSTANA_AGENT_KEY"), client, s.logger) } + s.meter = newMeter(s.logger) if agent == nil { agent = newAgent(s.serviceOrBinaryName(), s.options.AgentHost, s.options.AgentPort, s.logger) } s.setAgent(agent) - s.meter = newMeter(s.logger) return s } @@ -225,7 +225,9 @@ func InitSensor(options *Options) { configureAutoProfiling(options) // start collecting metrics - go sensor.meter.Run(getTransmissionDelay(options)) + // Interval is configured via agent's configuration.yaml (com.instana.plugin.golang.poll_rate) + // Default is 1 second if not configured + // go sensor.meter.Run(options.Metrics.GetTransmissionInterval()) sensor.logger.Debug("initialized Instana sensor v", Version) } From bd8f1e2968c6976f2847b9fe2755bbe1b10e29ff Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 4 May 2026 19:58:56 +0530 Subject: [PATCH 06/21] feat: handle metrics collection logic on agent reset --- README.md | 40 ++- example/metrics-interval/README.md | 78 ----- example/metrics-interval/code-config/main.go | 110 -------- example/metrics-interval/env-config/main.go | 88 ------ fsm.go | 8 +- fsm_test.go | 74 +++++ meter.go | 98 +++++-- meter_test.go | 281 ++++++++++++++++++- 8 files changed, 445 insertions(+), 332 deletions(-) delete mode 100644 example/metrics-interval/README.md delete mode 100644 example/metrics-interval/code-config/main.go delete mode 100644 example/metrics-interval/env-config/main.go diff --git a/README.md b/README.md index be26a4374..1ddc51deb 100644 --- a/README.md +++ b/README.md @@ -67,32 +67,30 @@ func init() { 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. -#### Configuring Metrics Transmission Interval +#### Metrics Transmission Interval -By default, metrics are transmitted to the Instana Agent every 1000ms (1 second). You can customize this interval to suit your application's needs: +Metrics are transmitted to the Instana Agent at a configurable interval. The interval is configured through the agent's `configuration.yaml` file. -**Via Environment Variable:** -```bash -export INSTANA_METRICS_TRANSMISSION_DELAY=2000 # 2 seconds -``` +**Configuration:** -**Via Code Configuration:** -```go -col = instana.InitCollector(&instana.Options{ - Service: "My app", - Metrics: instana.MetricsOptions{ - TransmissionDelay: 3000, // 3 seconds - }, -}) +In the agent's `configuration.yaml`: +```yaml +# Configure metrics transmission interval for Go applications +com.instana.plugin.golang: + poll_rate: 5 # Valid range: 1-3600 (seconds) ``` -**Configuration Rules:** -- Valid range: 1000ms to 5000ms -- Default: 1000ms (if not specified or invalid) -- Values above 5000ms are automatically capped -- Environment variable takes precedence over code configuration - -For detailed examples and use cases, see [example/metrics-interval/](example/metrics-interval/). +**Valid Values:** +- Any value between `1` and `3600` seconds +- Default: `1` second (if not configured) +- Minimum: `1` second +- Maximum: `3600` seconds (1 hour) + +**Behavior:** +- If `poll_rate` is not configured, defaults to 1 second +- Values less than 1 will be set to the minimum value of 1 second. +- Values greater than 3600 will be set to the maximum value of 3600 seconds. +- Configuration is applied when the Go sensor announces itself to the agent. This data is then already available in the dashboard. diff --git a/example/metrics-interval/README.md b/example/metrics-interval/README.md deleted file mode 100644 index 128f92611..000000000 --- a/example/metrics-interval/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# Configurable Metrics Transmission Interval Examples - -This directory contains examples demonstrating how to configure the metrics transmission interval in the Instana Go Sensor. - -## Overview - -The Instana Go Sensor allows you to configure how frequently metrics are transmitted to the Instana agent. By default, metrics are sent every 1000ms (1 second), but this can be customized based on your application's needs. - -## Configuration Methods - -### 1. Environment Variable (env-config/) - -Configure the interval using the `INSTANA_METRICS_TRANSMISSION_DELAY` environment variable. - -```bash -export INSTANA_METRICS_TRANSMISSION_DELAY=2000 -go run example/metrics-interval/env-config/main.go -``` - -**Advantages:** -- No code changes required -- Easy to adjust per environment (dev, staging, prod) -- Takes precedence over code configuration - -### 2. Code Configuration (code-config/) - -Configure the interval programmatically in your application code. - -```go -instana.InitCollector(&instana.Options{ - Service: "my-service", - Metrics: instana.MetricsOptions{ - TransmissionDelay: 3000, // 3 seconds - }, -}) -``` - -**Advantages:** -- Explicit and visible in code -- Can be set conditionally based on application logic -- Type-safe configuration - -## Configuration Rules - -- **Valid Range**: 1000ms to 5000ms -- **Default**: 1000ms (if not specified or invalid) -- **Maximum Cap**: Values above 5000ms are automatically capped at 5000ms -- **Invalid Values**: Non-numeric, zero, or negative values fall back to default 1000ms -- **Precedence**: Environment variable > Code configuration > Default - -## Running the Examples - -### Environment Variable Example -environment variable is set programmatically in the code. -```bash -cd example/metrics-interval/env-config -go run main.go -``` - -### Code Configuration Example -```bash -cd example/metrics-interval/code-config -go run main.go -``` - -## Validation and Error Handling - -The sensor validates all configuration values and provides warning logs for invalid inputs: - -- **Invalid format**: Falls back to default 1000ms with warning -- **Out of range**: Caps at 5000ms or uses default for values ≤ 1000 -- **Graceful degradation**: Application continues running with safe defaults - -## See Also - -- [Main Documentation](../../README.md) -- [Options Documentation](../../docs/options.md) -- [Instana Go Sensor API](https://pkg.go.dev/github.com/instana/go-sensor) \ No newline at end of file diff --git a/example/metrics-interval/code-config/main.go b/example/metrics-interval/code-config/main.go deleted file mode 100644 index 3990cba75..000000000 --- a/example/metrics-interval/code-config/main.go +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: 2026 IBM Corp. -// -// SPDX-License-Identifier: MIT - -package main - -import ( - "fmt" - "log" - "net/http" - "os" - "os/signal" - "time" - - instana "github.com/instana/go-sensor" -) - -func main() { - fmt.Println("=== Instana Metrics Transmission Interval - Code Configuration ===") - fmt.Println() - - // Example 1: Configure metrics transmission interval via code - fmt.Println("Example 1: Setting custom interval to 3000ms (3 seconds)") - - opts := &instana.Options{ - Service: "metrics-interval-code-example-2", - Metrics: instana.MetricsOptions{ - TransmissionDelay: 3000, // 3000 milliseconds = 3 seconds - }, - } - - col := instana.InitCollector(opts) - - fmt.Println("✓ Instana collector initialized") - fmt.Println("✓ Metrics will be transmitted every 3000ms") - fmt.Println() - - // Example 2: Different configurations - fmt.Println("Other configuration examples:") - fmt.Println() - - // Fast interval for high-frequency monitoring - fmt.Println(" Fast interval (500ms):") - fmt.Println(" Metrics: instana.MetricsOptions{") - fmt.Println(" TransmissionDelay: 500,") - fmt.Println(" }") - fmt.Println() - - // Slow interval for resource-constrained environments - fmt.Println(" Slow interval (5000ms - maximum):") - fmt.Println(" Metrics: instana.MetricsOptions{") - fmt.Println(" TransmissionDelay: 5000,") - fmt.Println(" }") - fmt.Println() - - // Default behavior - fmt.Println(" Default interval (1000ms):") - fmt.Println(" Metrics: instana.MetricsOptions{") - fmt.Println(" TransmissionDelay: 0, // or omit the field") - fmt.Println(" }") - fmt.Println() - - fmt.Println("Configuration rules:") - fmt.Println(" - Valid range: 1ms to 5000ms") - fmt.Println(" - Values above 5000ms are automatically capped at 5000ms") - fmt.Println(" - Zero or negative values use default 1000ms") - fmt.Println(" - Environment variable INSTANA_METRICS_TRANSMISSION_DELAY takes precedence") - fmt.Println() - - // Simulate application running - go func() { - http.HandleFunc("/endpoint", instana.TracingHandlerFunc(col, "/endpoint", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - log.Fatal(http.ListenAndServe(":7070", nil)) - }() - - go func() { - ticker := time.NewTicker(5 * time.Second) - - client := &http.Client{ - Timeout: 10 * time.Second, - } - - for range ticker.C { - url := "http://localhost:7070/endpoint" - // Create request - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - fmt.Println("Error creating request:", err) - return - } - // Send request - _, err = client.Do(req) - if err != nil { - fmt.Println("Error making request:", err) - return - } - } - }() - - fmt.Println("Please go to the Instana UI to see metrics") - fmt.Println("Application running... (press Ctrl+C to exit)") - - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt) - <-stop - fmt.Println("Application stopped.") -} diff --git a/example/metrics-interval/env-config/main.go b/example/metrics-interval/env-config/main.go deleted file mode 100644 index 139e1cb04..000000000 --- a/example/metrics-interval/env-config/main.go +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2026 IBM Corp. -// -// SPDX-License-Identifier: MIT - -package main - -import ( - "fmt" - "log" - "net/http" - "os" - "os/signal" - "time" - - instana "github.com/instana/go-sensor" -) - -func main() { - // Example: Configure metrics transmission interval via environment variable - // Set INSTANA_METRICS_TRANSMISSION_DELAY before starting the application - - // For demonstration, we'll set it programmatically here - // In production, set this via your deployment configuration - os.Setenv("INSTANA_METRICS_TRANSMISSION_DELAY", "2000") - - fmt.Println("=== Instana Metrics Transmission Interval - ENV Configuration ===") - fmt.Println() - fmt.Println("Environment variable INSTANA_METRICS_TRANSMISSION_DELAY=2000") - fmt.Println("This will configure metrics to be transmitted every 2000ms (2 seconds)") - fmt.Println() - - // Initialize the Instana collector with default options - // The environment variable will be automatically applied - col := instana.InitCollector(&instana.Options{ - Service: "metrics-interval-env-example", - }) - - fmt.Println("✓ Instana collector initialized") - fmt.Println("✓ Metrics will be transmitted every 2000ms") - fmt.Println() - fmt.Println("Valid values:") - fmt.Println(" - Minimum: 1ms") - fmt.Println(" - Maximum: 5000ms (values above will be capped)") - fmt.Println(" - Default: 1000ms (if not specified or invalid)") - fmt.Println() - fmt.Println("Invalid values (non-numeric, negative, zero) will fall back to default 1000ms") - fmt.Println() - - // Simulate application running - go func() { - http.HandleFunc("/endpoint", instana.TracingHandlerFunc(col, "/endpoint", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - log.Fatal(http.ListenAndServe(":7070", nil)) - }() - - go func() { - ticker := time.NewTicker(5 * time.Second) - - client := &http.Client{ - Timeout: 10 * time.Second, - } - - for range ticker.C { - url := "http://localhost:7070/endpoint" - // Create request - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - fmt.Println("Error creating request:", err) - return - } - // Send request - _, err = client.Do(req) - if err != nil { - fmt.Println("Error making request:", err) - return - } - } - }() - - fmt.Println("Please go to the Instana UI to see metrics") - fmt.Println("Application running... (press Ctrl+C to exit)") - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt) - <-stop - fmt.Println("Application stopped.") -} diff --git a/fsm.go b/fsm.go index 25eaf6da1..f78482ded 100644 --- a/fsm.go +++ b/fsm.go @@ -276,6 +276,12 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { // applyMetricsPollRateConfig applies the metrics poll rate configuration from agent response. // The poll rate is configured in the agent's configuration.yaml under com.instana.plugin.golang.poll_rate func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { + // Check if sensor is initialized + if sensor == nil || sensor.options == 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") @@ -435,7 +441,7 @@ func (r *fsmS) reset() { func (r *fsmS) ready(_ context.Context, e *f.Event) { go delayed.flush() - go sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) + go sensor.meter.reset(sensor.options.Metrics.getTransmissionInterval()) } func (r *fsmS) cpuSetFileContent(pid int) string { diff --git a/fsm_test.go b/fsm_test.go index 6c8e69581..a293597ae 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -635,3 +635,77 @@ func TestApplyDisableTracingConfig(t *testing.T) { }) } } + +func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { + tests := []struct { + name string + pollRate int + expectedSecs int + }{ + { + name: "Valid 1 second (minimum)", + pollRate: 1, + expectedSecs: 1, + }, + { + name: "Valid 5 seconds", + pollRate: 5, + expectedSecs: 5, + }, + { + name: "Valid 10 seconds", + pollRate: 10, + expectedSecs: 10, + }, + { + name: "Valid 60 seconds", + pollRate: 60, + expectedSecs: 60, + }, + { + name: "Valid 3600 seconds (maximum)", + pollRate: 3600, + expectedSecs: 3600, + }, + { + name: "Zero seconds - sets to minimum (1)", + pollRate: 0, + expectedSecs: 1, + }, + { + name: "Negative value - sets to minimum (1)", + pollRate: -5, + expectedSecs: 1, + }, + { + name: "Exceeds maximum (5000) - sets to maximum (3600)", + pollRate: 5000, + expectedSecs: 3600, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize sensor with default options + sensor = newSensor(DefaultOptions()) + defer func() { sensor = nil }() + + fsm := &fsmS{ + logger: &testLogger{}, + } + + 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) + }) + } +} diff --git a/meter.go b/meter.go index 3d4b0eafe..b78af05e5 100644 --- a/meter.go +++ b/meter.go @@ -4,7 +4,6 @@ package instana import ( - "fmt" "runtime" "sync" "time" @@ -12,6 +11,13 @@ import ( "github.com/instana/go-sensor/acceptor" ) +const ( + // Metrics transmission interval constraints (in seconds) + defaultTransmissionInterval = 1 + minTransmissionInterval = 1 + maxTransmissionInterval = 3600 +) + // SnapshotS struct to hold snapshot data type SnapshotS acceptor.RuntimeInfo @@ -25,8 +31,10 @@ type MetricsS acceptor.Metrics type EntityData acceptor.GoProcessData type meterS struct { - numGC uint32 - done chan struct{} + numGC uint32 + running bool + done chan struct{} + mu sync.Mutex } // MetricsOptions contains configuration for metrics collection and transmission. @@ -43,25 +51,35 @@ func (m *MetricsOptions) getTransmissionInterval() time.Duration { defer m.mu.RUnlock() if m.transmissionInterval == 0 { - return 1 * time.Second // default + return defaultTransmissionInterval * time.Second } return m.transmissionInterval } // setTransmissionInterval sets the metrics transmission interval. // This is an internal method called when agent configuration is received. -// Only 1 or 5 seconds are valid values; others default to 1 second. +// Valid range: minTransmissionInterval-maxTransmissionInterval seconds. +// Values < minTransmissionInterval are set to minTransmissionInterval, +// values > maxTransmissionInterval are set to maxTransmissionInterval. func (m *MetricsOptions) setTransmissionInterval(seconds int) { m.mu.Lock() defer m.mu.Unlock() - // Validate: only 1 or 5 seconds allowed - if seconds != 1 && seconds != 5 { - defaultLogger.Warn("Invalid poll_rate value from agent: ", seconds, ", using default 1 second. Valid values are 1 or 5.") - m.transmissionInterval = 1 * time.Second + // Apply minimum value constraint + if seconds < minTransmissionInterval { + defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", minTransmissionInterval, " second.") + m.transmissionInterval = minTransmissionInterval * time.Second + return + } + + // Apply maximum value constraint + if seconds > maxTransmissionInterval { + defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", maxTransmissionInterval, " seconds.") + m.transmissionInterval = maxTransmissionInterval * time.Second return } + // Valid value within range m.transmissionInterval = time.Duration(seconds) * time.Second defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") } @@ -75,31 +93,55 @@ func newMeter(logger LeveledLogger) *meterS { } func (m *meterS) Run(collectInterval time.Duration) { - fmt.Println("collectInterval: ", collectInterval) - ticker := time.NewTicker(collectInterval) - defer ticker.Stop() - for { - select { - case <-m.done: - return - case <-ticker.C: - if isAgentReady() { - go func() { - s, err := getSensor() - if err != nil { - defaultLogger.Error("meter: ", err.Error()) - return - } - - _ = s.Agent().SendMetrics(m.collectMetrics()) - }() + m.mu.Lock() + m.done = make(chan struct{}, 1) + m.running = true + m.mu.Unlock() + + go func() { + ticker := time.NewTicker(collectInterval) + m.running = true + defer ticker.Stop() + for { + select { + case <-m.done: + return + case <-ticker.C: + if isAgentReady() { + go func() { + s, err := getSensor() + if err != nil { + defaultLogger.Error("meter: ", err.Error()) + return + } + + _ = s.Agent().SendMetrics(m.collectMetrics()) + }() + } } } + }() +} + +func (m *meterS) reset(interval time.Duration) { + if m == nil { + return } + m.Stop() + m.Run(interval) } func (m *meterS) Stop() { - m.done <- struct{}{} + if m == nil { + return + } + m.mu.Lock() + defer m.mu.Unlock() + + if m.running { + close(m.done) + m.running = false + } } func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats { diff --git a/meter_test.go b/meter_test.go index b05db92c3..606c17f1f 100644 --- a/meter_test.go +++ b/meter_test.go @@ -148,7 +148,7 @@ func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { expected time.Duration }{ { - name: "Valid 1 second", + name: "Valid 1 second (minimum)", seconds: 1, expected: 1 * time.Second, }, @@ -158,20 +158,50 @@ func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { expected: 5 * time.Second, }, { - name: "Invalid 0 seconds defaults to 1 second", + name: "Valid 60 seconds", + seconds: 60, + expected: 60 * time.Second, + }, + { + name: "Valid 300 seconds", + seconds: 300, + expected: 300 * time.Second, + }, + { + name: "Valid 3600 seconds (maximum)", + seconds: 3600, + expected: 3600 * time.Second, + }, + { + name: "Zero seconds sets to minimum (1 second)", seconds: 0, expected: 1 * time.Second, }, { - name: "Invalid 2 seconds defaults to 1 second", - seconds: 2, + name: "Negative value sets to minimum (1 second)", + seconds: -1, expected: 1 * time.Second, }, { - name: "Invalid negative defaults to 1 second", - seconds: -1, + name: "Negative value -100 sets to minimum (1 second)", + seconds: -100, expected: 1 * time.Second, }, + { + name: "Value exceeding maximum (3601) sets to maximum (3600 seconds)", + seconds: 3601, + expected: 3600 * time.Second, + }, + { + name: "Value exceeding maximum (5000) sets to maximum (3600 seconds)", + seconds: 5000, + expected: 3600 * time.Second, + }, + { + name: "Value exceeding maximum (10000) sets to maximum (3600 seconds)", + seconds: 10000, + expected: 3600 * time.Second, + }, } for _, tt := range tests { @@ -184,3 +214,242 @@ func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { }) } } + +func TestMeterS_Reset(t *testing.T) { + t.Run("reset running meter", func(t *testing.T) { + m := newMeter(defaultLogger) + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + m.Run(100 * time.Millisecond) + }() + + time.Sleep(150 * time.Millisecond) + + m.mu.Lock() + assert.True(t, m.running, "Meter should be running before reset") + m.mu.Unlock() + + m.reset(200 * time.Millisecond) + time.Sleep(100 * time.Millisecond) + + m.mu.Lock() + assert.True(t, m.running, "Meter should be running after reset") + m.mu.Unlock() + + m.Stop() + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("meter.Run() did not exit after Stop() was called") + } + }) + + t.Run("multiple resets with different intervals", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(150 * time.Millisecond) + + m.reset(50 * time.Millisecond) + time.Sleep(100 * time.Millisecond) + + m.reset(200 * time.Millisecond) + time.Sleep(100 * time.Millisecond) + + m.mu.Lock() + running := m.running + m.mu.Unlock() + + assert.True(t, running, "Meter should still be running after multiple resets") + + m.Stop() + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped") + m.mu.Unlock() + }) + + t.Run("reset without initial run", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.mu.Lock() + assert.False(t, m.running, "Meter should not be running initially") + m.mu.Unlock() + + m.reset(100 * time.Millisecond) + time.Sleep(150 * time.Millisecond) + + m.mu.Lock() + running := m.running + m.mu.Unlock() + + assert.True(t, running, "Meter should be running after reset") + + m.Stop() + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped") + m.mu.Unlock() + }) +} + +func TestMeterS_ConcurrentOperations(t *testing.T) { + t.Run("concurrent stop and run", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(2) + go func() { + defer wg.Done() + m.Stop() + }() + go func() { + defer wg.Done() + m.Run(100 * time.Millisecond) + }() + } + + wg.Wait() + m.Stop() + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped after concurrent operations") + m.mu.Unlock() + }) + + t.Run("concurrent reset", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(interval time.Duration) { + defer wg.Done() + m.reset(interval) + }(time.Duration(50+i*10) * time.Millisecond) + } + + wg.Wait() + time.Sleep(100 * time.Millisecond) + + m.mu.Lock() + running := m.running + m.mu.Unlock() + + assert.True(t, running, "Meter should be running after concurrent resets") + + m.Stop() + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped") + m.mu.Unlock() + }) +} + +func TestMeterS_StopAndRestart(t *testing.T) { + t.Run("stop multiple times", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + // Stop multiple times - should not panic + m.Stop() + m.Stop() + m.Stop() + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped") + m.mu.Unlock() + }) + + t.Run("run after stop", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(150 * time.Millisecond) + + m.Stop() + time.Sleep(50 * time.Millisecond) + + m.mu.Lock() + assert.False(t, m.running, "Meter should be stopped") + m.mu.Unlock() + + // Start again + m.Run(100 * time.Millisecond) + time.Sleep(150 * time.Millisecond) + + m.mu.Lock() + running := m.running + m.mu.Unlock() + + assert.True(t, running, "Meter should be running after restart") + + m.Stop() + }) +} + +func TestMeterS_Reset_InternalState(t *testing.T) { + t.Run("preserves numGC", func(t *testing.T) { + m := newMeter(defaultLogger) + + _ = m.collectMetrics() + + m.mu.Lock() + initialNumGC := m.numGC + m.mu.Unlock() + + m.Run(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + m.reset(200 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + m.mu.Lock() + currentNumGC := m.numGC + m.mu.Unlock() + + assert.GreaterOrEqual(t, currentNumGC, initialNumGC, "numGC should be preserved or increased") + + m.Stop() + }) + + t.Run("creates new done channel", func(t *testing.T) { + m := newMeter(defaultLogger) + + m.Run(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + m.mu.Lock() + firstDone := m.done + m.mu.Unlock() + + m.reset(200 * time.Millisecond) + time.Sleep(50 * time.Millisecond) + + m.mu.Lock() + secondDone := m.done + m.mu.Unlock() + + assert.NotEqual(t, firstDone, secondDone, "Reset should create a new done channel") + + m.Stop() + }) +} From 4f5c4f30f3ee7898acdce264af5f84ced60fe4e5 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 4 May 2026 20:33:54 +0530 Subject: [PATCH 07/21] chore: code cleanup --- agent.go | 2 +- fsm.go | 1 - options.go | 1 - sensor.go | 5 ----- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/agent.go b/agent.go index 1eb99707f..a010f1424 100644 --- a/agent.go +++ b/agent.go @@ -55,7 +55,7 @@ type agentResponse struct { Disable []map[string]bool `json:"disable"` } `json:"tracing"` PluginConfig struct { - PollRate int `json:"poll_rate"` // Poll rate in seconds from agent configuration + PollRate int `json:"poll_rate"` // Poll rate in seconds } `json:"plugin.golang"` } diff --git a/fsm.go b/fsm.go index f78482ded..b98dc4a48 100644 --- a/fsm.go +++ b/fsm.go @@ -274,7 +274,6 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { } // applyMetricsPollRateConfig applies the metrics poll rate configuration from agent response. -// The poll rate is configured in the agent's configuration.yaml under com.instana.plugin.golang.poll_rate func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { // Check if sensor is initialized if sensor == nil || sensor.options == nil { diff --git a/options.go b/options.go index 60e2c0990..d8da61489 100644 --- a/options.go +++ b/options.go @@ -41,7 +41,6 @@ type Options struct { // IncludeProfilerFrames is whether to include profiler calls into the profile or not IncludeProfilerFrames bool // Metrics contains metrics collection and transmission configuration. - // This is managed internally and populated from agent configuration. Metrics MetricsOptions // Tracer contains tracer-specific configuration used by all tracers Tracer TracerOptions diff --git a/sensor.go b/sensor.go index ad49625af..f5f76dc18 100644 --- a/sensor.go +++ b/sensor.go @@ -224,11 +224,6 @@ func InitSensor(options *Options) { // configure auto-profiling configureAutoProfiling(options) - // start collecting metrics - // Interval is configured via agent's configuration.yaml (com.instana.plugin.golang.poll_rate) - // Default is 1 second if not configured - // go sensor.meter.Run(options.Metrics.GetTransmissionInterval()) - sensor.logger.Debug("initialized Instana sensor v", Version) } From b68cba7c6dcd136c190b2be398e0247026799e66 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Tue, 5 May 2026 12:57:37 +0530 Subject: [PATCH 08/21] fix: cuncurrent access of stop and run --- meter.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/meter.go b/meter.go index b78af05e5..e2ea49602 100644 --- a/meter.go +++ b/meter.go @@ -94,13 +94,20 @@ func newMeter(logger LeveledLogger) *meterS { func (m *meterS) Run(collectInterval time.Duration) { m.mu.Lock() - m.done = make(chan struct{}, 1) + defer m.mu.Unlock() + + // If already running, stop first + if m.running { + close(m.done) + m.running = false + } + + // Create new channel and start + m.done = make(chan struct{}) m.running = true - m.mu.Unlock() go func() { ticker := time.NewTicker(collectInterval) - m.running = true defer ticker.Stop() for { select { @@ -132,11 +139,12 @@ func (m *meterS) reset(interval time.Duration) { } func (m *meterS) Stop() { + m.mu.Lock() + defer m.mu.Unlock() + if m == nil { return } - m.mu.Lock() - defer m.mu.Unlock() if m.running { close(m.done) From 81afc08eb473456b18696c52f4369ba7005aef32 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Tue, 5 May 2026 13:54:00 +0530 Subject: [PATCH 09/21] fix: resolve metrics collection issue for serverless agent --- meter.go | 17 +++++++++++------ sensor.go | 7 +++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/meter.go b/meter.go index e2ea49602..8fff6930a 100644 --- a/meter.go +++ b/meter.go @@ -92,26 +92,31 @@ func newMeter(logger LeveledLogger) *meterS { } } -func (m *meterS) Run(collectInterval time.Duration) { +func (m *meterS) prepareRun() chan struct{} { m.mu.Lock() defer m.mu.Unlock() // If already running, stop first if m.running { close(m.done) - m.running = false } - // Create new channel and start + // Create new channel and mark as running m.done = make(chan struct{}) m.running = true - go func() { + return m.done +} + +func (m *meterS) Run(collectInterval time.Duration) { + done := m.prepareRun() + + go func(done chan struct{}) { ticker := time.NewTicker(collectInterval) defer ticker.Stop() for { select { - case <-m.done: + case <-done: return case <-ticker.C: if isAgentReady() { @@ -127,7 +132,7 @@ func (m *meterS) Run(collectInterval time.Duration) { } } } - }() + }(done) } func (m *meterS) reset(interval time.Duration) { diff --git a/sensor.go b/sensor.go index f5f76dc18..5acf84a90 100644 --- a/sensor.go +++ b/sensor.go @@ -115,6 +115,7 @@ func newSensor(options *Options) *sensorS { } var agent AgentClient + var isServerless bool if options.AgentClient != nil { agent = options.AgentClient @@ -122,6 +123,7 @@ func newSensor(options *Options) *sensorS { if agentEndpoint := os.Getenv("INSTANA_ENDPOINT_URL"); agentEndpoint != "" && agent == nil { s.logger.Debug("INSTANA_ENDPOINT_URL= is set, switching to the serverless mode") + isServerless = true timeout, err := parseInstanaTimeout(os.Getenv("INSTANA_TIMEOUT")) if err != nil { @@ -149,6 +151,11 @@ func newSensor(options *Options) *sensorS { s.setAgent(agent) + // For serverless agents, start the meter immediately since they don't use the FSM + if isServerless { + s.meter.Run(s.options.Metrics.getTransmissionInterval()) + } + return s } From d531608ec5c3a0b1fe7d2a71ba0da494ec2e6760 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 20 Jul 2026 12:36:00 +0530 Subject: [PATCH 10/21] refactor: change the upper limit of the poll_rate configuration --- README.md | 8 ++++---- fsm_test.go | 11 +++-------- meter.go | 2 +- meter_test.go | 28 +++++++++------------------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1ddc51deb..1bcbc05d2 100644 --- a/README.md +++ b/README.md @@ -77,19 +77,19 @@ In the agent's `configuration.yaml`: ```yaml # Configure metrics transmission interval for Go applications com.instana.plugin.golang: - poll_rate: 5 # Valid range: 1-3600 (seconds) + poll_rate: 5 # Valid range: 1-60 (seconds) ``` **Valid Values:** -- Any value between `1` and `3600` seconds +- Any value between `1` and `60` seconds - Default: `1` second (if not configured) - Minimum: `1` second -- Maximum: `3600` seconds (1 hour) +- Maximum: `60` seconds **Behavior:** - If `poll_rate` is not configured, defaults to 1 second - Values less than 1 will be set to the minimum value of 1 second. -- Values greater than 3600 will be set to the maximum value of 3600 seconds. +- Values greater than 60 will be set to the maximum value of 60 seconds. - Configuration is applied when the Go sensor announces itself to the agent. This data is then already available in the dashboard. diff --git a/fsm_test.go b/fsm_test.go index a293597ae..df48e6f99 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -658,15 +658,10 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { expectedSecs: 10, }, { - name: "Valid 60 seconds", + name: "Valid 60 seconds (maximum)", pollRate: 60, expectedSecs: 60, }, - { - name: "Valid 3600 seconds (maximum)", - pollRate: 3600, - expectedSecs: 3600, - }, { name: "Zero seconds - sets to minimum (1)", pollRate: 0, @@ -678,9 +673,9 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { expectedSecs: 1, }, { - name: "Exceeds maximum (5000) - sets to maximum (3600)", + name: "Exceeds maximum (5000) - sets to maximum (60)", pollRate: 5000, - expectedSecs: 3600, + expectedSecs: 60, }, } diff --git a/meter.go b/meter.go index 8fff6930a..5fd92b430 100644 --- a/meter.go +++ b/meter.go @@ -15,7 +15,7 @@ const ( // Metrics transmission interval constraints (in seconds) defaultTransmissionInterval = 1 minTransmissionInterval = 1 - maxTransmissionInterval = 3600 + maxTransmissionInterval = 60 ) // SnapshotS struct to hold snapshot data diff --git a/meter_test.go b/meter_test.go index 606c17f1f..c59e798df 100644 --- a/meter_test.go +++ b/meter_test.go @@ -163,14 +163,14 @@ func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { expected: 60 * time.Second, }, { - name: "Valid 300 seconds", - seconds: 300, - expected: 300 * time.Second, + name: "Value exceeding maximum (1000) sets to maximum (60 seconds)", + seconds: 1000, + expected: 60 * time.Second, }, { - name: "Valid 3600 seconds (maximum)", - seconds: 3600, - expected: 3600 * time.Second, + name: "Valid 60 seconds (maximum)", + seconds: 60, + expected: 60 * time.Second, }, { name: "Zero seconds sets to minimum (1 second)", @@ -188,19 +188,9 @@ func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { expected: 1 * time.Second, }, { - name: "Value exceeding maximum (3601) sets to maximum (3600 seconds)", - seconds: 3601, - expected: 3600 * time.Second, - }, - { - name: "Value exceeding maximum (5000) sets to maximum (3600 seconds)", - seconds: 5000, - expected: 3600 * time.Second, - }, - { - name: "Value exceeding maximum (10000) sets to maximum (3600 seconds)", - seconds: 10000, - expected: 3600 * time.Second, + name: "Value exceeding maximum (61) sets to maximum (60 seconds)", + seconds: 61, + expected: 60 * time.Second, }, } From 072077680f69ab9477689fb243d32ddadae18593 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Wed, 22 Jul 2026 09:34:48 +0530 Subject: [PATCH 11/21] refactor: ensure the metric collection run function is executed only once --- README.md | 13 +- fsm.go | 2 +- fsm_test.go | 11 +- meter.go | 107 +++++------- meter_test.go | 463 ++++++++++---------------------------------------- 5 files changed, 146 insertions(+), 450 deletions(-) diff --git a/README.md b/README.md index 1bcbc05d2..17d29da49 100644 --- a/README.md +++ b/README.md @@ -77,20 +77,23 @@ In the agent's `configuration.yaml`: ```yaml # Configure metrics transmission interval for Go applications com.instana.plugin.golang: - poll_rate: 5 # Valid range: 1-60 (seconds) + poll_rate: 5 # Valid range: 1-600 (seconds) ``` **Valid Values:** -- Any value between `1` and `60` seconds +- Any value between `1` and `600` seconds - Default: `1` second (if not configured) - Minimum: `1` second -- Maximum: `60` seconds +- Maximum: `600` seconds **Behavior:** - If `poll_rate` is not configured, defaults to 1 second - Values less than 1 will be set to the minimum value of 1 second. -- Values greater than 60 will be set to the maximum value of 60 seconds. -- Configuration is applied when the Go sensor announces itself to the agent. +- Values greater than 600 will be set to the maximum value of 600 seconds. +- 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. This data is then already available in the dashboard. diff --git a/fsm.go b/fsm.go index b98dc4a48..1432705cc 100644 --- a/fsm.go +++ b/fsm.go @@ -440,7 +440,7 @@ func (r *fsmS) reset() { func (r *fsmS) ready(_ context.Context, e *f.Event) { go delayed.flush() - go sensor.meter.reset(sensor.options.Metrics.getTransmissionInterval()) + go sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) } func (r *fsmS) cpuSetFileContent(pid int) string { diff --git a/fsm_test.go b/fsm_test.go index df48e6f99..24352b9f3 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -658,10 +658,15 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { expectedSecs: 10, }, { - name: "Valid 60 seconds (maximum)", + name: "Valid 60 seconds", pollRate: 60, expectedSecs: 60, }, + { + name: "Valid 600 seconds (maximum)", + pollRate: 600, + expectedSecs: 600, + }, { name: "Zero seconds - sets to minimum (1)", pollRate: 0, @@ -673,9 +678,9 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { expectedSecs: 1, }, { - name: "Exceeds maximum (5000) - sets to maximum (60)", + name: "Exceeds maximum (5000) - sets to maximum (600)", pollRate: 5000, - expectedSecs: 60, + expectedSecs: 600, }, } diff --git a/meter.go b/meter.go index 5fd92b430..f77eea6f5 100644 --- a/meter.go +++ b/meter.go @@ -15,7 +15,7 @@ const ( // Metrics transmission interval constraints (in seconds) defaultTransmissionInterval = 1 minTransmissionInterval = 1 - maxTransmissionInterval = 60 + maxTransmissionInterval = 600 ) // SnapshotS struct to hold snapshot data @@ -31,10 +31,10 @@ type MetricsS acceptor.Metrics type EntityData acceptor.GoProcessData type meterS struct { - numGC uint32 - running bool - done chan struct{} - mu sync.Mutex + numGC uint32 + once sync.Once + stopOnce sync.Once + done chan struct{} } // MetricsOptions contains configuration for metrics collection and transmission. @@ -44,7 +44,7 @@ type MetricsOptions struct { transmissionInterval time.Duration } -// GetTransmissionInterval returns the current metrics transmission interval. +// getTransmissionInterval returns the current metrics transmission interval. // This value is configured through the agent's configuration.yaml file. func (m *MetricsOptions) getTransmissionInterval() time.Duration { m.mu.RLock() @@ -57,29 +57,25 @@ func (m *MetricsOptions) getTransmissionInterval() time.Duration { } // setTransmissionInterval sets the metrics transmission interval. -// This is an internal method called when agent configuration is received. -// Valid range: minTransmissionInterval-maxTransmissionInterval seconds. -// Values < minTransmissionInterval are set to minTransmissionInterval, -// values > maxTransmissionInterval are set to maxTransmissionInterval. +// This is an internal method called when agent configuration is received during +// the initial handshake. Valid range: minTransmissionInterval-maxTransmissionInterval seconds. +// Values outside the range are clamped to the nearest bound. func (m *MetricsOptions) setTransmissionInterval(seconds int) { m.mu.Lock() defer m.mu.Unlock() - // Apply minimum value constraint if seconds < minTransmissionInterval { defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", minTransmissionInterval, " second.") m.transmissionInterval = minTransmissionInterval * time.Second return } - // Apply maximum value constraint if seconds > maxTransmissionInterval { defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", maxTransmissionInterval, " seconds.") m.transmissionInterval = maxTransmissionInterval * time.Second return } - // Valid value within range m.transmissionInterval = time.Duration(seconds) * time.Second defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") } @@ -88,73 +84,50 @@ func newMeter(logger LeveledLogger) *meterS { logger.Debug("initializing meter") return &meterS{ - done: make(chan struct{}, 1), + done: make(chan struct{}), } } -func (m *meterS) prepareRun() chan struct{} { - m.mu.Lock() - defer m.mu.Unlock() - - // If already running, stop first - if m.running { - close(m.done) - } - - // Create new channel and mark as running - m.done = make(chan struct{}) - m.running = true - - return m.done -} - +// Run starts the metrics collection loop at the given interval. +// It is safe to call Run multiple times — only the first call starts the loop; +// subsequent calls (e.g. on agent reconnect) are ignored so the running loop +// continues uninterrupted with the original interval. func (m *meterS) Run(collectInterval time.Duration) { - done := m.prepareRun() - - go func(done chan struct{}) { - ticker := time.NewTicker(collectInterval) - defer ticker.Stop() - for { - select { - case <-done: - return - case <-ticker.C: - if isAgentReady() { - go func() { - s, err := getSensor() - if err != nil { - defaultLogger.Error("meter: ", err.Error()) - return - } - - _ = s.Agent().SendMetrics(m.collectMetrics()) - }() - } - } - } - }(done) -} - -func (m *meterS) reset(interval time.Duration) { if m == nil { return } - m.Stop() - m.Run(interval) + m.once.Do(func() { + go func() { + ticker := time.NewTicker(collectInterval) + defer ticker.Stop() + for { + select { + case <-m.done: + return + case <-ticker.C: + if isAgentReady() { + go func() { + s, err := getSensor() + if err != nil { + defaultLogger.Error("meter: ", err.Error()) + return + } + + _ = s.Agent().SendMetrics(m.collectMetrics()) + }() + } + } + } + }() + }) } +// Stop shuts down the metrics collection loop. Safe to call multiple times. func (m *meterS) Stop() { - m.mu.Lock() - defer m.mu.Unlock() - if m == nil { return } - - if m.running { - close(m.done) - m.running = false - } + m.stopOnce.Do(func() { close(m.done) }) } func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats { diff --git a/meter_test.go b/meter_test.go index c59e798df..356130a6a 100644 --- a/meter_test.go +++ b/meter_test.go @@ -11,435 +11,150 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMeterS_Stop(t *testing.T) { - // Create a new meter +// TestNewMeter verifies the meter is properly initialised. +func TestNewMeter(t *testing.T) { m := newMeter(defaultLogger) - // Track if Run is still executing - var wg sync.WaitGroup - wg.Add(1) - - // Start the meter in a goroutine - go func() { - defer wg.Done() - m.Run(100 * time.Millisecond) - }() - - // Let it run for a bit - time.Sleep(300 * time.Millisecond) - - // Stop the meter - m.Stop() - - // Wait for Run to exit with a timeout - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - select { - case <-done: - // Success - Run exited after Stop was called - case <-time.After(2 * time.Second): - t.Fatal("meter.Run() did not exit after Stop() was called") - } + assert.NotNil(t, m) + assert.NotNil(t, m.done) + assert.Equal(t, uint32(0), m.numGC) } -func TestMeterS_Run_StopImmediately(t *testing.T) { - // Create a new meter +// TestMeterRun_StartsOnce ensures the collection goroutine is only started once +// regardless of how many times Run is called — matching the agent-reconnect use case +// where the FSM calls Run again but the loop must continue uninterrupted. +func TestMeterRun_StartsOnce(t *testing.T) { m := newMeter(defaultLogger) - // Track if Run is still executing - var wg sync.WaitGroup - wg.Add(1) + // Call Run three times in quick succession — only the first must start the loop. + m.Run(50 * time.Millisecond) + m.Run(50 * time.Millisecond) + m.Run(50 * time.Millisecond) - // Start the meter in a goroutine - go func() { - defer wg.Done() - m.Run(100 * time.Millisecond) - }() + // Give one tick to fire so we know the loop is alive. + time.Sleep(80 * time.Millisecond) - // Stop immediately without waiting - m.Stop() - - // Wait for Run to exit with a timeout - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() + // Stop and confirm the channel closes cleanly (not double-closed/panicked). + assert.NotPanics(t, m.Stop) + // Confirm done channel is closed (reading from a closed channel returns immediately). select { - case <-done: - // Success - Run exited after Stop was called - case <-time.After(2 * time.Second): - t.Fatal("meter.Run() did not exit after immediate Stop() was called") + case <-m.done: + // expected + default: + t.Fatal("done channel should be closed after Stop()") } } -func TestMeterS_CollectMetrics(t *testing.T) { - // Create a new meter +// TestMeterStop_Idempotent verifies that calling Stop multiple times never panics. +func TestMeterStop_Idempotent(t *testing.T) { m := newMeter(defaultLogger) + m.Run(100 * time.Millisecond) - // Collect metrics - metrics := m.collectMetrics() + assert.NotPanics(t, func() { + m.Stop() + m.Stop() + m.Stop() + }) +} - // Verify metrics are collected - if metrics.Goroutine <= 0 { - t.Errorf("Expected positive goroutine count, got %d", metrics.Goroutine) - } +// TestMeterStop_WithoutRun verifies Stop is safe even when Run was never called. +func TestMeterStop_WithoutRun(t *testing.T) { + m := newMeter(defaultLogger) - if metrics.MemoryStats.Alloc == 0 { - t.Error("Expected non-zero memory allocation") - } + assert.NotPanics(t, func() { + m.Stop() + }) } -func TestMeterS_CollectMemoryMetrics(t *testing.T) { - // Create a new meter +// TestMeterRun_LoopExitsOnStop verifies the collection goroutine stops when Stop is called. +func TestMeterRun_LoopExitsOnStop(t *testing.T) { m := newMeter(defaultLogger) + m.Run(50 * time.Millisecond) - // Collect memory metrics - memStats := m.collectMemoryMetrics() + // Let at least one tick fire. + time.Sleep(80 * time.Millisecond) - // Verify memory stats are collected - if memStats.Alloc == 0 { - t.Error("Expected non-zero Alloc") - } - - if memStats.Sys == 0 { - t.Error("Expected non-zero Sys") - } + stopped := make(chan struct{}) + go func() { + m.Stop() + close(stopped) + }() - if memStats.HeapAlloc == 0 { - t.Error("Expected non-zero HeapAlloc") + select { + case <-stopped: + // expected + case <-time.After(2 * time.Second): + t.Fatal("Stop() did not return in time") } } -func TestMeterS_NewMeter(t *testing.T) { - // Create a new meter +// TestMeterRun_ConcurrentCallsSafe verifies concurrent calls to Run are race-free. +func TestMeterRun_ConcurrentCallsSafe(t *testing.T) { m := newMeter(defaultLogger) - if m == nil { - t.Fatal("Expected non-nil meter") - } - - if m.done == nil { - t.Error("Expected done channel to be initialized") + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + m.Run(100 * time.Millisecond) + }() } + wg.Wait() - if m.numGC != 0 { - t.Errorf("Expected initial numGC to be 0, got %d", m.numGC) - } + // Should stop cleanly with no panic. + assert.NotPanics(t, m.Stop) } +// TestMetricsOptions_GetTransmissionInterval_Default verifies the default interval +// is returned when none has been configured. func TestMetricsOptions_GetTransmissionInterval_Default(t *testing.T) { opts := &MetricsOptions{} - - interval := opts.getTransmissionInterval() - - assert.Equal(t, 1*time.Second, interval, "Default transmission interval should be 1 second") + assert.Equal(t, time.Second, opts.getTransmissionInterval()) } +// TestMetricsOptions_SetTransmissionInterval verifies clamping and valid values. func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { tests := []struct { name string seconds int expected time.Duration }{ - { - name: "Valid 1 second (minimum)", - seconds: 1, - expected: 1 * time.Second, - }, - { - name: "Valid 5 seconds", - seconds: 5, - expected: 5 * time.Second, - }, - { - name: "Valid 60 seconds", - seconds: 60, - expected: 60 * time.Second, - }, - { - name: "Value exceeding maximum (1000) sets to maximum (60 seconds)", - seconds: 1000, - expected: 60 * time.Second, - }, - { - name: "Valid 60 seconds (maximum)", - seconds: 60, - expected: 60 * time.Second, - }, - { - name: "Zero seconds sets to minimum (1 second)", - seconds: 0, - expected: 1 * time.Second, - }, - { - name: "Negative value sets to minimum (1 second)", - seconds: -1, - expected: 1 * time.Second, - }, - { - name: "Negative value -100 sets to minimum (1 second)", - seconds: -100, - expected: 1 * time.Second, - }, - { - name: "Value exceeding maximum (61) sets to maximum (60 seconds)", - seconds: 61, - expected: 60 * time.Second, - }, + {"minimum (1s)", 1, 1 * time.Second}, + {"valid 5s", 5, 5 * time.Second}, + {"valid 60s", 60, 60 * time.Second}, + {"valid 300s", 300, 300 * time.Second}, + {"maximum (600s)", 600, 600 * time.Second}, + {"zero clamps to minimum", 0, 1 * time.Second}, + {"negative clamps to minimum", -1, 1 * time.Second}, + {"exceeds maximum clamps to 600s", 1000, 600 * time.Second}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opts := &MetricsOptions{} - opts.setTransmissionInterval(tt.seconds) - assert.Equal(t, tt.expected, opts.getTransmissionInterval()) }) } } -func TestMeterS_Reset(t *testing.T) { - t.Run("reset running meter", func(t *testing.T) { - m := newMeter(defaultLogger) - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - m.Run(100 * time.Millisecond) - }() - - time.Sleep(150 * time.Millisecond) - - m.mu.Lock() - assert.True(t, m.running, "Meter should be running before reset") - m.mu.Unlock() - - m.reset(200 * time.Millisecond) - time.Sleep(100 * time.Millisecond) - - m.mu.Lock() - assert.True(t, m.running, "Meter should be running after reset") - m.mu.Unlock() - - m.Stop() - - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - select { - case <-done: - case <-time.After(2 * time.Second): - t.Fatal("meter.Run() did not exit after Stop() was called") - } - }) - - t.Run("multiple resets with different intervals", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(150 * time.Millisecond) - - m.reset(50 * time.Millisecond) - time.Sleep(100 * time.Millisecond) - - m.reset(200 * time.Millisecond) - time.Sleep(100 * time.Millisecond) - - m.mu.Lock() - running := m.running - m.mu.Unlock() - - assert.True(t, running, "Meter should still be running after multiple resets") - - m.Stop() - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped") - m.mu.Unlock() - }) - - t.Run("reset without initial run", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.mu.Lock() - assert.False(t, m.running, "Meter should not be running initially") - m.mu.Unlock() - - m.reset(100 * time.Millisecond) - time.Sleep(150 * time.Millisecond) - - m.mu.Lock() - running := m.running - m.mu.Unlock() - - assert.True(t, running, "Meter should be running after reset") - - m.Stop() - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped") - m.mu.Unlock() - }) -} - -func TestMeterS_ConcurrentOperations(t *testing.T) { - t.Run("concurrent stop and run", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(2) - go func() { - defer wg.Done() - m.Stop() - }() - go func() { - defer wg.Done() - m.Run(100 * time.Millisecond) - }() - } - - wg.Wait() - m.Stop() - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped after concurrent operations") - m.mu.Unlock() - }) - - t.Run("concurrent reset", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - var wg sync.WaitGroup - for i := 0; i < 5; i++ { - wg.Add(1) - go func(interval time.Duration) { - defer wg.Done() - m.reset(interval) - }(time.Duration(50+i*10) * time.Millisecond) - } - - wg.Wait() - time.Sleep(100 * time.Millisecond) - - m.mu.Lock() - running := m.running - m.mu.Unlock() - - assert.True(t, running, "Meter should be running after concurrent resets") - - m.Stop() - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped") - m.mu.Unlock() - }) -} - -func TestMeterS_StopAndRestart(t *testing.T) { - t.Run("stop multiple times", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - // Stop multiple times - should not panic - m.Stop() - m.Stop() - m.Stop() - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped") - m.mu.Unlock() - }) - - t.Run("run after stop", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(150 * time.Millisecond) - - m.Stop() - time.Sleep(50 * time.Millisecond) - - m.mu.Lock() - assert.False(t, m.running, "Meter should be stopped") - m.mu.Unlock() - - // Start again - m.Run(100 * time.Millisecond) - time.Sleep(150 * time.Millisecond) - - m.mu.Lock() - running := m.running - m.mu.Unlock() - - assert.True(t, running, "Meter should be running after restart") +// TestMeterCollectMetrics verifies that metric collection returns non-zero values. +func TestMeterCollectMetrics(t *testing.T) { + m := newMeter(defaultLogger) + metrics := m.collectMetrics() - m.Stop() - }) + assert.Greater(t, metrics.Goroutine, 0) + assert.NotZero(t, metrics.MemoryStats.Alloc) } -func TestMeterS_Reset_InternalState(t *testing.T) { - t.Run("preserves numGC", func(t *testing.T) { - m := newMeter(defaultLogger) - - _ = m.collectMetrics() - - m.mu.Lock() - initialNumGC := m.numGC - m.mu.Unlock() - - m.Run(100 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - m.reset(200 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - m.mu.Lock() - currentNumGC := m.numGC - m.mu.Unlock() - - assert.GreaterOrEqual(t, currentNumGC, initialNumGC, "numGC should be preserved or increased") - - m.Stop() - }) - - t.Run("creates new done channel", func(t *testing.T) { - m := newMeter(defaultLogger) - - m.Run(100 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - m.mu.Lock() - firstDone := m.done - m.mu.Unlock() - - m.reset(200 * time.Millisecond) - time.Sleep(50 * time.Millisecond) - - m.mu.Lock() - secondDone := m.done - m.mu.Unlock() - - assert.NotEqual(t, firstDone, secondDone, "Reset should create a new done channel") +// TestMeterCollectMemoryMetrics verifies that memory stats are populated. +func TestMeterCollectMemoryMetrics(t *testing.T) { + m := newMeter(defaultLogger) + mem := m.collectMemoryMetrics() - m.Stop() - }) + assert.NotZero(t, mem.Alloc) + assert.NotZero(t, mem.Sys) + assert.NotZero(t, mem.HeapAlloc) } From 54784a2fa77a64b33b951aa053055326b181abb2 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Fri, 24 Jul 2026 10:32:14 +0530 Subject: [PATCH 12/21] fix: resolve race condition in collectMemoryMetrics --- fsm.go | 12 ++++++------ meter.go | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/fsm.go b/fsm.go index 1432705cc..a72d66b4f 100644 --- a/fsm.go +++ b/fsm.go @@ -275,21 +275,21 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { // applyMetricsPollRateConfig applies the metrics poll rate configuration from agent response. func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { - // Check if sensor is initialized - if sensor == nil || sensor.options == nil { + 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 { + if resp.PluginConfig.PollRate <= 0 { r.logger.Debug("No poll_rate configuration received from agent, using default 1 second") - sensor.options.Metrics.setTransmissionInterval(1) + s.options.Metrics.setTransmissionInterval(1) return } r.logger.Debug("Applying metrics poll_rate configuration from agent: ", resp.PluginConfig.PollRate, " second(s)") - sensor.options.Metrics.setTransmissionInterval(resp.PluginConfig.PollRate) + s.options.Metrics.setTransmissionInterval(resp.PluginConfig.PollRate) } func (r *fsmS) applyDisableTracingConfig(resp agentResponse) { @@ -440,7 +440,7 @@ func (r *fsmS) reset() { func (r *fsmS) ready(_ context.Context, e *f.Event) { go delayed.flush() - go sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) + sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) } func (r *fsmS) cpuSetFileContent(pid int) string { diff --git a/meter.go b/meter.go index f77eea6f5..d6803585f 100644 --- a/meter.go +++ b/meter.go @@ -6,6 +6,7 @@ package instana import ( "runtime" "sync" + "sync/atomic" "time" "github.com/instana/go-sensor/acceptor" @@ -31,7 +32,7 @@ type MetricsS acceptor.Metrics type EntityData acceptor.GoProcessData type meterS struct { - numGC uint32 + numGC atomic.Uint32 once sync.Once stopOnce sync.Once done chan struct{} @@ -92,6 +93,8 @@ func newMeter(logger LeveledLogger) *meterS { // It is safe to call Run multiple times — only the first call starts the loop; // subsequent calls (e.g. on agent reconnect) are ignored so the running loop // continues uninterrupted with the original interval. +// The interval is fixed at the first call; changing poll_rate in the agent +// configuration after startup requires an application restart to take effect. func (m *meterS) Run(collectInterval time.Duration) { if m == nil { return @@ -150,9 +153,9 @@ func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats { NumGC: memStats.NumGC, GCCPUFraction: memStats.GCCPUFraction} - if m.numGC < memStats.NumGC { + if m.numGC.Load() < memStats.NumGC { ret.PauseNs = memStats.PauseNs[(memStats.NumGC+255)%256] - m.numGC = memStats.NumGC + m.numGC.Store(memStats.NumGC) } return ret From 520caecf418e2c372ab61015d9fcc871decbe066 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Fri, 24 Jul 2026 11:08:02 +0530 Subject: [PATCH 13/21] fix: resolve unit test failures --- meter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meter_test.go b/meter_test.go index 356130a6a..cb81ce431 100644 --- a/meter_test.go +++ b/meter_test.go @@ -17,7 +17,7 @@ func TestNewMeter(t *testing.T) { assert.NotNil(t, m) assert.NotNil(t, m.done) - assert.Equal(t, uint32(0), m.numGC) + assert.Equal(t, uint32(0), m.numGC.Load()) } // TestMeterRun_StartsOnce ensures the collection goroutine is only started once From e5c398600f06189c3dae1ebb4826e948fe0eb6ac Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Tue, 28 Jul 2026 13:15:02 +0530 Subject: [PATCH 14/21] doc: update README.md for serverless agent --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17d29da49..dfaaef82a 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,11 @@ Once the collector has been initialized with `instana.InitCollector`, applicatio #### Metrics Transmission Interval -Metrics are transmitted to the Instana Agent at a configurable interval. The interval is configured through the agent's `configuration.yaml` file. +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:** @@ -95,7 +99,9 @@ com.instana.plugin.golang: > [!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. -This data is then already available in the dashboard. +##### 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 From 9bbceb3a18e6f47bfb963ca36f9a01bee90f4606 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Tue, 28 Jul 2026 15:36:55 +0530 Subject: [PATCH 15/21] fix: address review comments --- fsm.go | 9 +++++++-- meter.go | 21 ++++++++++----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/fsm.go b/fsm.go index a72d66b4f..0a099da05 100644 --- a/fsm.go +++ b/fsm.go @@ -284,7 +284,7 @@ func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { // 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(1) + s.options.Metrics.setTransmissionInterval(defaultTransmissionInterval) return } @@ -440,7 +440,12 @@ func (r *fsmS) reset() { func (r *fsmS) ready(_ context.Context, e *f.Event) { go delayed.flush() - sensor.meter.Run(sensor.options.Metrics.getTransmissionInterval()) + s, err := getSensor() + if err != nil { + r.logger.Error(err.Error()) + return + } + s.meter.Run(s.options.Metrics.getTransmissionInterval()) } func (r *fsmS) cpuSetFileContent(pid int) string { diff --git a/meter.go b/meter.go index d6803585f..e3d22a966 100644 --- a/meter.go +++ b/meter.go @@ -62,23 +62,22 @@ func (m *MetricsOptions) getTransmissionInterval() time.Duration { // the initial handshake. Valid range: minTransmissionInterval-maxTransmissionInterval seconds. // Values outside the range are clamped to the nearest bound. func (m *MetricsOptions) setTransmissionInterval(seconds int) { - m.mu.Lock() - defer m.mu.Unlock() + var interval time.Duration if seconds < minTransmissionInterval { defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", minTransmissionInterval, " second.") - m.transmissionInterval = minTransmissionInterval * time.Second - return - } - - if seconds > maxTransmissionInterval { + interval = minTransmissionInterval * time.Second + } else if seconds > maxTransmissionInterval { defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", maxTransmissionInterval, " seconds.") - m.transmissionInterval = maxTransmissionInterval * time.Second - return + interval = maxTransmissionInterval * time.Second + } else { + interval = time.Duration(seconds) * time.Second + defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") } - m.transmissionInterval = time.Duration(seconds) * time.Second - defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") + m.mu.Lock() + defer m.mu.Unlock() + m.transmissionInterval = interval } func newMeter(logger LeveledLogger) *meterS { From 08717bfb8bab02a17c5f20472667c1938f347256 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Wed, 29 Jul 2026 12:11:56 +0530 Subject: [PATCH 16/21] fix: removed transmissionInterval fallback logic from getTransmissionInterval function --- fsm.go | 7 ++++++- meter.go | 3 --- meter_test.go | 6 +++--- sensor.go | 1 + 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/fsm.go b/fsm.go index 0a099da05..40b700818 100644 --- a/fsm.go +++ b/fsm.go @@ -445,7 +445,12 @@ func (r *fsmS) ready(_ context.Context, e *f.Event) { r.logger.Error(err.Error()) return } - s.meter.Run(s.options.Metrics.getTransmissionInterval()) + 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 { diff --git a/meter.go b/meter.go index e3d22a966..e574846ae 100644 --- a/meter.go +++ b/meter.go @@ -51,9 +51,6 @@ func (m *MetricsOptions) getTransmissionInterval() time.Duration { m.mu.RLock() defer m.mu.RUnlock() - if m.transmissionInterval == 0 { - return defaultTransmissionInterval * time.Second - } return m.transmissionInterval } diff --git a/meter_test.go b/meter_test.go index cb81ce431..b7813830b 100644 --- a/meter_test.go +++ b/meter_test.go @@ -107,11 +107,11 @@ func TestMeterRun_ConcurrentCallsSafe(t *testing.T) { assert.NotPanics(t, m.Stop) } -// TestMetricsOptions_GetTransmissionInterval_Default verifies the default interval -// is returned when none has been configured. +// TestMetricsOptions_GetTransmissionInterval_Default verifies that an unconfigured +// MetricsOptions returns zero, as callers are responsible for applying the default. func TestMetricsOptions_GetTransmissionInterval_Default(t *testing.T) { opts := &MetricsOptions{} - assert.Equal(t, time.Second, opts.getTransmissionInterval()) + assert.Equal(t, time.Duration(0), opts.getTransmissionInterval()) } // TestMetricsOptions_SetTransmissionInterval verifies clamping and valid values. diff --git a/sensor.go b/sensor.go index 94552663e..a6b423271 100644 --- a/sensor.go +++ b/sensor.go @@ -153,6 +153,7 @@ func newSensor(options *Options) *sensorS { // For serverless agents, start the meter immediately since they don't use the FSM if isServerless { + s.options.Metrics.setTransmissionInterval(defaultTransmissionInterval) s.meter.Run(s.options.Metrics.getTransmissionInterval()) } From 6d987b0daa06dc11f5d5a47995c5f1654bf60a6c Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Wed, 29 Jul 2026 12:57:03 +0530 Subject: [PATCH 17/21] refactor: code cleanup --- meter.go | 6 ++++-- sensor.go | 37 +++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/meter.go b/meter.go index e574846ae..d4608d251 100644 --- a/meter.go +++ b/meter.go @@ -62,10 +62,12 @@ func (m *MetricsOptions) setTransmissionInterval(seconds int) { var interval time.Duration if seconds < minTransmissionInterval { - defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", minTransmissionInterval, " second.") + defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", + minTransmissionInterval, " second.") interval = minTransmissionInterval * time.Second } else if seconds > maxTransmissionInterval { - defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", maxTransmissionInterval, " seconds.") + defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", + maxTransmissionInterval, " seconds.") interval = maxTransmissionInterval * time.Second } else { interval = time.Duration(seconds) * time.Second diff --git a/sensor.go b/sensor.go index a6b423271..959942315 100644 --- a/sensor.go +++ b/sensor.go @@ -125,22 +125,7 @@ func newSensor(options *Options) *sensorS { s.logger.Debug("INSTANA_ENDPOINT_URL= is set, switching to the serverless mode") isServerless = true - timeout, err := parseInstanaTimeout(os.Getenv("INSTANA_TIMEOUT")) - if err != nil { - s.logger.Warn("malformed INSTANA_TIMEOUT value, falling back to the default one: ", err) - timeout = defaultServerlessTimeout - } - - client, err := acceptor.NewHTTPClient(timeout) - if err != nil { - if err == acceptor.ErrMalformedProxyURL { - s.logger.Warn(err) - } else { - s.logger.Error("failed to initialize acceptor HTTP client, falling back to the default one: ", err) - client = http.DefaultClient - } - } - + client := s.initServerlessHTTPClient() agent = newServerlessAgent(s.serviceOrBinaryName(), agentEndpoint, os.Getenv("INSTANA_AGENT_KEY"), client, s.logger) } @@ -206,6 +191,26 @@ func (r *sensorS) Agent() AgentClient { return r.agent } +func (r *sensorS) initServerlessHTTPClient() *http.Client { + timeout, err := parseInstanaTimeout(os.Getenv("INSTANA_TIMEOUT")) + if err != nil { + r.logger.Warn("malformed INSTANA_TIMEOUT value, falling back to the default one: ", err) + timeout = defaultServerlessTimeout + } + + client, err := acceptor.NewHTTPClient(timeout) + if err != nil { + if err == acceptor.ErrMalformedProxyURL { + r.logger.Warn(err) + } else { + r.logger.Error("failed to initialize acceptor HTTP client, falling back to the default one: ", err) + client = http.DefaultClient + } + } + + return client +} + func (r *sensorS) serviceOrBinaryName() string { if r == nil { return "" From a336e7a59909e186dc5834ff82ea7e24650d0bfb Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Fri, 31 Jul 2026 12:05:00 +0530 Subject: [PATCH 18/21] test: add unit tests to increase coverage --- agent_test.go | 73 ++++++++++++++++++++++++++++++++ fsm_test.go | 53 ++++++++++++++++++++++++ meter_test.go | 112 ++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 208 insertions(+), 30 deletions(-) diff --git a/agent_test.go b/agent_test.go index b7de5034e..1ae431774 100644 --- a/agent_test.go +++ b/agent_test.go @@ -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{})) +} diff --git a/fsm_test.go b/fsm_test.go index 24352b9f3..ef80ca879 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -709,3 +709,56 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { }) } } + +// 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()) +} diff --git a/meter_test.go b/meter_test.go index b7813830b..0637cd2df 100644 --- a/meter_test.go +++ b/meter_test.go @@ -26,53 +26,26 @@ func TestNewMeter(t *testing.T) { func TestMeterRun_StartsOnce(t *testing.T) { m := newMeter(defaultLogger) - // Call Run three times in quick succession — only the first must start the loop. m.Run(50 * time.Millisecond) m.Run(50 * time.Millisecond) m.Run(50 * time.Millisecond) - // Give one tick to fire so we know the loop is alive. time.Sleep(80 * time.Millisecond) - // Stop and confirm the channel closes cleanly (not double-closed/panicked). assert.NotPanics(t, m.Stop) - // Confirm done channel is closed (reading from a closed channel returns immediately). select { case <-m.done: - // expected + // expected: channel is closed default: t.Fatal("done channel should be closed after Stop()") } } -// TestMeterStop_Idempotent verifies that calling Stop multiple times never panics. -func TestMeterStop_Idempotent(t *testing.T) { - m := newMeter(defaultLogger) - m.Run(100 * time.Millisecond) - - assert.NotPanics(t, func() { - m.Stop() - m.Stop() - m.Stop() - }) -} - -// TestMeterStop_WithoutRun verifies Stop is safe even when Run was never called. -func TestMeterStop_WithoutRun(t *testing.T) { - m := newMeter(defaultLogger) - - assert.NotPanics(t, func() { - m.Stop() - }) -} - // TestMeterRun_LoopExitsOnStop verifies the collection goroutine stops when Stop is called. func TestMeterRun_LoopExitsOnStop(t *testing.T) { m := newMeter(defaultLogger) m.Run(50 * time.Millisecond) - - // Let at least one tick fire. time.Sleep(80 * time.Millisecond) stopped := make(chan struct{}) @@ -103,12 +76,50 @@ func TestMeterRun_ConcurrentCallsSafe(t *testing.T) { } wg.Wait() - // Should stop cleanly with no panic. assert.NotPanics(t, m.Stop) } +// TestMeterStop covers idempotency and nil-receiver safety of Stop, and calling +// Stop before Run has been called. +func TestMeterStop(t *testing.T) { + tests := []struct { + name string + setup func() *meterS + stop func(*meterS) + }{ + { + name: "idempotent: multiple Stop calls do not panic", + setup: func() *meterS { m := newMeter(defaultLogger); m.Run(100 * time.Millisecond); return m }, + stop: func(m *meterS) { m.Stop(); m.Stop(); m.Stop() }, + }, + { + name: "safe when Run was never called", + setup: func() *meterS { return newMeter(defaultLogger) }, + stop: func(m *meterS) { m.Stop() }, + }, + { + name: "nil receiver is a no-op", + setup: func() *meterS { return nil }, + stop: func(m *meterS) { m.Stop() }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := tt.setup() + assert.NotPanics(t, func() { tt.stop(m) }) + }) + } +} + +// TestMeterRun_NilReceiver verifies Run is a no-op when called on a nil *meterS. +func TestMeterRun_NilReceiver(t *testing.T) { + var m *meterS + assert.NotPanics(t, func() { m.Run(50 * time.Millisecond) }) +} + // TestMetricsOptions_GetTransmissionInterval_Default verifies that an unconfigured -// MetricsOptions returns zero, as callers are responsible for applying the default. +// MetricsOptions returns zero (callers are responsible for applying the default). func TestMetricsOptions_GetTransmissionInterval_Default(t *testing.T) { opts := &MetricsOptions{} assert.Equal(t, time.Duration(0), opts.getTransmissionInterval()) @@ -158,3 +169,44 @@ func TestMeterCollectMemoryMetrics(t *testing.T) { assert.NotZero(t, mem.Sys) assert.NotZero(t, mem.HeapAlloc) } + +// TestMeterRun_SendMetrics_SensorNil verifies the meter loop skips SendMetrics when +// the global sensor is nil (agent not ready). The loop stays alive and stops cleanly. +func TestMeterRun_SendMetrics_SensorNil(t *testing.T) { + m := newMeter(defaultLogger) + m.Run(20 * time.Millisecond) + // Give several ticks to fire; none should panic because isAgentReady returns false. + time.Sleep(80 * time.Millisecond) + assert.NotPanics(t, m.Stop) +} + +// TestMeterRun_SendMetrics_AgentReady verifies that the tick handler enters the +// SendMetrics path when a ready sensor is present, without panic. +func TestMeterRun_SendMetrics_AgentReady(t *testing.T) { + // Build a sensor with a ready mock agent. + mock := &sensorS{ + options: DefaultOptions(), + meter: newMeter(defaultLogger), + } + mock.setLogger(defaultLogger) + mock.setAgent(alwaysReadyClient{}) + + // Protect all writes to sensor with muSensor so isAgentReady() (which holds + // muSensor.RLock) does not race with this goroutine. + muSensor.Lock() + orig := sensor + sensor = mock + muSensor.Unlock() + + m := newMeter(defaultLogger) + m.Run(20 * time.Millisecond) + time.Sleep(80 * time.Millisecond) + + // Stop the meter BEFORE restoring sensor so the background goroutine is dead + // before we mutate the shared variable again. + assert.NotPanics(t, m.Stop) + + muSensor.Lock() + sensor = orig + muSensor.Unlock() +} From 84bfc6f69049e3a089e0e29004f5d66bd1d5055b Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 3 Aug 2026 00:22:52 +0530 Subject: [PATCH 19/21] refactor: replace poll_rate range clamping with canonical-set validation --- README.md | 15 ++++++------- fsm.go | 28 ++++++++++++++++++++++++ fsm_test.go | 59 ++++++++++++++++++++++++++++++++++++--------------- meter.go | 21 +++++++----------- meter_test.go | 20 +++++++++-------- 5 files changed, 96 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index dfaaef82a..801ea68fa 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,18 @@ In the agent's `configuration.yaml`: ```yaml # Configure metrics transmission interval for Go applications com.instana.plugin.golang: - poll_rate: 5 # Valid range: 1-600 (seconds) + poll_rate: 5 # seconds ``` **Valid Values:** -- Any value between `1` and `600` seconds -- Default: `1` second (if not configured) -- Minimum: `1` second -- Maximum: `600` seconds + +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, defaults to 1 second -- Values less than 1 will be set to the minimum value of 1 second. -- Values greater than 600 will be set to the maximum value of 600 seconds. +- 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] diff --git a/fsm.go b/fsm.go index 40b700818..1a226a32c 100644 --- a/fsm.go +++ b/fsm.go @@ -5,6 +5,7 @@ package instana import ( "context" + "encoding/json" "fmt" "math" "net" @@ -29,6 +30,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 @@ -267,6 +283,10 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { sensor.options.Tracer.CollectableHTTPHeaders = resp.getExtraHTTPHeaders() } + b, _ := json.MarshalIndent(resp, "", " ") + fmt.Println("resp:", string(b)) + + r.logger.Debug("resp:", resp) r.applyDisableTracingConfig(resp) r.applyMetricsPollRateConfig(resp) @@ -274,6 +294,9 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { } // 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 { @@ -288,6 +311,11 @@ func (r *fsmS) applyMetricsPollRateConfig(resp agentResponse) { 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) } diff --git a/fsm_test.go b/fsm_test.go index ef80ca879..87855e30a 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -21,6 +21,7 @@ import ( type testLogger struct { infoMsg string + warnMsg string errMsg string } @@ -28,7 +29,9 @@ 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...) } @@ -638,49 +641,64 @@ func TestApplyDisableTracingConfig(t *testing.T) { func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { tests := []struct { - name string - pollRate int - expectedSecs int + name string + pollRate int + expectedSecs int + expectWarn bool }{ { - name: "Valid 1 second (minimum)", + name: "Canonical 1 second — no warning", pollRate: 1, expectedSecs: 1, + expectWarn: false, }, { - name: "Valid 5 seconds", + name: "Canonical 5 seconds — no warning", pollRate: 5, expectedSecs: 5, + expectWarn: false, }, { - name: "Valid 10 seconds", + name: "Canonical 10 seconds — no warning", pollRate: 10, expectedSecs: 10, + expectWarn: false, }, { - name: "Valid 60 seconds", + name: "Canonical 60 seconds — no warning", pollRate: 60, expectedSecs: 60, + expectWarn: false, }, { - name: "Valid 600 seconds (maximum)", + 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 seconds - sets to minimum (1)", + name: "Zero — uses default (1s), no warning", pollRate: 0, expectedSecs: 1, + expectWarn: false, }, { - name: "Negative value - sets to minimum (1)", + name: "Negative value (-5) — uses default (1s), no warning", pollRate: -5, expectedSecs: 1, - }, - { - name: "Exceeds maximum (5000) - sets to maximum (600)", - pollRate: 5000, - expectedSecs: 600, + expectWarn: false, }, } @@ -690,8 +708,9 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { sensor = newSensor(DefaultOptions()) defer func() { sensor = nil }() + tLogger := &testLogger{} fsm := &fsmS{ - logger: &testLogger{}, + logger: tLogger, } resp := agentResponse{ @@ -706,6 +725,12 @@ func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { 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) + } }) } } diff --git a/meter.go b/meter.go index d4608d251..5ff40ed32 100644 --- a/meter.go +++ b/meter.go @@ -13,10 +13,8 @@ import ( ) const ( - // Metrics transmission interval constraints (in seconds) + // defaultTransmissionInterval is the fallback metrics transmission interval in seconds. defaultTransmissionInterval = 1 - minTransmissionInterval = 1 - maxTransmissionInterval = 600 ) // SnapshotS struct to hold snapshot data @@ -56,19 +54,16 @@ func (m *MetricsOptions) getTransmissionInterval() time.Duration { // setTransmissionInterval sets the metrics transmission interval. // This is an internal method called when agent configuration is received during -// the initial handshake. Valid range: minTransmissionInterval-maxTransmissionInterval seconds. -// Values outside the range are clamped to the nearest bound. +// the initial handshake. The only local constraint enforced here is that the value +// must be positive (> 0); range and canonical-set validation is the responsibility +// of the Instana Agent. Non-positive values fall back to defaultTransmissionInterval. func (m *MetricsOptions) setTransmissionInterval(seconds int) { var interval time.Duration - if seconds < minTransmissionInterval { - defaultLogger.Warn("poll_rate value from agent (", seconds, ") is less than minimum. Setting to minimum value of ", - minTransmissionInterval, " second.") - interval = minTransmissionInterval * time.Second - } else if seconds > maxTransmissionInterval { - defaultLogger.Warn("poll_rate value from agent (", seconds, ") exceeds maximum. Setting to maximum value of ", - maxTransmissionInterval, " seconds.") - interval = maxTransmissionInterval * time.Second + if seconds <= 0 { + defaultLogger.Error("poll_rate value from agent (", seconds, ") is not positive. Using default of ", + defaultTransmissionInterval, " second.") + interval = defaultTransmissionInterval * time.Second } else { interval = time.Duration(seconds) * time.Second defaultLogger.Info("Metrics transmission interval set to ", seconds, " second(s) from agent configuration") diff --git a/meter_test.go b/meter_test.go index 0637cd2df..53b0e65d1 100644 --- a/meter_test.go +++ b/meter_test.go @@ -125,21 +125,23 @@ func TestMetricsOptions_GetTransmissionInterval_Default(t *testing.T) { assert.Equal(t, time.Duration(0), opts.getTransmissionInterval()) } -// TestMetricsOptions_SetTransmissionInterval verifies clamping and valid values. +// TestMetricsOptions_SetTransmissionInterval verifies that positive values are stored +// as-is and non-positive values fall back to the default (1 second). func TestMetricsOptions_SetTransmissionInterval(t *testing.T) { tests := []struct { name string seconds int expected time.Duration }{ - {"minimum (1s)", 1, 1 * time.Second}, - {"valid 5s", 5, 5 * time.Second}, - {"valid 60s", 60, 60 * time.Second}, - {"valid 300s", 300, 300 * time.Second}, - {"maximum (600s)", 600, 600 * time.Second}, - {"zero clamps to minimum", 0, 1 * time.Second}, - {"negative clamps to minimum", -1, 1 * time.Second}, - {"exceeds maximum clamps to 600s", 1000, 600 * time.Second}, + {"minimum canonical value (1s)", 1, 1 * time.Second}, + {"canonical 5s", 5, 5 * time.Second}, + {"canonical 60s", 60, 60 * time.Second}, + {"canonical 300s", 300, 300 * time.Second}, + {"canonical 600s", 600, 600 * time.Second}, + {"non-canonical positive value stored as-is (7s)", 7, 7 * time.Second}, + {"large positive value stored as-is (1000s)", 1000, 1000 * time.Second}, + {"zero uses default (1s)", 0, 1 * time.Second}, + {"negative uses default (1s)", -1, 1 * time.Second}, } for _, tt := range tests { From e60ec05681c508f572a8593642187d1cf97e91e3 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 3 Aug 2026 17:51:10 +0530 Subject: [PATCH 20/21] fix: corrected format of fsm_test --- fsm_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fsm_test.go b/fsm_test.go index 87855e30a..1f45821c1 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -641,10 +641,10 @@ func TestApplyDisableTracingConfig(t *testing.T) { func Test_fsmS_applyMetricsPollRateConfig(t *testing.T) { tests := []struct { - name string - pollRate int - expectedSecs int - expectWarn bool + name string + pollRate int + expectedSecs int + expectWarn bool }{ { name: "Canonical 1 second — no warning", From 7ea288588573cd86608777b5773f3e70571b86cf Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Thu, 6 Aug 2026 15:23:15 +0530 Subject: [PATCH 21/21] fix: remove unwanted debug logs --- fsm.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fsm.go b/fsm.go index 1a226a32c..216bfce9d 100644 --- a/fsm.go +++ b/fsm.go @@ -5,7 +5,6 @@ package instana import ( "context" - "encoding/json" "fmt" "math" "net" @@ -283,10 +282,6 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { sensor.options.Tracer.CollectableHTTPHeaders = resp.getExtraHTTPHeaders() } - b, _ := json.MarshalIndent(resp, "", " ") - fmt.Println("resp:", string(b)) - - r.logger.Debug("resp:", resp) r.applyDisableTracingConfig(resp) r.applyMetricsPollRateConfig(resp)