Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,32 @@ 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.

#### 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.

**Configuration:**

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)
```

**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.

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

func (a *agentResponse) getExtraHTTPHeaders() []string {
Expand Down
21 changes: 21 additions & 0 deletions fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,30 @@ 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.
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")
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 {
Expand Down Expand Up @@ -420,6 +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())
}

func (r *fsmS) cpuSetFileContent(pid int) string {
Expand Down
74 changes: 74 additions & 0 deletions fsm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
135 changes: 115 additions & 20 deletions meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ package instana

import (
"runtime"
"sync"
"time"

"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

Expand All @@ -23,8 +31,57 @@ 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.
// This configuration is managed internally and populated from agent configuration.
type MetricsOptions struct {
mu sync.RWMutex
transmissionInterval time.Duration
}

// 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 defaultTransmissionInterval * time.Second
}
return m.transmissionInterval
}

// 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.
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")
}

func newMeter(logger LeveledLogger) *meterS {
Expand All @@ -35,31 +92,69 @@ func newMeter(logger LeveledLogger) *meterS {
}
}

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
}

func (m *meterS) Run(collectInterval time.Duration) {
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())
}()
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)
}

func (m *meterS) Stop() {
m.done <- struct{}{}
m.mu.Lock()
defer m.mu.Unlock()

if m == nil {
return
}

if m.running {
close(m.done)
m.running = false
}
}

func (m *meterS) collectMemoryMetrics() acceptor.MemoryStats {
Expand Down
Loading
Loading