Skip to content

Commit a1cf946

Browse files
committed
feat: add support for CPU temperature metrics on Apple Silicon
1 parent 6ba05bf commit a1cf946

File tree

3 files changed

+200
-9
lines changed

3 files changed

+200
-9
lines changed

collector/thermal_darwin.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type thermCollector struct {
5858
cpuSchedulerLimit typedDesc
5959
cpuAvailableCPU typedDesc
6060
cpuSpeedLimit typedDesc
61+
temperature typedDesc
6162
logger *slog.Logger
6263
}
6364

@@ -96,23 +97,37 @@ func NewThermCollector(logger *slog.Logger) (Collector, error) {
9697
),
9798
valueType: prometheus.GaugeValue,
9899
},
100+
temperature: typedDesc{
101+
desc: prometheus.NewDesc(
102+
prometheus.BuildFQName(namespace, thermal, "temperature_celsius"),
103+
"Temperature of the thermal sensor in Celsius.",
104+
[]string{"sensor"},
105+
nil,
106+
),
107+
valueType: prometheus.GaugeValue,
108+
},
99109
logger: logger,
100110
}, nil
101111
}
102112

103113
func (c *thermCollector) Update(ch chan<- prometheus.Metric) error {
104114
cpuPowerStatus, err := fetchCPUPowerStatus()
105115
if err != nil {
106-
return err
107-
}
108-
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitSchedulerTimeKey))]; ok {
109-
ch <- c.cpuSchedulerLimit.mustNewConstMetric(float64(value) / 100.0)
110-
}
111-
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitProcessorCountKey))]; ok {
112-
ch <- c.cpuAvailableCPU.mustNewConstMetric(float64(value))
116+
c.logger.Debug("failed to fetch CPU power status", "err", err)
117+
} else {
118+
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitSchedulerTimeKey))]; ok {
119+
ch <- c.cpuSchedulerLimit.mustNewConstMetric(float64(value) / 100.0)
120+
}
121+
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitProcessorCountKey))]; ok {
122+
ch <- c.cpuAvailableCPU.mustNewConstMetric(float64(value))
123+
}
124+
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitProcessorSpeedKey))]; ok {
125+
ch <- c.cpuSpeedLimit.mustNewConstMetric(float64(value) / 100.0)
126+
}
113127
}
114-
if value, ok := cpuPowerStatus[(string(C.kIOPMCPUPowerLimitProcessorSpeedKey))]; ok {
115-
ch <- c.cpuSpeedLimit.mustNewConstMetric(float64(value) / 100.0)
128+
129+
if err := c.updateTemperatures(ch); err != nil {
130+
c.logger.Debug("failed to update temperatures", "err", err)
116131
}
117132
return nil
118133
}

collector/thermal_darwin_amd64.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2024 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build !notherm && darwin && amd64 && cgo
15+
16+
package collector
17+
18+
import "github.com/prometheus/client_golang/prometheus"
19+
20+
func (c *thermCollector) updateTemperatures(ch chan<- prometheus.Metric) error {
21+
return nil
22+
}

collector/thermal_darwin_arm64.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright 2024 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build !notherm && darwin && arm64
15+
16+
package collector
17+
18+
/*
19+
#cgo LDFLAGS: -framework IOKit -framework CoreFoundation
20+
#include <CoreFoundation/CoreFoundation.h>
21+
#include <IOKit/IOKitLib.h>
22+
#include <stdlib.h>
23+
24+
typedef struct __IOHIDEventSystemClient * IOHIDEventSystemClientRef;
25+
typedef struct __IOHIDServiceClient * IOHIDServiceClientRef;
26+
typedef struct __IOHIDEvent * IOHIDEventRef;
27+
28+
#define kIOHIDEventTypeTemperature 15
29+
#define IOHIDEventFieldBase(type) (type << 16)
30+
31+
int32_t GetIOHIDEventFieldBase(int32_t type) {
32+
return IOHIDEventFieldBase(type);
33+
}
34+
35+
// External functions
36+
IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator);
37+
void IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match);
38+
CFArrayRef IOHIDEventSystemClientCopyServices(IOHIDEventSystemClientRef client);
39+
IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef service, int64_t type, int32_t options, int64_t timestamp);
40+
double IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);
41+
CFTypeRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef key);
42+
*/
43+
import "C"
44+
45+
import (
46+
"unsafe"
47+
48+
"github.com/prometheus/client_golang/prometheus"
49+
"github.com/prometheus/node_exporter/collector/utils"
50+
)
51+
52+
func (c *thermCollector) updateTemperatures(ch chan<- prometheus.Metric) error {
53+
client := C.IOHIDEventSystemClientCreate(C.kCFAllocatorDefault)
54+
if client == nil {
55+
return nil
56+
}
57+
defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(client)))
58+
59+
page := 0xff00
60+
usage := 5
61+
62+
pageNum := C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberIntType, unsafe.Pointer(&page))
63+
defer C.CFRelease(C.CFTypeRef(pageNum))
64+
usageNum := C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberIntType, unsafe.Pointer(&usage))
65+
defer C.CFRelease(C.CFTypeRef(usageNum))
66+
67+
keyPage := C.CString("PrimaryUsagePage")
68+
defer C.free(unsafe.Pointer(keyPage))
69+
keyUsage := C.CString("PrimaryUsage")
70+
defer C.free(unsafe.Pointer(keyUsage))
71+
72+
cfKeyPage := C.CFStringCreateWithCString(C.kCFAllocatorDefault, keyPage, C.kCFStringEncodingUTF8)
73+
defer C.CFRelease(C.CFTypeRef(cfKeyPage))
74+
cfKeyUsage := C.CFStringCreateWithCString(C.kCFAllocatorDefault, keyUsage, C.kCFStringEncodingUTF8)
75+
defer C.CFRelease(C.CFTypeRef(cfKeyUsage))
76+
77+
keys := []C.CFTypeRef{C.CFTypeRef(cfKeyPage), C.CFTypeRef(cfKeyUsage)}
78+
values := []C.CFTypeRef{C.CFTypeRef(pageNum), C.CFTypeRef(usageNum)}
79+
80+
matching := C.CFDictionaryCreate(C.kCFAllocatorDefault,
81+
(*unsafe.Pointer)(unsafe.Pointer(&keys[0])),
82+
(*unsafe.Pointer)(unsafe.Pointer(&values[0])),
83+
2,
84+
&C.kCFTypeDictionaryKeyCallBacks,
85+
&C.kCFTypeDictionaryValueCallBacks)
86+
defer C.CFRelease(C.CFTypeRef(matching))
87+
88+
C.IOHIDEventSystemClientSetMatching(client, matching)
89+
90+
services := C.IOHIDEventSystemClientCopyServices(client)
91+
if services == 0 {
92+
return nil
93+
}
94+
defer C.CFRelease(C.CFTypeRef(services))
95+
96+
count := C.CFArrayGetCount(services)
97+
98+
prodKey := C.CString("Product")
99+
defer C.free(unsafe.Pointer(prodKey))
100+
cfProdKey := C.CFStringCreateWithCString(C.kCFAllocatorDefault, prodKey, C.kCFStringEncodingUTF8)
101+
defer C.CFRelease(C.CFTypeRef(cfProdKey))
102+
103+
const absoluteZeroCelsius = -273.15
104+
105+
for i := 0; i < int(count); i++ {
106+
service := C.CFArrayGetValueAtIndex(services, C.CFIndex(i))
107+
108+
event := C.IOHIDServiceClientCopyEvent((C.IOHIDServiceClientRef)(service), C.kIOHIDEventTypeTemperature, 0, 0)
109+
if event == nil {
110+
continue
111+
}
112+
113+
temp := C.IOHIDEventGetFloatValue(event, C.GetIOHIDEventFieldBase(C.kIOHIDEventTypeTemperature))
114+
C.CFRelease(C.CFTypeRef(unsafe.Pointer(event)))
115+
116+
// Observed invalid values on some Apple Silicon devices are around -9200.
117+
// Filter out physically impossible temperatures.
118+
if temp < absoluteZeroCelsius {
119+
continue
120+
}
121+
122+
nameRef := C.IOHIDServiceClientCopyProperty((C.IOHIDServiceClientRef)(service), cfProdKey)
123+
name := "Unknown"
124+
if nameRef != 0 {
125+
name = cfStringToString((C.CFStringRef)(nameRef))
126+
C.CFRelease(C.CFTypeRef(nameRef))
127+
}
128+
129+
ch <- c.temperature.mustNewConstMetric(float64(temp), name)
130+
}
131+
return nil
132+
}
133+
134+
func cfStringToString(s C.CFStringRef) string {
135+
p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8)
136+
if p != nil {
137+
return C.GoString(p)
138+
}
139+
length := C.CFStringGetLength(s)
140+
if length <= 0 {
141+
return ""
142+
}
143+
maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8)
144+
if maxBufLen <= 0 {
145+
return ""
146+
}
147+
if maxBufLen > 4096 {
148+
maxBufLen = 4096
149+
}
150+
buf := make([]byte, maxBufLen)
151+
var usedBufLen C.CFIndex
152+
_ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen)
153+
return utils.SafeBytesToString(buf[:usedBufLen])
154+
}

0 commit comments

Comments
 (0)