250 lines
6.5 KiB
Go
250 lines
6.5 KiB
Go
|
//go:build linux
|
||
|
// +build linux
|
||
|
|
||
|
package temp
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/influxdata/telegraf"
|
||
|
"github.com/influxdata/telegraf/internal"
|
||
|
)
|
||
|
|
||
|
const scalingFactor = float64(1000.0)
|
||
|
|
||
|
type temperatureStat struct {
|
||
|
name string
|
||
|
label string
|
||
|
device string
|
||
|
temperature float64
|
||
|
additional map[string]interface{}
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) Init() error {
|
||
|
switch t.MetricFormat {
|
||
|
case "":
|
||
|
t.MetricFormat = "v2"
|
||
|
case "v1", "v2":
|
||
|
// Do nothing as those are valid
|
||
|
default:
|
||
|
return fmt.Errorf("invalid 'metric_format' %q", t.MetricFormat)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) Gather(acc telegraf.Accumulator) error {
|
||
|
// Get all sensors and honor the HOST_SYS environment variable
|
||
|
path := internal.GetSysPath()
|
||
|
|
||
|
// Try to use the hwmon interface
|
||
|
temperatures, err := t.gatherHwmon(path)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("getting temperatures failed: %w", err)
|
||
|
}
|
||
|
|
||
|
if len(temperatures) == 0 {
|
||
|
// There is no hwmon interface, fallback to thermal-zone parsing
|
||
|
temperatures, err = t.gatherThermalZone(path)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("getting temperatures (via fallback) failed: %w", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch t.MetricFormat {
|
||
|
case "v1":
|
||
|
t.createMetricsV1(acc, temperatures)
|
||
|
case "v2":
|
||
|
t.createMetricsV2(acc, temperatures)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) createMetricsV1(acc telegraf.Accumulator, temperatures []temperatureStat) {
|
||
|
for _, temp := range temperatures {
|
||
|
sensor := temp.name
|
||
|
if temp.label != "" {
|
||
|
sensor += "_" + strings.ReplaceAll(temp.label, " ", "")
|
||
|
}
|
||
|
|
||
|
// Mandatory measurement value
|
||
|
tags := map[string]string{"sensor": sensor + "_input"}
|
||
|
if t.DeviceTag {
|
||
|
tags["device"] = temp.device
|
||
|
}
|
||
|
acc.AddFields("temp", map[string]interface{}{"temp": temp.temperature}, tags)
|
||
|
|
||
|
// Optional values values
|
||
|
for measurement, value := range temp.additional {
|
||
|
tags := map[string]string{"sensor": sensor + "_" + measurement}
|
||
|
if t.DeviceTag {
|
||
|
tags["device"] = temp.device
|
||
|
}
|
||
|
acc.AddFields("temp", map[string]interface{}{"temp": value}, tags)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) createMetricsV2(acc telegraf.Accumulator, temperatures []temperatureStat) {
|
||
|
for _, temp := range temperatures {
|
||
|
sensor := temp.name
|
||
|
if temp.label != "" {
|
||
|
sensor += "_" + strings.ReplaceAll(temp.label, " ", "_")
|
||
|
}
|
||
|
|
||
|
// Mandatory measurement value
|
||
|
tags := map[string]string{"sensor": sensor}
|
||
|
if t.DeviceTag {
|
||
|
tags["device"] = temp.device
|
||
|
}
|
||
|
acc.AddFields("temp", map[string]interface{}{"temp": temp.temperature}, tags)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) gatherHwmon(syspath string) ([]temperatureStat, error) {
|
||
|
// Get all hwmon devices
|
||
|
sensors, err := filepath.Glob(filepath.Join(syspath, "class", "hwmon", "hwmon*", "temp*_input"))
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("getting sensors failed: %w", err)
|
||
|
}
|
||
|
|
||
|
// Handle CentOS special path containing an additional "device" directory
|
||
|
// see https://github.com/shirou/gopsutil/blob/master/host/host_linux.go
|
||
|
if len(sensors) == 0 {
|
||
|
sensors, err = filepath.Glob(filepath.Join(syspath, "class", "hwmon", "hwmon*", "device", "temp*_input"))
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("getting sensors on CentOS failed: %w", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Exit early if we cannot find any device
|
||
|
if len(sensors) == 0 {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// Collect the sensor information
|
||
|
stats := make([]temperatureStat, 0, len(sensors))
|
||
|
for _, s := range sensors {
|
||
|
// Get the sensor directory and the temperature prefix from the path
|
||
|
path := filepath.Dir(s)
|
||
|
prefix := strings.SplitN(filepath.Base(s), "_", 2)[0]
|
||
|
|
||
|
// Read the sensor and device name
|
||
|
deviceName, err := os.Readlink(filepath.Join(path, "device"))
|
||
|
if err == nil {
|
||
|
deviceName = filepath.Base(deviceName)
|
||
|
}
|
||
|
|
||
|
// Read the sensor name and use the device name as fallback
|
||
|
name := deviceName
|
||
|
n, err := os.ReadFile(filepath.Join(path, "name"))
|
||
|
if err == nil {
|
||
|
name = strings.TrimSpace(string(n))
|
||
|
}
|
||
|
|
||
|
// Get the sensor label
|
||
|
var label string
|
||
|
if buf, err := os.ReadFile(filepath.Join(path, prefix+"_label")); err == nil {
|
||
|
label = strings.TrimSpace(string(buf))
|
||
|
}
|
||
|
|
||
|
// Do the actual sensor readings
|
||
|
temp := temperatureStat{
|
||
|
name: name,
|
||
|
label: strings.ToLower(label),
|
||
|
device: deviceName,
|
||
|
additional: make(map[string]interface{}),
|
||
|
}
|
||
|
|
||
|
// Temperature (mandatory)
|
||
|
fn := filepath.Join(path, prefix+"_input")
|
||
|
buf, err := os.ReadFile(fn)
|
||
|
if err != nil {
|
||
|
t.Log.Debugf("Couldn't read temperature from %q: %v", fn, err)
|
||
|
continue
|
||
|
}
|
||
|
if v, err := strconv.ParseFloat(strings.TrimSpace(string(buf)), 64); err == nil {
|
||
|
temp.temperature = v / scalingFactor
|
||
|
}
|
||
|
|
||
|
// Read all possible values of the sensor
|
||
|
matches, err := filepath.Glob(filepath.Join(path, prefix+"_*"))
|
||
|
if err != nil {
|
||
|
t.Log.Warnf("Couldn't read files from %q: %v", filepath.Join(path, prefix+"_*"), err)
|
||
|
continue
|
||
|
}
|
||
|
for _, fn := range matches {
|
||
|
buf, err = os.ReadFile(fn)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
parts := strings.SplitN(filepath.Base(fn), "_", 2)
|
||
|
if len(parts) != 2 {
|
||
|
continue
|
||
|
}
|
||
|
measurement := parts[1]
|
||
|
|
||
|
// Skip already added values
|
||
|
switch measurement {
|
||
|
case "label", "input":
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
v, err := strconv.ParseFloat(strings.TrimSpace(string(buf)), 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
temp.additional[measurement] = v / scalingFactor
|
||
|
}
|
||
|
|
||
|
stats = append(stats, temp)
|
||
|
}
|
||
|
|
||
|
return stats, nil
|
||
|
}
|
||
|
|
||
|
func (t *Temperature) gatherThermalZone(syspath string) ([]temperatureStat, error) {
|
||
|
// For file layout see https://www.kernel.org/doc/Documentation/thermal/sysfs-api.txt
|
||
|
zones, err := filepath.Glob(filepath.Join(syspath, "class", "thermal", "thermal_zone*"))
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("getting thermal zones failed: %w", err)
|
||
|
}
|
||
|
|
||
|
// Exit early if we cannot find any zone
|
||
|
if len(zones) == 0 {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// Collect the sensor information
|
||
|
stats := make([]temperatureStat, 0, len(zones))
|
||
|
for _, path := range zones {
|
||
|
// Type of the zone corresponding to the sensor name in our nomenclature
|
||
|
buf, err := os.ReadFile(filepath.Join(path, "type"))
|
||
|
if err != nil {
|
||
|
t.Log.Errorf("Cannot read name of zone %q", path)
|
||
|
continue
|
||
|
}
|
||
|
name := strings.TrimSpace(string(buf))
|
||
|
|
||
|
// Actual temperature
|
||
|
buf, err = os.ReadFile(filepath.Join(path, "temp"))
|
||
|
if err != nil {
|
||
|
t.Log.Errorf("Cannot read temperature of zone %q", path)
|
||
|
continue
|
||
|
}
|
||
|
v, err := strconv.ParseFloat(strings.TrimSpace(string(buf)), 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
temp := temperatureStat{name: name, temperature: v / scalingFactor}
|
||
|
stats = append(stats, temp)
|
||
|
}
|
||
|
|
||
|
return stats, nil
|
||
|
}
|