Skip to content

Commit 5043098

Browse files
committed
feat(cpuinfo): Added cpu info collector
Added a collector for cpu info built in prometheus procfs Signed-off-by: Vimal Kumar <vimal78@gmail.com>
1 parent 75a40a3 commit 5043098

File tree

4 files changed

+300
-5
lines changed

4 files changed

+300
-5
lines changed

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ require (
66
github.com/alecthomas/kingpin/v2 v2.4.0
77
github.com/oklog/run v1.1.0
88
github.com/prometheus/client_golang v1.22.0
9+
github.com/prometheus/client_model v0.6.1
910
github.com/prometheus/exporter-toolkit v0.14.0
11+
github.com/prometheus/procfs v0.15.1
1012
github.com/stretchr/testify v1.10.0
1113
gopkg.in/yaml.v3 v3.0.1
1214
)
@@ -23,9 +25,7 @@ require (
2325
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2426
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
2527
github.com/pmezard/go-difflib v1.0.0 // indirect
26-
github.com/prometheus/client_model v0.6.1 // indirect
2728
github.com/prometheus/common v0.62.0 // indirect
28-
github.com/prometheus/procfs v0.15.1 // indirect
2929
github.com/stretchr/objx v0.5.2 // indirect
3030
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
3131
golang.org/x/crypto v0.32.0 // indirect
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+
}
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+
}

internal/exporter/prometheus/prometheus.go

+20-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
prom "github.com/prometheus/client_golang/prometheus"
1313
"github.com/prometheus/client_golang/prometheus/collectors"
1414
"github.com/prometheus/client_golang/prometheus/promhttp"
15+
"github.com/sustainable-computing-io/kepler/config"
1516
collector "github.com/sustainable-computing-io/kepler/internal/exporter/prometheus/collectors"
1617
"github.com/sustainable-computing-io/kepler/internal/monitor"
1718
"github.com/sustainable-computing-io/kepler/internal/service"
@@ -29,6 +30,7 @@ type APIRegistry interface {
2930
type Opts struct {
3031
logger *slog.Logger
3132
debugCollectors map[string]bool
33+
config *config.Config
3234
}
3335

3436
// DefaultOpts() returns a new Opts with defaults set
@@ -38,6 +40,7 @@ func DefaultOpts() Opts {
3840
debugCollectors: map[string]bool{
3941
"go": true,
4042
},
43+
config: config.DefaultConfig(),
4144
}
4245
}
4346

@@ -60,13 +63,20 @@ func WithDebugCollectors(c *[]string) OptionFn {
6063
}
6164
}
6265

66+
func WithConfig(config *config.Config) OptionFn {
67+
return func(o *Opts) {
68+
o.config = config
69+
}
70+
}
71+
6372
// Exporter exports power data to Prometheus
6473
type Exporter struct {
6574
logger *slog.Logger
6675
monitor Monitor
6776
registry *prom.Registry
6877
server APIRegistry
6978
debugCollectors map[string]bool
79+
config *config.Config
7080
}
7181

7282
var _ Service = (*Exporter)(nil)
@@ -78,15 +88,16 @@ func NewExporter(pm Monitor, s APIRegistry, applyOpts ...OptionFn) *Exporter {
7888
apply(&opts)
7989
}
8090

81-
monitor := &Exporter{
91+
exporter := &Exporter{
8292
monitor: pm,
8393
server: s,
8494
logger: opts.logger.With("service", "prometheus"),
8595
debugCollectors: opts.debugCollectors,
8696
registry: prom.NewRegistry(),
97+
config: opts.config,
8798
}
8899

89-
return monitor
100+
return exporter
90101
}
91102

92103
func collectorForName(name string) (prom.Collector, error) {
@@ -118,7 +129,13 @@ func (e *Exporter) Start(ctx context.Context) error {
118129
buildInfoCollector := collector.NewBuildInfoCollector()
119130
e.registry.MustRegister(buildInfoCollector)
120131

121-
err := e.server.Register("/metrics", "Metrics", "Prometheus metrics",
132+
cpuInfoCollector, err := collector.NewCPUInfoCollector(e.config.Host.ProcFS)
133+
if err != nil {
134+
return err
135+
}
136+
e.registry.MustRegister(cpuInfoCollector)
137+
138+
err = e.server.Register("/metrics", "Metrics", "Prometheus metrics",
122139
promhttp.HandlerFor(
123140
e.registry,
124141
promhttp.HandlerOpts{

0 commit comments

Comments
 (0)