Skip to content

Commit

Permalink
fix(inputs.temp): Recover pre-v1.22.4 temperature sensor readings (in…
Browse files Browse the repository at this point in the history
  • Loading branch information
srebhan authored Jan 18, 2024
1 parent 03bcd85 commit 9aee268
Show file tree
Hide file tree
Showing 70 changed files with 835 additions and 58 deletions.
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

0 comments on commit 9aee268

Please sign in to comment.