From c1d33173b918a29d2e14b1b297c529d5ca9d6d99 Mon Sep 17 00:00:00 2001 From: Angith Jayan Date: Mon, 20 Apr 2026 10:42:16 +0530 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 }