From 1989a5855d3ce0603a77ac931ba1f2bdc77bd843 Mon Sep 17 00:00:00 2001
From: Rene Zbinden <rene.zbinden@postfinance.ch>
Date: Fri, 24 Jun 2016 10:18:02 +0200
Subject: [PATCH] remove cgo dependeny with forking sensors command

closes #1414
closes #649
---
 CHANGELOG.md                                |   1 +
 README.md                                   |   2 +-
 plugins/inputs/sensors/README.md            |  47 +++
 plugins/inputs/sensors/sensors.go           | 141 +++++----
 plugins/inputs/sensors/sensors_nocompile.go |   3 -
 plugins/inputs/sensors/sensors_notlinux.go  |   3 +
 plugins/inputs/sensors/sensors_test.go      | 328 ++++++++++++++++++++
 7 files changed, 464 insertions(+), 61 deletions(-)
 create mode 100644 plugins/inputs/sensors/README.md
 delete mode 100644 plugins/inputs/sensors/sensors_nocompile.go
 create mode 100644 plugins/inputs/sensors/sensors_notlinux.go
 create mode 100644 plugins/inputs/sensors/sensors_test.go

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a252c675..762c7cef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -80,6 +80,7 @@ consistent with the behavior of `collection_jitter`.
 - [#1296](https://github.com/influxdata/telegraf/issues/1296): Refactor of flush_jitter argument.
 - [#1213](https://github.com/influxdata/telegraf/issues/1213): Add inactive & active memory to mem plugin.
 - [#1543](https://github.com/influxdata/telegraf/pull/1543): Official Windows service.
+- [#1414](https://github.com/influxdata/telegraf/pull/1414): Forking sensors command to remove C package dependency.
 
 ### Bugfixes
 
diff --git a/README.md b/README.md
index 9d2ee3ce..74bbf2a4 100644
--- a/README.md
+++ b/README.md
@@ -188,7 +188,7 @@ Currently implemented sources:
 * [redis](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/redis)
 * [rethinkdb](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/rethinkdb)
 * [riak](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/riak)
-* [sensors ](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sensors) (only available if built from source)
+* [sensors](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sensors)
 * [snmp](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp)
 * [sql server](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sqlserver) (microsoft)
 * [twemproxy](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/twemproxy)
diff --git a/plugins/inputs/sensors/README.md b/plugins/inputs/sensors/README.md
new file mode 100644
index 00000000..237a9b78
--- /dev/null
+++ b/plugins/inputs/sensors/README.md
@@ -0,0 +1,47 @@
+# sensors Input Plugin
+
+Collect [lm-sensors](https://en.wikipedia.org/wiki/Lm_sensors) metrics - requires the lm-sensors
+package installed.
+
+This plugin collects sensor metrics with the `sensors` executable from the lm-sensor package.
+
+### Configuration:
+```
+# Monitor sensors, requires lm-sensors package
+[[inputs.sensors]]
+  ## Remove numbers from field names.
+  ## If true, a field name like 'temp1_input' will be changed to 'temp_input'.
+  # remove_numbers = true
+```
+
+### Measurements & Fields:
+Fields are created dynamicaly depending on the sensors. All fields are float.
+
+### Tags:
+
+- All measurements have the following tags:
+    - chip
+    - feature
+
+### Example Output:
+
+#### Default
+```
+$ telegraf -config telegraf.conf -input-filter sensors -test
+* Plugin: sensors, Collection 1
+> sensors,chip=power_meter-acpi-0,feature=power1 power_average=0,power_average_interval=300 1466751326000000000
+> sensors,chip=k10temp-pci-00c3,feature=temp1 temp_crit=70,temp_crit_hyst=65,temp_input=29,temp_max=70 1466751326000000000
+> sensors,chip=k10temp-pci-00cb,feature=temp1 temp_input=29,temp_max=70 1466751326000000000
+> sensors,chip=k10temp-pci-00d3,feature=temp1 temp_input=27.5,temp_max=70 1466751326000000000
+> sensors,chip=k10temp-pci-00db,feature=temp1 temp_crit=70,temp_crit_hyst=65,temp_input=29.5,temp_max=70 1466751326000000000
+```
+
+#### With remove_numbers=false
+```
+* Plugin: sensors, Collection 1
+> sensors,chip=power_meter-acpi-0,feature=power1 power1_average=0,power1_average_interval=300 1466753424000000000
+> sensors,chip=k10temp-pci-00c3,feature=temp1 temp1_crit=70,temp1_crit_hyst=65,temp1_input=29.125,temp1_max=70 1466753424000000000
+> sensors,chip=k10temp-pci-00cb,feature=temp1 temp1_input=29,temp1_max=70 1466753424000000000
+> sensors,chip=k10temp-pci-00d3,feature=temp1 temp1_input=29.5,temp1_max=70 1466753424000000000
+> sensors,chip=k10temp-pci-00db,feature=temp1 temp1_crit=70,temp1_crit_hyst=65,temp1_input=30,temp1_max=70 1466753424000000000
+```
diff --git a/plugins/inputs/sensors/sensors.go b/plugins/inputs/sensors/sensors.go
index dbb304b7..6e165e4c 100644
--- a/plugins/inputs/sensors/sensors.go
+++ b/plugins/inputs/sensors/sensors.go
@@ -1,91 +1,118 @@
-// +build linux,sensors
+// +build linux
 
 package sensors
 
 import (
+	"errors"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strconv"
 	"strings"
-
-	"github.com/md14454/gosensors"
+	"time"
 
 	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/internal"
 	"github.com/influxdata/telegraf/plugins/inputs"
 )
 
+var (
+	execCommand = exec.Command // execCommand is used to mock commands in tests.
+	numberRegp  = regexp.MustCompile("[0-9]+")
+)
+
 type Sensors struct {
-	Sensors []string
+	RemoveNumbers bool `toml:"remove_numbers"`
+	path          string
 }
 
-func (_ *Sensors) Description() string {
-	return "Monitor sensors using lm-sensors package"
+func (*Sensors) Description() string {
+	return "Monitor sensors, requires lm-sensors package"
 }
 
-var sensorsSampleConfig = `
-  ## By default, telegraf gathers stats from all sensors detected by the
-  ## lm-sensors module.
-  ##
-  ## Only collect stats from the selected sensors. Sensors are listed as
-  ## <chip name>:<feature name>. This information can be found by running the
-  ## sensors command, e.g. sensors -u
-  ##
-  ## A * as the feature name will return all features of the chip
-  ##
-  # sensors = ["coretemp-isa-0000:Core 0", "coretemp-isa-0001:*"]
+func (*Sensors) SampleConfig() string {
+	return `
+  ## Remove numbers from field names.
+  ## If true, a field name like 'temp1_input' will be changed to 'temp_input'.
+  # remove_numbers = true
 `
 
-func (_ *Sensors) SampleConfig() string {
-	return sensorsSampleConfig
 }
 
 func (s *Sensors) Gather(acc telegraf.Accumulator) error {
-	gosensors.Init()
-	defer gosensors.Cleanup()
-
-	for _, chip := range gosensors.GetDetectedChips() {
-		for _, feature := range chip.GetFeatures() {
-			chipName := chip.String()
-			featureLabel := feature.GetLabel()
-
-			if len(s.Sensors) != 0 {
-				var found bool
-
-				for _, sensor := range s.Sensors {
-					parts := strings.SplitN(sensor, ":", 2)
+	if len(s.path) == 0 {
+		return errors.New("sensors not found: verify that lm-sensors package is installed and that sensors is in your PATH")
+	}
 
-					if parts[0] == chipName {
-						if parts[1] == "*" || parts[1] == featureLabel {
-							found = true
-							break
-						}
-					}
-				}
+	return s.parse(acc)
+}
 
-				if !found {
-					continue
-				}
+// parse forks the command:
+//     sensors -u -A
+// and parses the output to add it to the telegraf.Accumulator.
+func (s *Sensors) parse(acc telegraf.Accumulator) error {
+	tags := map[string]string{}
+	fields := map[string]interface{}{}
+	chip := ""
+	cmd := execCommand(s.path, "-A", "-u")
+	out, err := internal.CombinedOutputTimeout(cmd, time.Second*5)
+	if err != nil {
+		return fmt.Errorf("failed to run command %s: %s - %s", strings.Join(cmd.Args, " "), err, string(out))
+	}
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
+	for _, line := range lines {
+		if len(line) == 0 {
+			acc.AddFields("sensors", fields, tags)
+			chip = ""
+			tags = map[string]string{}
+			fields = map[string]interface{}{}
+			continue
+		}
+		if len(chip) == 0 {
+			chip = line
+			tags["chip"] = chip
+			continue
+		}
+		if !strings.HasPrefix(line, " ") {
+			if len(tags) > 1 {
+				acc.AddFields("sensors", fields, tags)
 			}
-
-			tags := map[string]string{
-				"chip":          chipName,
-				"adapter":       chip.AdapterName(),
-				"feature-name":  feature.Name,
-				"feature-label": featureLabel,
+			fields = map[string]interface{}{}
+			tags = map[string]string{
+				"chip":    chip,
+				"feature": strings.TrimRight(snake(line), ":"),
 			}
-
-			fieldName := chipName + ":" + featureLabel
-
-			fields := map[string]interface{}{
-				fieldName: feature.GetValue(),
+		} else {
+			splitted := strings.Split(line, ":")
+			fieldName := strings.TrimSpace(splitted[0])
+			if s.RemoveNumbers {
+				fieldName = numberRegp.ReplaceAllString(fieldName, "")
 			}
-
-			acc.AddFields("sensors", fields, tags)
+			fieldValue, err := strconv.ParseFloat(strings.TrimSpace(splitted[1]), 64)
+			if err != nil {
+				return err
+			}
+			fields[fieldName] = fieldValue
 		}
 	}
-
+	acc.AddFields("sensors", fields, tags)
 	return nil
 }
 
 func init() {
+	s := Sensors{
+		RemoveNumbers: true,
+	}
+	path, _ := exec.LookPath("sensors")
+	if len(path) > 0 {
+		s.path = path
+	}
 	inputs.Add("sensors", func() telegraf.Input {
-		return &Sensors{}
+		return &s
 	})
 }
+
+// snake converts string to snake case
+func snake(input string) string {
+	return strings.ToLower(strings.Replace(input, " ", "_", -1))
+}
diff --git a/plugins/inputs/sensors/sensors_nocompile.go b/plugins/inputs/sensors/sensors_nocompile.go
deleted file mode 100644
index 5c38a437..00000000
--- a/plugins/inputs/sensors/sensors_nocompile.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// +build !linux !sensors
-
-package sensors
diff --git a/plugins/inputs/sensors/sensors_notlinux.go b/plugins/inputs/sensors/sensors_notlinux.go
new file mode 100644
index 00000000..62a62115
--- /dev/null
+++ b/plugins/inputs/sensors/sensors_notlinux.go
@@ -0,0 +1,3 @@
+// +build !linux
+
+package sensors
diff --git a/plugins/inputs/sensors/sensors_test.go b/plugins/inputs/sensors/sensors_test.go
new file mode 100644
index 00000000..01d27abc
--- /dev/null
+++ b/plugins/inputs/sensors/sensors_test.go
@@ -0,0 +1,328 @@
+// +build linux
+
+package sensors
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"testing"
+
+	"github.com/influxdata/telegraf/testutil"
+)
+
+func TestGatherDefault(t *testing.T) {
+	s := Sensors{
+		RemoveNumbers: true,
+		path:          "sensors",
+	}
+	// overwriting exec commands with mock commands
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	var acc testutil.Accumulator
+
+	err := s.Gather(&acc)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var tests = []struct {
+		tags   map[string]string
+		fields map[string]interface{}
+	}{
+		{
+			map[string]string{
+				"chip":    "acpitz-virtual-0",
+				"feature": "temp1",
+			},
+			map[string]interface{}{
+				"temp_input": 8.3,
+				"temp_crit":  31.3,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "power_meter-acpi-0",
+				"feature": "power1",
+			},
+			map[string]interface{}{
+				"power_average":          0.0,
+				"power_average_interval": 300.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "physical_id_0",
+			},
+			map[string]interface{}{
+				"temp_input":      77.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "core_0",
+			},
+			map[string]interface{}{
+				"temp_input":      75.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "core_1",
+			},
+			map[string]interface{}{
+				"temp_input":      77.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "physical_id_1",
+			},
+			map[string]interface{}{
+				"temp_input":      70.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "core_0",
+			},
+			map[string]interface{}{
+				"temp_input":      66.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "core_1",
+			},
+			map[string]interface{}{
+				"temp_input":      70.0,
+				"temp_max":        82.0,
+				"temp_crit":       92.0,
+				"temp_crit_alarm": 0.0,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		acc.AssertContainsTaggedFields(t, "sensors", test.fields, test.tags)
+	}
+}
+
+func TestGatherNotRemoveNumbers(t *testing.T) {
+	s := Sensors{
+		RemoveNumbers: false,
+		path:          "sensors",
+	}
+	// overwriting exec commands with mock commands
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	var acc testutil.Accumulator
+
+	err := s.Gather(&acc)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var tests = []struct {
+		tags   map[string]string
+		fields map[string]interface{}
+	}{
+		{
+			map[string]string{
+				"chip":    "acpitz-virtual-0",
+				"feature": "temp1",
+			},
+			map[string]interface{}{
+				"temp1_input": 8.3,
+				"temp1_crit":  31.3,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "power_meter-acpi-0",
+				"feature": "power1",
+			},
+			map[string]interface{}{
+				"power1_average":          0.0,
+				"power1_average_interval": 300.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "physical_id_0",
+			},
+			map[string]interface{}{
+				"temp1_input":      77.0,
+				"temp1_max":        82.0,
+				"temp1_crit":       92.0,
+				"temp1_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "core_0",
+			},
+			map[string]interface{}{
+				"temp2_input":      75.0,
+				"temp2_max":        82.0,
+				"temp2_crit":       92.0,
+				"temp2_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0000",
+				"feature": "core_1",
+			},
+			map[string]interface{}{
+				"temp3_input":      77.0,
+				"temp3_max":        82.0,
+				"temp3_crit":       92.0,
+				"temp3_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "physical_id_1",
+			},
+			map[string]interface{}{
+				"temp1_input":      70.0,
+				"temp1_max":        82.0,
+				"temp1_crit":       92.0,
+				"temp1_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "core_0",
+			},
+			map[string]interface{}{
+				"temp2_input":      66.0,
+				"temp2_max":        82.0,
+				"temp2_crit":       92.0,
+				"temp2_crit_alarm": 0.0,
+			},
+		},
+		{
+			map[string]string{
+				"chip":    "coretemp-isa-0001",
+				"feature": "core_1",
+			},
+			map[string]interface{}{
+				"temp3_input":      70.0,
+				"temp3_max":        82.0,
+				"temp3_crit":       92.0,
+				"temp3_crit_alarm": 0.0,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		acc.AssertContainsTaggedFields(t, "sensors", test.fields, test.tags)
+	}
+}
+
+// fackeExecCommand is a helper function that mock
+// the exec.Command call (and call the test binary)
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+	cs := []string{"-test.run=TestHelperProcess", "--", command}
+	cs = append(cs, args...)
+	cmd := exec.Command(os.Args[0], cs...)
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	return cmd
+}
+
+// TestHelperProcess isn't a real test. It's used to mock exec.Command
+// For example, if you run:
+// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcess -- chrony tracking
+// it returns below mockData.
+func TestHelperProcess(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+		return
+	}
+
+	mockData := `acpitz-virtual-0
+temp1:
+  temp1_input: 8.300
+  temp1_crit: 31.300
+
+power_meter-acpi-0
+power1:
+  power1_average: 0.000
+  power1_average_interval: 300.000
+
+coretemp-isa-0000
+Physical id 0:
+  temp1_input: 77.000
+  temp1_max: 82.000
+  temp1_crit: 92.000
+  temp1_crit_alarm: 0.000
+Core 0:
+  temp2_input: 75.000
+  temp2_max: 82.000
+  temp2_crit: 92.000
+  temp2_crit_alarm: 0.000
+Core 1:
+  temp3_input: 77.000
+  temp3_max: 82.000
+  temp3_crit: 92.000
+  temp3_crit_alarm: 0.000
+
+coretemp-isa-0001
+Physical id 1:
+  temp1_input: 70.000
+  temp1_max: 82.000
+  temp1_crit: 92.000
+  temp1_crit_alarm: 0.000
+Core 0:
+  temp2_input: 66.000
+  temp2_max: 82.000
+  temp2_crit: 92.000
+  temp2_crit_alarm: 0.000
+Core 1:
+  temp3_input: 70.000
+  temp3_max: 82.000
+  temp3_crit: 92.000
+  temp3_crit_alarm: 0.000
+`
+
+	args := os.Args
+
+	// Previous arguments are tests stuff, that looks like :
+	// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
+	cmd, args := args[3], args[4:]
+
+	if cmd == "sensors" {
+		fmt.Fprint(os.Stdout, mockData)
+	} else {
+		fmt.Fprint(os.Stdout, "command not found")
+		os.Exit(1)
+
+	}
+	os.Exit(0)
+}
-- 
GitLab