diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-07 16:32:10 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-07 16:32:10 +0200 |
| commit | 3fd46f3977fb650974e5e936cba362c787c00637 (patch) | |
| tree | b49111ddd0b7af4a007bca6a304dba10efcd88ff /internal/metrics | |
reimport this PoC
Diffstat (limited to 'internal/metrics')
| -rw-r--r-- | internal/metrics/generator.go | 85 | ||||
| -rw-r--r-- | internal/metrics/generator_test.go | 53 | ||||
| -rw-r--r-- | internal/metrics/sample.go | 34 | ||||
| -rw-r--r-- | internal/metrics/sample_test.go | 160 |
4 files changed, 332 insertions, 0 deletions
diff --git a/internal/metrics/generator.go b/internal/metrics/generator.go new file mode 100644 index 0000000..c85906a --- /dev/null +++ b/internal/metrics/generator.go @@ -0,0 +1,85 @@ +package metrics + +import ( + "math/rand" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + minTemperature = 15.0 + maxTemperature = 35.0 + maxConnections = 100 + maxRequests = 10 +) + +var ( + jobTypes = []string{"email", "report", "backup"} + statuses = []string{"success", "failed"} +) + +// Collectors holds Prometheus metric collectors for realtime mode +type Collectors struct { + RequestsTotal prometheus.Counter + ActiveConnections prometheus.Gauge + TemperatureCelsius prometheus.Gauge + RequestDuration prometheus.Histogram + JobsProcessed *prometheus.CounterVec +} + +// NewCollectors creates new Prometheus metric collectors for testing. +// All metrics are prefixed with "epimetheus_test_" to clearly indicate +// they are generated by the prometheus-pusher test/demo functionality. +func NewCollectors() Collectors { + return Collectors{ + RequestsTotal: prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "epimetheus_test_requests_total", + Help: "Total number of requests processed (test metric)", + }, + ), + ActiveConnections: prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "epimetheus_test_active_connections", + Help: "Number of currently active connections (test metric)", + }, + ), + TemperatureCelsius: prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "epimetheus_test_temperature_celsius", + Help: "Current temperature in Celsius (test metric)", + }, + ), + RequestDuration: prometheus.NewHistogram( + prometheus.HistogramOpts{ + Name: "epimetheus_test_request_duration_seconds", + Help: "Histogram of request duration in seconds (test metric)", + Buckets: prometheus.DefBuckets, + }, + ), + JobsProcessed: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "epimetheus_test_jobs_processed_total", + Help: "Total number of jobs processed by type (test metric)", + }, + []string{"job_type", "status"}, + ), + } +} + +// Simulate generates random metric values for the collectors +func (c Collectors) Simulate() { + c.RequestsTotal.Add(float64(rand.Intn(maxRequests) + 1)) + c.ActiveConnections.Set(float64(rand.Intn(maxConnections))) + c.TemperatureCelsius.Set(minTemperature + rand.Float64()*(maxTemperature-minTemperature)) + + for i := 0; i < rand.Intn(5)+1; i++ { + duration := rand.Float64() * 2 + c.RequestDuration.Observe(duration) + } + + for _, jobType := range jobTypes { + status := statuses[rand.Intn(len(statuses))] + c.JobsProcessed.WithLabelValues(jobType, status).Add(float64(rand.Intn(5))) + } +} diff --git a/internal/metrics/generator_test.go b/internal/metrics/generator_test.go new file mode 100644 index 0000000..69395eb --- /dev/null +++ b/internal/metrics/generator_test.go @@ -0,0 +1,53 @@ +package metrics + +import ( + "testing" +) + +func TestNewCollectors(t *testing.T) { + collectors := NewCollectors() + + if collectors.RequestsTotal == nil { + t.Error("RequestsTotal should not be nil") + } + if collectors.ActiveConnections == nil { + t.Error("ActiveConnections should not be nil") + } + if collectors.TemperatureCelsius == nil { + t.Error("TemperatureCelsius should not be nil") + } + if collectors.RequestDuration == nil { + t.Error("RequestDuration should not be nil") + } + if collectors.JobsProcessed == nil { + t.Error("JobsProcessed should not be nil") + } +} + +func TestCollectors_Simulate(t *testing.T) { + collectors := NewCollectors() + + // Should not panic + collectors.Simulate() + + // Run multiple times to test randomness + for i := 0; i < 10; i++ { + collectors.Simulate() + } +} + +func TestCollectors_SimulateMetrics(t *testing.T) { + collectors := NewCollectors() + + // Test that metrics get values after simulation + collectors.Simulate() + + // We can't easily inspect the values without the prometheus client, + // but we can verify the collectors were created properly + if collectors.RequestsTotal == nil { + t.Error("RequestsTotal not initialized") + } + if collectors.JobsProcessed == nil { + t.Error("JobsProcessed not initialized") + } +} diff --git a/internal/metrics/sample.go b/internal/metrics/sample.go new file mode 100644 index 0000000..04360f5 --- /dev/null +++ b/internal/metrics/sample.go @@ -0,0 +1,34 @@ +package metrics + +import "time" + +// Sample represents a single metric sample with timestamp +type Sample struct { + MetricName string + Labels map[string]string + Value float64 + Timestamp time.Time +} + +// NewSample creates a new Sample +func NewSample(name string, labels map[string]string, value float64, timestamp time.Time) Sample { + if labels == nil { + labels = make(map[string]string) + } + return Sample{ + MetricName: name, + Labels: labels, + Value: value, + Timestamp: timestamp, + } +} + +// Age returns how old the sample is +func (s Sample) Age() time.Duration { + return time.Since(s.Timestamp) +} + +// IsRecent returns true if the sample is recent enough for realtime ingestion +func (s Sample) IsRecent(threshold time.Duration) bool { + return s.Age() < threshold +} diff --git a/internal/metrics/sample_test.go b/internal/metrics/sample_test.go new file mode 100644 index 0000000..2ffd78b --- /dev/null +++ b/internal/metrics/sample_test.go @@ -0,0 +1,160 @@ +package metrics + +import ( + "testing" + "time" +) + +func TestNewSample(t *testing.T) { + tests := []struct { + name string + metric string + labels map[string]string + value float64 + timestamp time.Time + wantNil bool + }{ + { + name: "with labels", + metric: "test_metric", + labels: map[string]string{"env": "prod", "host": "server1"}, + value: 42.5, + timestamp: time.Now(), + wantNil: false, + }, + { + name: "nil labels initialized", + metric: "test_metric", + labels: nil, + value: 100, + timestamp: time.Now(), + wantNil: false, + }, + { + name: "empty labels", + metric: "test_metric", + labels: map[string]string{}, + value: 0, + timestamp: time.Now(), + wantNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sample := NewSample(tt.metric, tt.labels, tt.value, tt.timestamp) + + if sample.MetricName != tt.metric { + t.Errorf("MetricName = %v, want %v", sample.MetricName, tt.metric) + } + if sample.Value != tt.value { + t.Errorf("Value = %v, want %v", sample.Value, tt.value) + } + if sample.Labels == nil { + t.Error("Labels should never be nil") + } + if !sample.Timestamp.Equal(tt.timestamp) { + t.Errorf("Timestamp = %v, want %v", sample.Timestamp, tt.timestamp) + } + }) + } +} + +func TestSample_Age(t *testing.T) { + tests := []struct { + name string + sample Sample + wantNear time.Duration + }{ + { + name: "recent sample", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-5 * time.Minute), + }, + wantNear: 5 * time.Minute, + }, + { + name: "old sample", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-1 * time.Hour), + }, + wantNear: 1 * time.Hour, + }, + { + name: "very recent", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-10 * time.Second), + }, + wantNear: 10 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + age := tt.sample.Age() + // Allow 1 second tolerance for test execution time + if age < tt.wantNear-time.Second || age > tt.wantNear+time.Second { + t.Errorf("Age() = %v, want near %v", age, tt.wantNear) + } + }) + } +} + +func TestSample_IsRecent(t *testing.T) { + threshold := 5 * time.Minute + + tests := []struct { + name string + sample Sample + threshold time.Duration + want bool + }{ + { + name: "within threshold", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-2 * time.Minute), + }, + threshold: threshold, + want: true, + }, + { + name: "beyond threshold", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-10 * time.Minute), + }, + threshold: threshold, + want: false, + }, + { + name: "exactly at threshold", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-5 * time.Minute), + }, + threshold: threshold, + want: false, + }, + { + name: "very recent", + sample: Sample{ + MetricName: "test", + Timestamp: time.Now().Add(-10 * time.Second), + }, + threshold: threshold, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.sample.IsRecent(tt.threshold); got != tt.want { + t.Errorf("IsRecent() = %v, want %v (age: %v)", got, tt.want, tt.sample.Age()) + } + }) + } +} |
