summaryrefslogtreecommitdiff
path: root/internal/metrics
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-07 16:32:10 +0200
committerPaul Buetow <paul@buetow.org>2026-02-07 16:32:10 +0200
commit3fd46f3977fb650974e5e936cba362c787c00637 (patch)
treeb49111ddd0b7af4a007bca6a304dba10efcd88ff /internal/metrics
reimport this PoC
Diffstat (limited to 'internal/metrics')
-rw-r--r--internal/metrics/generator.go85
-rw-r--r--internal/metrics/generator_test.go53
-rw-r--r--internal/metrics/sample.go34
-rw-r--r--internal/metrics/sample_test.go160
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())
+ }
+ })
+ }
+}