Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(inputs.temp): Recover pre-v1.22.4 temperature sensor readings #14575

Merged
merged 5 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cmd/telegraf/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,14 @@ func TestUsageFlag(t *testing.T) {
ExpectedOutput: `
# Read metrics about temperature
[[inputs.temp]]
# no configuration
## Desired output format (Linux only)
## Available values are
## v1 -- use pre-v1.22.4 sensor naming, e.g. coretemp_core0_input
## v2 -- use v1.22.4+ sensor naming, e.g. coretemp_core_0_input
# metric_format = "v2"

## Add device tag to distinguish devices with the same name (Linux only)
# add_device_tag = false

`,
},
Expand Down
13 changes: 0 additions & 13 deletions plugins/inputs/system/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"

Expand All @@ -26,7 +25,6 @@ type PS interface {
SwapStat() (*mem.SwapMemoryStat, error)
NetConnections() ([]net.ConnectionStat, error)
NetConntrack(perCPU bool) ([]net.ConntrackStat, error)
Temperature() ([]host.TemperatureStat, error)
}

type PSDiskDeps interface {
Expand Down Expand Up @@ -214,17 +212,6 @@ func (s *SystemPS) SwapStat() (*mem.SwapMemoryStat, error) {
return mem.SwapMemory()
}

func (s *SystemPS) Temperature() ([]host.TemperatureStat, error) {
temp, err := host.SensorsTemperatures()
if err != nil {
var hostWarnings *host.Warnings
if !errors.As(err, &hostWarnings) {
return temp, err
}
}
return temp, nil
}

func (s *SystemPSDisk) Partitions(all bool) ([]disk.PartitionStat, error) {
return disk.Partitions(all)
}
Expand Down
9 changes: 8 additions & 1 deletion plugins/inputs/temp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
```toml @sample.conf
# Read metrics about temperature
[[inputs.temp]]
# no configuration
## Desired output format (Linux only)
## Available values are
## v1 -- use pre-v1.22.4 sensor naming, e.g. coretemp_core0_input
## v2 -- use v1.22.4+ sensor naming, e.g. coretemp_core_0_input
# metric_format = "v2"

## Add device tag to distinguish devices with the same name (Linux only)
# add_device_tag = false
```

## Metrics
Expand Down
9 changes: 8 additions & 1 deletion plugins/inputs/temp/sample.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Read metrics about temperature
[[inputs.temp]]
# no configuration
## Desired output format (Linux only)
## Available values are
## v1 -- use pre-v1.22.4 sensor naming, e.g. coretemp_core0_input
## v2 -- use v1.22.4+ sensor naming, e.g. coretemp_core_0_input
# metric_format = "v2"

## Add device tag to distinguish devices with the same name (Linux only)
# add_device_tag = false
29 changes: 4 additions & 25 deletions plugins/inputs/temp/temp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,26 @@ package temp

import (
_ "embed"
"fmt"
"strings"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/inputs/system"
)

//go:embed sample.conf
var sampleConfig string

type Temperature struct {
ps system.PS
MetricFormat string `toml:"metric_format"`
DeviceTag bool `toml:"add_device_tag"`
Log telegraf.Logger `toml:"-"`
}

func (*Temperature) SampleConfig() string {
return sampleConfig
}

func (t *Temperature) Gather(acc telegraf.Accumulator) error {
temps, err := t.ps.Temperature()
if err != nil {
if strings.Contains(err.Error(), "not implemented yet") {
return fmt.Errorf("plugin is not supported on this platform: %w", err)
}
return fmt.Errorf("error getting temperatures info: %w", err)
}
for _, temp := range temps {
tags := map[string]string{
"sensor": temp.SensorKey,
}
fields := map[string]interface{}{
"temp": temp.Temperature,
}
acc.AddFields("temp", fields, tags)
}
return nil
}

func init() {
inputs.Add("temp", func() telegraf.Input {
return &Temperature{ps: system.NewSystemPS()}
return &Temperature{}
})
}
249 changes: 249 additions & 0 deletions plugins/inputs/temp/temp_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//go:build linux
// +build linux

package temp

import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/influxdata/telegraf"
)

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 := os.Getenv("HOST_SYS")
if path == "" {
path = "/sys"
}

// 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)
}
}

for _, temp := range temperatures {
acc.AddFields(
"temp",
map[string]interface{}{"temp": temp.Temperature},
t.getTagsForTemperature(temp, "_input"),
)

for measurement, value := range temp.Additional {
fieldname := "temp"
if measurement == "alarm" {
fieldname = "active"
}
acc.AddFields(
"temp",
map[string]interface{}{fieldname: value},
t.getTagsForTemperature(temp, "_"+measurement),
)
}
}
return nil
}

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.Warnf("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
}

// Alarm (optional)
fn = filepath.Join(path, prefix+"_alarm")
buf, err = os.ReadFile(fn)
if err == nil {
if a, err := strconv.ParseBool(strings.TrimSpace(string(buf))); err == nil {
temp.Additional["alarm"] = a
}
}

// 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", "alarm":
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
}

func (t *Temperature) getTagsForTemperature(temp TemperatureStat, suffix string) map[string]string {
sensor := temp.Name
if temp.Label != "" && suffix != "" {
switch t.MetricFormat {
case "v1":
sensor += "_" + strings.ReplaceAll(temp.Label, " ", "") + suffix
case "v2":
sensor += "_" + strings.ReplaceAll(temp.Label, " ", "_") + suffix
}
}

tags := map[string]string{"sensor": sensor}
if t.DeviceTag {
tags["device"] = temp.Device
}
return tags
}
Loading
Loading