Skip to content

Commit e52bd2d

Browse files
committed
feat(energy_zone): Added Energy Zone Collector
Signed-off-by: Vimal Kumar <vimal78@gmail.com>
1 parent 2ad09ba commit e52bd2d

File tree

9 files changed

+313
-33
lines changed

9 files changed

+313
-33
lines changed

cmd/kepler/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ func createServices(logger *slog.Logger, cfg *config.Config) ([]service.Service,
217217
pm,
218218
prometheus.WithLogger(logger),
219219
prometheus.WithProcFSPath(cfg.Host.ProcFS),
220+
prometheus.WithSysFSPath(cfg.Host.SysFS),
220221
)
221222
if err != nil {
222223
return nil, fmt.Errorf("failed to create Prometheus collectors: %w", err)

internal/exporter/prometheus/collector/cpuinfo.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,8 @@ import (
88
"sync"
99

1010
prom "github.com/prometheus/client_golang/prometheus"
11-
"github.com/prometheus/procfs"
1211
)
1312

14-
// procFS is an interface for CPUInfo.
15-
type procFS interface {
16-
CPUInfo() ([]procfs.CPUInfo, error)
17-
}
18-
19-
type realProcFS struct {
20-
fs procfs.FS
21-
}
22-
23-
func (r *realProcFS) CPUInfo() ([]procfs.CPUInfo, error) {
24-
return r.fs.CPUInfo()
25-
}
26-
27-
func newProcFS(mountPoint string) (procFS, error) {
28-
fs, err := procfs.NewFS(mountPoint)
29-
if err != nil {
30-
return nil, err
31-
}
32-
return &realProcFS{fs: fs}, nil
33-
}
34-
3513
// cpuInfoCollector collects CPU info metrics from procfs.
3614
type cpuInfoCollector struct {
3715
sync.Mutex

internal/exporter/prometheus/collector/cpuinfo_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"sync"
99
"testing"
1010

11-
"github.com/prometheus/client_golang/prometheus"
11+
prom "github.com/prometheus/client_golang/prometheus"
1212
dto "github.com/prometheus/client_model/go"
1313
"github.com/prometheus/procfs"
1414
"github.com/stretchr/testify/assert"
@@ -43,7 +43,7 @@ func sampleCPUInfo() []procfs.CPUInfo {
4343
}
4444
}
4545

46-
func expectedLabels() map[string]string {
46+
func expectedCPUInfoLabels() map[string]string {
4747
return map[string]string{
4848
"processor": "",
4949
"vendor_id": "",
@@ -87,7 +87,7 @@ func TestCPUInfoCollector_Describe(t *testing.T) {
8787
}
8888
collector := newCPUInfoCollectorWithFS(mockFS)
8989

90-
ch := make(chan *prometheus.Desc, 1)
90+
ch := make(chan *prom.Desc, 1)
9191
collector.Describe(ch)
9292
close(ch)
9393

@@ -104,18 +104,18 @@ func TestCPUInfoCollector_Collect_Success(t *testing.T) {
104104
}
105105
collector := newCPUInfoCollectorWithFS(mockFS)
106106

107-
ch := make(chan prometheus.Metric, 10)
107+
ch := make(chan prom.Metric, 10)
108108
collector.Collect(ch)
109109
close(ch)
110110

111-
var metrics []prometheus.Metric
111+
var metrics []prom.Metric
112112
for m := range ch {
113113
metrics = append(metrics, m)
114114
}
115115

116116
assert.Len(t, metrics, 2, "expected two CPU info metrics")
117117

118-
el := expectedLabels()
118+
el := expectedCPUInfoLabels()
119119

120120
for _, m := range metrics {
121121
dtoMetric := &dto.Metric{}
@@ -142,11 +142,11 @@ func TestCPUInfoCollector_Collect_Error(t *testing.T) {
142142
}
143143
collector := newCPUInfoCollectorWithFS(mockFS)
144144

145-
ch := make(chan prometheus.Metric, 10)
145+
ch := make(chan prom.Metric, 10)
146146
collector.Collect(ch)
147147
close(ch)
148148

149-
var metrics []prometheus.Metric
149+
var metrics []prom.Metric
150150
for m := range ch {
151151
metrics = append(metrics, m)
152152
}
@@ -165,7 +165,7 @@ func TestCPUInfoCollector_Collect_Concurrency(t *testing.T) {
165165

166166
const numGoroutines = 10
167167
var wg sync.WaitGroup
168-
ch := make(chan prometheus.Metric, numGoroutines*len(sampleCPUInfo()))
168+
ch := make(chan prom.Metric, numGoroutines*len(sampleCPUInfo()))
169169

170170
for i := 0; i < numGoroutines; i++ {
171171
wg.Add(1)
@@ -178,7 +178,7 @@ func TestCPUInfoCollector_Collect_Concurrency(t *testing.T) {
178178
wg.Wait()
179179
close(ch)
180180

181-
var metrics []prometheus.Metric
181+
var metrics []prom.Metric
182182
for m := range ch {
183183
metrics = append(metrics, m)
184184
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package collector
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
prom "github.com/prometheus/client_golang/prometheus"
8+
)
9+
10+
type energyZone struct {
11+
sync.Mutex
12+
13+
sysfs sysFS
14+
desc *prom.Desc
15+
}
16+
17+
var _ prom.Collector = &energyZone{}
18+
19+
func NewEnergyZoneCollector(sysPath string) (*energyZone, error) {
20+
sysfs, err := newSysFS(sysPath)
21+
if err != nil {
22+
return nil, fmt.Errorf("creating sysfs failed: %w", err)
23+
}
24+
return newEnergyCollectorWithFS(sysfs), nil
25+
}
26+
27+
// newEnergyCollectorWithFS injects a sysFS interface
28+
func newEnergyCollectorWithFS(fs sysFS) *energyZone {
29+
return &energyZone{
30+
sysfs: fs,
31+
desc: prom.NewDesc(
32+
prom.BuildFQName(namespace, "node", "rapl_zone"),
33+
"Rapl Zones from sysfs",
34+
[]string{"name", "index", "path"},
35+
nil,
36+
),
37+
}
38+
}
39+
40+
func (e *energyZone) Describe(ch chan<- *prom.Desc) {
41+
ch <- e.desc
42+
}
43+
44+
func (e *energyZone) Collect(ch chan<- prom.Metric) {
45+
e.Lock()
46+
defer e.Unlock()
47+
48+
zones, err := e.sysfs.Zones()
49+
if err != nil {
50+
return
51+
}
52+
for _, z := range zones {
53+
ch <- prom.MustNewConstMetric(
54+
e.desc,
55+
prom.GaugeValue,
56+
1,
57+
z.Name,
58+
fmt.Sprintf("%d", z.Index),
59+
z.Path,
60+
)
61+
}
62+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package collector
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
"testing"
7+
8+
prom "github.com/prometheus/client_golang/prometheus"
9+
dto "github.com/prometheus/client_model/go"
10+
"github.com/prometheus/procfs/sysfs"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
// mockSysFS is a mock implementation of the sysFS interface for testing.
15+
type mockSysFS struct {
16+
zonesFunc func() ([]sysfs.RaplZone, error)
17+
}
18+
19+
func (m *mockSysFS) Zones() ([]sysfs.RaplZone, error) {
20+
return m.zonesFunc()
21+
}
22+
23+
// sampleEnergyZoneInfo returns a sample RaplZone slice for testing.
24+
func sampleEnergyZoneInfo() []sysfs.RaplZone {
25+
return []sysfs.RaplZone{{
26+
Name: "package-0",
27+
Index: 0,
28+
Path: "/sys/class/powercap/intel-rapl:0",
29+
MaxMicrojoules: 262143328850.0,
30+
}, {
31+
Name: "dram",
32+
Index: 0,
33+
Path: "/sys/class/powercap/intel-rapl:0:0",
34+
MaxMicrojoules: 262143328850.0,
35+
}}
36+
}
37+
38+
func expectedEnergyZoneLabels() map[string]string {
39+
return map[string]string{
40+
"name": "",
41+
"index": "",
42+
"path": "",
43+
}
44+
}
45+
46+
// TestNewEnergyZoneCollector tests creation of EnergyZone Collector
47+
func TestNewEnergyZoneCollector(t *testing.T) {
48+
collector, err := NewEnergyZoneCollector("/sys")
49+
assert.NoError(t, err)
50+
assert.NotNil(t, collector)
51+
assert.NotNil(t, collector.sysfs)
52+
assert.NotNil(t, collector.desc)
53+
}
54+
55+
// TestNewEnergyZoneCollectorWithFS tests creator creation with injected sysfs
56+
func TestNewEnergyZoneCollectorWithFS(t *testing.T) {
57+
mockFS := &mockSysFS{
58+
zonesFunc: func() ([]sysfs.RaplZone, error) {
59+
return sampleEnergyZoneInfo(), nil
60+
},
61+
}
62+
collector := newEnergyCollectorWithFS(mockFS)
63+
assert.NotNil(t, collector)
64+
assert.Equal(t, mockFS, collector.sysfs)
65+
assert.NotNil(t, collector.desc)
66+
assert.Contains(t, collector.desc.String(), "kepler_node_rapl_zone")
67+
assert.Contains(t, collector.desc.String(), "variableLabels: {name,index,path}")
68+
}
69+
70+
// TestEnergyZoneCollector_Describe tests the Describe method.
71+
func TestEnergyZoneCollector_Describe(t *testing.T) {
72+
mockFS := &mockSysFS{
73+
zonesFunc: func() ([]sysfs.RaplZone, error) {
74+
return sampleEnergyZoneInfo(), nil
75+
},
76+
}
77+
collector := newEnergyCollectorWithFS(mockFS)
78+
79+
ch := make(chan *prom.Desc, 1)
80+
collector.Describe(ch)
81+
close(ch)
82+
83+
desc := <-ch
84+
assert.Equal(t, collector.desc, desc)
85+
}
86+
87+
func TestEnergyZoneCollector_Collect_Success(t *testing.T) {
88+
mockFS := &mockSysFS{
89+
zonesFunc: func() ([]sysfs.RaplZone, error) {
90+
return sampleEnergyZoneInfo(), nil
91+
},
92+
}
93+
collector := newEnergyCollectorWithFS(mockFS)
94+
95+
ch := make(chan prom.Metric, 10)
96+
collector.Collect(ch)
97+
close(ch)
98+
99+
var metrics []prom.Metric
100+
for m := range ch {
101+
metrics = append(metrics, m)
102+
}
103+
104+
assert.Len(t, metrics, 2, "expected two zone info")
105+
106+
el := expectedEnergyZoneLabels()
107+
108+
for _, m := range metrics {
109+
dtoMetric := &dto.Metric{}
110+
err := m.Write(dtoMetric)
111+
assert.NoError(t, err)
112+
assert.NotNil(t, dtoMetric.Gauge)
113+
assert.NotNil(t, dtoMetric.Gauge.Value)
114+
assert.Equal(t, 1.0, *dtoMetric.Gauge.Value)
115+
assert.NotNil(t, dtoMetric.Label)
116+
for _, l := range dtoMetric.Label {
117+
assert.NotNil(t, l.Name)
118+
delete(el, *l.Name)
119+
}
120+
}
121+
assert.Empty(t, el, "all expected labels not received")
122+
}
123+
124+
func TestEnergyZoneCollector_Collect_Error(t *testing.T) {
125+
mockFS := &mockSysFS{
126+
zonesFunc: func() ([]sysfs.RaplZone, error) {
127+
return nil, fmt.Errorf("cannot read zones")
128+
},
129+
}
130+
collector := newEnergyCollectorWithFS(mockFS)
131+
132+
ch := make(chan prom.Metric, 10)
133+
collector.Collect(ch)
134+
close(ch)
135+
136+
var metrics []prom.Metric
137+
for m := range ch {
138+
metrics = append(metrics, m)
139+
}
140+
141+
assert.Len(t, metrics, 0, "expected no metrics on error")
142+
}
143+
144+
func TestEnergyZoneCollector_Collect_Concurrency(t *testing.T) {
145+
mockFS := &mockSysFS{
146+
zonesFunc: func() ([]sysfs.RaplZone, error) {
147+
return sampleEnergyZoneInfo(), nil
148+
},
149+
}
150+
collector := newEnergyCollectorWithFS(mockFS)
151+
152+
const numGoroutines = 10
153+
var wg sync.WaitGroup
154+
ch := make(chan prom.Metric, numGoroutines*len(sampleCPUInfo()))
155+
156+
for i := 0; i < numGoroutines; i++ {
157+
wg.Add(1)
158+
go func() {
159+
defer wg.Done()
160+
collector.Collect(ch)
161+
}()
162+
}
163+
164+
wg.Wait()
165+
close(ch)
166+
167+
var metrics []prom.Metric
168+
for m := range ch {
169+
metrics = append(metrics, m)
170+
}
171+
172+
// Expect numGoroutines * number of CPUs metrics
173+
expectedMetrics := numGoroutines * len(sampleCPUInfo())
174+
assert.Equal(t, expectedMetrics, len(metrics), "expected metrics from all goroutines")
175+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package collector
2+
3+
import (
4+
"github.com/prometheus/procfs"
5+
)
6+
7+
// procFS is an interface to prometheus/procfs
8+
type procFS interface {
9+
CPUInfo() ([]procfs.CPUInfo, error)
10+
}
11+
12+
type realProcFS struct {
13+
fs procfs.FS
14+
}
15+
16+
func (r *realProcFS) CPUInfo() ([]procfs.CPUInfo, error) {
17+
return r.fs.CPUInfo()
18+
}
19+
20+
func newProcFS(mountPoint string) (procFS, error) {
21+
fs, err := procfs.NewFS(mountPoint)
22+
if err != nil {
23+
return nil, err
24+
}
25+
return &realProcFS{fs: fs}, nil
26+
}

0 commit comments

Comments
 (0)