Skip to content

Commit 56645c2

Browse files
committed
feat(cpuinfo): Added cpu info collector
Added a collector for cpu info built in prometheus procfs Signed-off-by: Vimal Kumar <[email protected]>
1 parent 2bde02f commit 56645c2

File tree

6 files changed

+361
-18
lines changed

6 files changed

+361
-18
lines changed

cmd/kepler/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,19 @@ func createServices(logger *slog.Logger, cfg *config.Config) ([]service.Service,
155155
apiServer := server.NewAPIServer(
156156
server.WithLogger(logger),
157157
)
158+
159+
collectors, err := prometheus.CreateCollectors(
160+
pm,
161+
prometheus.WithLogger(logger),
162+
prometheus.WithProcFSPath(cfg.Host.ProcFS),
163+
)
158164
// TODO: enable exporters based on config / flags
159-
promExporter := prometheus.NewExporter(pm, apiServer, prometheus.WithLogger(logger))
165+
promExporter := prometheus.NewExporter(
166+
pm,
167+
apiServer,
168+
prometheus.WithLogger(logger),
169+
prometheus.WithCollectors(collectors),
170+
)
160171

161172
return []service.Service{
162173
promExporter,

internal/exporter/prometheus/collectors/build_info.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package collectors
55

66
import (
7-
"github.com/prometheus/client_golang/prometheus"
7+
prom "github.com/prometheus/client_golang/prometheus"
88
"github.com/sustainable-computing-io/kepler/internal/version"
99
)
1010

@@ -14,13 +14,13 @@ const (
1414
)
1515

1616
type BuildInfoCollector struct {
17-
buildInfo *prometheus.GaugeVec
17+
buildInfo *prom.GaugeVec
1818
}
1919

2020
// NewBuildInfoCollector creates a new collector for build information
2121
func NewBuildInfoCollector() *BuildInfoCollector {
22-
buildInfo := prometheus.NewGaugeVec(
23-
prometheus.GaugeOpts{
22+
buildInfo := prom.NewGaugeVec(
23+
prom.GaugeOpts{
2424
Namespace: namespace,
2525
Subsystem: buildSubsystem,
2626
Name: "info",
@@ -34,11 +34,11 @@ func NewBuildInfoCollector() *BuildInfoCollector {
3434
}
3535
}
3636

37-
func (c *BuildInfoCollector) Describe(ch chan<- *prometheus.Desc) {
37+
func (c *BuildInfoCollector) Describe(ch chan<- *prom.Desc) {
3838
c.buildInfo.Describe(ch)
3939
}
4040

41-
func (c *BuildInfoCollector) Collect(ch chan<- prometheus.Metric) {
41+
func (c *BuildInfoCollector) Collect(ch chan<- prom.Metric) {
4242
info := version.Info()
4343

4444
c.buildInfo.WithLabelValues(
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-FileCopyrightText: 2025 The Kepler Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package collectors
5+
6+
import (
7+
"fmt"
8+
"sync"
9+
10+
prom "github.com/prometheus/client_golang/prometheus"
11+
"github.com/prometheus/procfs"
12+
)
13+
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+
35+
// cpuInfoCollector collects CPU info metrics from procfs.
36+
type cpuInfoCollector struct {
37+
sync.Mutex
38+
39+
fs procFS
40+
desc *prom.Desc
41+
}
42+
43+
// NewCPUInfoCollector creates a CPUInfoCollector using a procfs mount path.
44+
func NewCPUInfoCollector(procPath string) (*cpuInfoCollector, error) {
45+
fs, err := newProcFS(procPath)
46+
if err != nil {
47+
return nil, fmt.Errorf("creating procfs failed: %w", err)
48+
}
49+
return newCPUInfoCollectorWithFS(fs), nil
50+
}
51+
52+
// newCPUInfoCollectorWithFS injects a procFS interface
53+
func newCPUInfoCollectorWithFS(fs procFS) *cpuInfoCollector {
54+
return &cpuInfoCollector{
55+
fs: fs,
56+
desc: prom.NewDesc(
57+
prom.BuildFQName(namespace, "", "cpu_info"),
58+
"CPU information from procfs",
59+
[]string{"processor", "vendor_id", "model_name", "physical_id", "core_id"},
60+
nil,
61+
),
62+
}
63+
}
64+
65+
func (c *cpuInfoCollector) Describe(ch chan<- *prom.Desc) {
66+
ch <- c.desc
67+
}
68+
69+
func (c *cpuInfoCollector) Collect(ch chan<- prom.Metric) {
70+
c.Lock()
71+
defer c.Unlock()
72+
73+
cpuInfos, err := c.fs.CPUInfo()
74+
if err != nil {
75+
return
76+
}
77+
for _, ci := range cpuInfos {
78+
ch <- prom.MustNewConstMetric(
79+
c.desc,
80+
prom.GaugeValue,
81+
1,
82+
fmt.Sprintf("%d", ci.Processor),
83+
ci.VendorID,
84+
ci.ModelName,
85+
ci.PhysicalID,
86+
ci.CoreID,
87+
)
88+
}
89+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// SPDX-FileCopyrightText: 2025 The Kepler Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package collectors
5+
6+
import (
7+
"errors"
8+
"sync"
9+
"testing"
10+
11+
"github.com/prometheus/client_golang/prometheus"
12+
dto "github.com/prometheus/client_model/go"
13+
"github.com/prometheus/procfs"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
// mockProcFS is a mock implementation of the procFS interface for testing.
18+
type mockProcFS struct {
19+
cpuInfoFunc func() ([]procfs.CPUInfo, error)
20+
}
21+
22+
func (m *mockProcFS) CPUInfo() ([]procfs.CPUInfo, error) {
23+
return m.cpuInfoFunc()
24+
}
25+
26+
// sampleCPUInfo returns a sample CPUInfo slice for testing.
27+
func sampleCPUInfo() []procfs.CPUInfo {
28+
return []procfs.CPUInfo{
29+
{
30+
Processor: 0,
31+
VendorID: "GenuineIntel",
32+
ModelName: "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz",
33+
PhysicalID: "0",
34+
CoreID: "0",
35+
},
36+
{
37+
Processor: 1,
38+
VendorID: "GenuineIntel",
39+
ModelName: "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz",
40+
PhysicalID: "0",
41+
CoreID: "1",
42+
},
43+
}
44+
}
45+
46+
func expectedLabels() map[string]string {
47+
return map[string]string{
48+
"processor": "",
49+
"vendor_id": "",
50+
"model_name": "",
51+
"physical_id": "",
52+
"core_id": "",
53+
}
54+
}
55+
56+
// TestNewCPUInfoCollector tests the creation of a new CPUInfoCollector.
57+
func TestNewCPUInfoCollector(t *testing.T) {
58+
// Test successful creation with a mock procfs
59+
collector, err := NewCPUInfoCollector("/proc")
60+
assert.NoError(t, err)
61+
assert.NotNil(t, collector)
62+
assert.NotNil(t, collector.fs)
63+
assert.NotNil(t, collector.desc)
64+
}
65+
66+
// TestNewCPUInfoCollectorWithFS tests the creation with an injected procFS.
67+
func TestNewCPUInfoCollectorWithFS(t *testing.T) {
68+
mockFS := &mockProcFS{
69+
cpuInfoFunc: func() ([]procfs.CPUInfo, error) {
70+
return sampleCPUInfo(), nil
71+
},
72+
}
73+
collector := newCPUInfoCollectorWithFS(mockFS)
74+
assert.NotNil(t, collector)
75+
assert.Equal(t, mockFS, collector.fs)
76+
assert.NotNil(t, collector.desc)
77+
assert.Contains(t, collector.desc.String(), "kepler_cpu_info")
78+
assert.Contains(t, collector.desc.String(), "variableLabels: {processor,vendor_id,model_name,physical_id,core_id}")
79+
}
80+
81+
// TestCPUInfoCollector_Describe tests the Describe method.
82+
func TestCPUInfoCollector_Describe(t *testing.T) {
83+
mockFS := &mockProcFS{
84+
cpuInfoFunc: func() ([]procfs.CPUInfo, error) {
85+
return sampleCPUInfo(), nil
86+
},
87+
}
88+
collector := newCPUInfoCollectorWithFS(mockFS)
89+
90+
ch := make(chan *prometheus.Desc, 1)
91+
collector.Describe(ch)
92+
close(ch)
93+
94+
desc := <-ch
95+
assert.Equal(t, collector.desc, desc)
96+
}
97+
98+
// TestCPUInfoCollector_Collect_Success tests the Collect method with valid CPU info.
99+
func TestCPUInfoCollector_Collect_Success(t *testing.T) {
100+
mockFS := &mockProcFS{
101+
cpuInfoFunc: func() ([]procfs.CPUInfo, error) {
102+
return sampleCPUInfo(), nil
103+
},
104+
}
105+
collector := newCPUInfoCollectorWithFS(mockFS)
106+
107+
ch := make(chan prometheus.Metric, 10)
108+
collector.Collect(ch)
109+
close(ch)
110+
111+
var metrics []prometheus.Metric
112+
for m := range ch {
113+
metrics = append(metrics, m)
114+
}
115+
116+
assert.Len(t, metrics, 2, "expected two CPU info metrics")
117+
118+
el := expectedLabels()
119+
120+
for _, m := range metrics {
121+
dtoMetric := &dto.Metric{}
122+
err := m.Write(dtoMetric)
123+
assert.NoError(t, err)
124+
assert.NotNil(t, dtoMetric.Gauge)
125+
assert.NotNil(t, dtoMetric.Gauge.Value)
126+
assert.Equal(t, 1.0, *dtoMetric.Gauge.Value)
127+
assert.NotNil(t, dtoMetric.Label)
128+
for _, l := range dtoMetric.Label {
129+
assert.NotNil(t, l.Name)
130+
delete(el, *l.Name)
131+
}
132+
}
133+
assert.Empty(t, el, "all expected labels not received")
134+
}
135+
136+
// TestCPUInfoCollector_Collect_Error tests the Collect method when CPUInfo fails.
137+
func TestCPUInfoCollector_Collect_Error(t *testing.T) {
138+
mockFS := &mockProcFS{
139+
cpuInfoFunc: func() ([]procfs.CPUInfo, error) {
140+
return nil, errors.New("failed to read CPU info")
141+
},
142+
}
143+
collector := newCPUInfoCollectorWithFS(mockFS)
144+
145+
ch := make(chan prometheus.Metric, 10)
146+
collector.Collect(ch)
147+
close(ch)
148+
149+
var metrics []prometheus.Metric
150+
for m := range ch {
151+
metrics = append(metrics, m)
152+
}
153+
154+
assert.Len(t, metrics, 0, "expected no metrics on error")
155+
}
156+
157+
// TestCPUInfoCollector_Collect_Concurrency tests concurrent calls to Collect.
158+
func TestCPUInfoCollector_Collect_Concurrency(t *testing.T) {
159+
mockFS := &mockProcFS{
160+
cpuInfoFunc: func() ([]procfs.CPUInfo, error) {
161+
return sampleCPUInfo(), nil
162+
},
163+
}
164+
collector := newCPUInfoCollectorWithFS(mockFS)
165+
166+
const numGoroutines = 10
167+
var wg sync.WaitGroup
168+
ch := make(chan prometheus.Metric, numGoroutines*len(sampleCPUInfo()))
169+
170+
for i := 0; i < numGoroutines; i++ {
171+
wg.Add(1)
172+
go func() {
173+
defer wg.Done()
174+
collector.Collect(ch)
175+
}()
176+
}
177+
178+
wg.Wait()
179+
close(ch)
180+
181+
var metrics []prometheus.Metric
182+
for m := range ch {
183+
metrics = append(metrics, m)
184+
}
185+
186+
// Expect numGoroutines * number of CPUs metrics
187+
expectedMetrics := numGoroutines * len(sampleCPUInfo())
188+
assert.Equal(t, expectedMetrics, len(metrics), "expected metrics from all goroutines")
189+
}

0 commit comments

Comments
 (0)