Skip to content

Commit 29bfb41

Browse files
committed
test: add comprehensive tests for metrics collector and otel config
Add collector tests: - CPU calculation with various edge cases (no previous stats, rapid scrape, counter rollover, zero system delta, fallback CPU count) - Stale CPU stats cleanup - CollectAndServe concurrency safety - Nil docker client handling Add OTel config validation tests: - YAML syntax validation - Required sections (receivers, exporters, service) - Prometheus receiver for DBLab scraping - OTLP exporter presence - Metrics pipeline configuration - DBLab scrape target configuration
1 parent cfc2fc8 commit 29bfb41

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

engine/configs/otel_config_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
2025 © Postgres.ai
3+
*/
4+
5+
package configs
6+
7+
import (
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
func TestOtelCollectorConfigValid(t *testing.T) {
17+
data, err := os.ReadFile("otel-collector.example.yml")
18+
require.NoError(t, err, "failed to read otel-collector.example.yml")
19+
20+
var config map[string]interface{}
21+
err = yaml.Unmarshal(data, &config)
22+
require.NoError(t, err, "otel config is not valid yaml")
23+
24+
assert.Contains(t, config, "receivers", "config must have receivers section")
25+
assert.Contains(t, config, "exporters", "config must have exporters section")
26+
assert.Contains(t, config, "service", "config must have service section")
27+
}
28+
29+
func TestOtelCollectorConfigHasPrometheusReceiver(t *testing.T) {
30+
data, err := os.ReadFile("otel-collector.example.yml")
31+
require.NoError(t, err)
32+
33+
var config map[string]interface{}
34+
err = yaml.Unmarshal(data, &config)
35+
require.NoError(t, err)
36+
37+
receivers, ok := config["receivers"].(map[string]interface{})
38+
require.True(t, ok, "receivers must be a map")
39+
40+
assert.Contains(t, receivers, "prometheus", "must have prometheus receiver for dblab scraping")
41+
}
42+
43+
func TestOtelCollectorConfigHasOTLPExporter(t *testing.T) {
44+
data, err := os.ReadFile("otel-collector.example.yml")
45+
require.NoError(t, err)
46+
47+
var config map[string]interface{}
48+
err = yaml.Unmarshal(data, &config)
49+
require.NoError(t, err)
50+
51+
exporters, ok := config["exporters"].(map[string]interface{})
52+
require.True(t, ok, "exporters must be a map")
53+
54+
assert.Contains(t, exporters, "otlp", "must have otlp exporter for otel backends")
55+
}
56+
57+
func TestOtelCollectorConfigHasMetricsPipeline(t *testing.T) {
58+
data, err := os.ReadFile("otel-collector.example.yml")
59+
require.NoError(t, err)
60+
61+
var config map[string]interface{}
62+
err = yaml.Unmarshal(data, &config)
63+
require.NoError(t, err)
64+
65+
service, ok := config["service"].(map[string]interface{})
66+
require.True(t, ok, "service must be a map")
67+
68+
pipelines, ok := service["pipelines"].(map[string]interface{})
69+
require.True(t, ok, "service must have pipelines")
70+
71+
assert.Contains(t, pipelines, "metrics", "must have metrics pipeline")
72+
}
73+
74+
func TestOtelCollectorConfigDBLabScrapeTarget(t *testing.T) {
75+
data, err := os.ReadFile("otel-collector.example.yml")
76+
require.NoError(t, err)
77+
78+
var config map[string]interface{}
79+
err = yaml.Unmarshal(data, &config)
80+
require.NoError(t, err)
81+
82+
receivers := config["receivers"].(map[string]interface{})
83+
prometheus := receivers["prometheus"].(map[string]interface{})
84+
promConfig := prometheus["config"].(map[string]interface{})
85+
scrapeConfigs := promConfig["scrape_configs"].([]interface{})
86+
87+
found := false
88+
89+
for _, sc := range scrapeConfigs {
90+
scrapeConfig := sc.(map[string]interface{})
91+
if scrapeConfig["job_name"] == "dblab" {
92+
found = true
93+
94+
staticConfigs := scrapeConfig["static_configs"].([]interface{})
95+
require.NotEmpty(t, staticConfigs)
96+
97+
firstConfig := staticConfigs[0].(map[string]interface{})
98+
targets := firstConfig["targets"].([]interface{})
99+
assert.Contains(t, targets, "localhost:2345", "must scrape dblab default port")
100+
101+
break
102+
}
103+
}
104+
105+
assert.True(t, found, "must have dblab scrape job")
106+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
2025 © Postgres.ai
3+
*/
4+
5+
package metrics
6+
7+
import (
8+
"context"
9+
"net/http"
10+
"net/http/httptest"
11+
"sync"
12+
"testing"
13+
"time"
14+
15+
"github.com/docker/docker/api/types/container"
16+
"github.com/prometheus/client_golang/prometheus"
17+
"github.com/prometheus/client_golang/prometheus/promhttp"
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
21+
"gitlab.com/postgres-ai/database-lab/v3/pkg/config/global"
22+
)
23+
24+
func TestNewCollector(t *testing.T) {
25+
m := NewMetrics()
26+
engProps := &global.EngineProps{InstanceID: "test-instance"}
27+
startedAt := time.Now()
28+
29+
c := NewCollector(m, nil, nil, nil, engProps, nil, startedAt)
30+
31+
require.NotNil(t, c)
32+
assert.NotNil(t, c.metrics)
33+
assert.NotNil(t, c.prevCPUStats)
34+
assert.Equal(t, engProps, c.engProps)
35+
assert.Equal(t, startedAt, c.startedAt)
36+
}
37+
38+
func TestCalculateCPUPercent_NoPreviousStats(t *testing.T) {
39+
m := NewMetrics()
40+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
41+
42+
stats := &container.StatsResponse{
43+
CPUStats: container.CPUStats{
44+
CPUUsage: container.CPUUsage{TotalUsage: 1000000000},
45+
SystemUsage: 5000000000,
46+
OnlineCPUs: 4,
47+
},
48+
}
49+
50+
result := c.calculateCPUPercent("clone-1", stats)
51+
assert.Equal(t, float64(0), result)
52+
53+
_, exists := c.prevCPUStats["clone-1"]
54+
assert.True(t, exists)
55+
}
56+
57+
func TestCalculateCPUPercent_WithPreviousStats(t *testing.T) {
58+
m := NewMetrics()
59+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
60+
61+
c.prevCPUStats["clone-1"] = containerCPUState{
62+
totalUsage: 1000000000,
63+
systemUsage: 5000000000,
64+
timestamp: time.Now().Add(-2 * time.Second),
65+
}
66+
67+
stats := &container.StatsResponse{
68+
CPUStats: container.CPUStats{
69+
CPUUsage: container.CPUUsage{TotalUsage: 2000000000},
70+
SystemUsage: 10000000000,
71+
OnlineCPUs: 4,
72+
},
73+
}
74+
75+
result := c.calculateCPUPercent("clone-1", stats)
76+
assert.Greater(t, result, float64(0))
77+
}
78+
79+
func TestCalculateCPUPercent_RapidScrape(t *testing.T) {
80+
m := NewMetrics()
81+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
82+
83+
c.prevCPUStats["clone-1"] = containerCPUState{
84+
totalUsage: 1000000000,
85+
systemUsage: 5000000000,
86+
timestamp: time.Now().Add(-100 * time.Millisecond),
87+
}
88+
89+
stats := &container.StatsResponse{
90+
CPUStats: container.CPUStats{
91+
CPUUsage: container.CPUUsage{TotalUsage: 2000000000},
92+
SystemUsage: 10000000000,
93+
OnlineCPUs: 4,
94+
},
95+
}
96+
97+
result := c.calculateCPUPercent("clone-1", stats)
98+
assert.Equal(t, float64(0), result)
99+
}
100+
101+
func TestCalculateCPUPercent_CounterRollover(t *testing.T) {
102+
m := NewMetrics()
103+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
104+
105+
c.prevCPUStats["clone-1"] = containerCPUState{
106+
totalUsage: 5000000000,
107+
systemUsage: 10000000000,
108+
timestamp: time.Now().Add(-2 * time.Second),
109+
}
110+
111+
stats := &container.StatsResponse{
112+
CPUStats: container.CPUStats{
113+
CPUUsage: container.CPUUsage{TotalUsage: 1000000000},
114+
SystemUsage: 2000000000,
115+
OnlineCPUs: 4,
116+
},
117+
}
118+
119+
result := c.calculateCPUPercent("clone-1", stats)
120+
assert.Equal(t, float64(0), result)
121+
}
122+
123+
func TestCalculateCPUPercent_ZeroSystemDelta(t *testing.T) {
124+
m := NewMetrics()
125+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
126+
127+
c.prevCPUStats["clone-1"] = containerCPUState{
128+
totalUsage: 1000000000,
129+
systemUsage: 5000000000,
130+
timestamp: time.Now().Add(-2 * time.Second),
131+
}
132+
133+
stats := &container.StatsResponse{
134+
CPUStats: container.CPUStats{
135+
CPUUsage: container.CPUUsage{TotalUsage: 2000000000},
136+
SystemUsage: 5000000000,
137+
OnlineCPUs: 4,
138+
},
139+
}
140+
141+
result := c.calculateCPUPercent("clone-1", stats)
142+
assert.Equal(t, float64(0), result)
143+
}
144+
145+
func TestCalculateCPUPercent_FallbackCPUCount(t *testing.T) {
146+
m := NewMetrics()
147+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
148+
149+
c.prevCPUStats["clone-1"] = containerCPUState{
150+
totalUsage: 1000000000,
151+
systemUsage: 5000000000,
152+
timestamp: time.Now().Add(-2 * time.Second),
153+
}
154+
155+
stats := &container.StatsResponse{
156+
CPUStats: container.CPUStats{
157+
CPUUsage: container.CPUUsage{
158+
TotalUsage: 2000000000,
159+
PercpuUsage: []uint64{500000000, 500000000, 500000000, 500000000},
160+
},
161+
SystemUsage: 10000000000,
162+
OnlineCPUs: 0,
163+
},
164+
}
165+
166+
result := c.calculateCPUPercent("clone-1", stats)
167+
assert.Greater(t, result, float64(0))
168+
}
169+
170+
func TestCleanupStaleCPUStats(t *testing.T) {
171+
m := NewMetrics()
172+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
173+
174+
c.prevCPUStats["clone-1"] = containerCPUState{totalUsage: 100}
175+
c.prevCPUStats["clone-2"] = containerCPUState{totalUsage: 200}
176+
c.prevCPUStats["clone-3"] = containerCPUState{totalUsage: 300}
177+
178+
activeCloneIDs := map[string]struct{}{
179+
"clone-1": {},
180+
"clone-3": {},
181+
}
182+
183+
c.cleanupStaleCPUStats(activeCloneIDs)
184+
185+
assert.Len(t, c.prevCPUStats, 2)
186+
_, exists1 := c.prevCPUStats["clone-1"]
187+
_, exists2 := c.prevCPUStats["clone-2"]
188+
_, exists3 := c.prevCPUStats["clone-3"]
189+
190+
assert.True(t, exists1)
191+
assert.False(t, exists2)
192+
assert.True(t, exists3)
193+
}
194+
195+
func TestCleanupStaleCPUStats_EmptyActive(t *testing.T) {
196+
m := NewMetrics()
197+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
198+
199+
c.prevCPUStats["clone-1"] = containerCPUState{totalUsage: 100}
200+
c.prevCPUStats["clone-2"] = containerCPUState{totalUsage: 200}
201+
202+
c.cleanupStaleCPUStats(map[string]struct{}{})
203+
204+
assert.Len(t, c.prevCPUStats, 0)
205+
}
206+
207+
func TestCollectAndServe_Concurrency(t *testing.T) {
208+
m := NewMetrics()
209+
reg := prometheus.NewRegistry()
210+
err := m.Register(reg)
211+
require.NoError(t, err)
212+
213+
engProps := &global.EngineProps{InstanceID: "test-instance"}
214+
c := NewCollector(m, nil, nil, nil, engProps, nil, time.Now())
215+
216+
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
217+
218+
var wg sync.WaitGroup
219+
errors := make(chan error, 10)
220+
221+
for i := 0; i < 10; i++ {
222+
wg.Add(1)
223+
224+
go func() {
225+
defer wg.Done()
226+
227+
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
228+
rec := httptest.NewRecorder()
229+
230+
c.CollectAndServe(context.Background(), handler, rec, req)
231+
232+
if rec.Code != http.StatusOK {
233+
errors <- assert.AnError
234+
}
235+
}()
236+
}
237+
238+
wg.Wait()
239+
close(errors)
240+
241+
for err := range errors {
242+
t.Errorf("concurrent request failed: %v", err)
243+
}
244+
}
245+
246+
func TestGetContainerStats_NilDockerClient(t *testing.T) {
247+
m := NewMetrics()
248+
c := NewCollector(m, nil, nil, nil, &global.EngineProps{}, nil, time.Now())
249+
250+
result := c.getContainerStats(context.Background(), nil)
251+
252+
assert.NotNil(t, result)
253+
assert.Len(t, result, 0)
254+
}

0 commit comments

Comments
 (0)