package chrony import ( "errors" "fmt" "os/exec" "strconv" "strings" "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. ) type Chrony struct { DNSLookup bool `toml:"dns_lookup"` path string } func (*Chrony) Description() string { return "Get standard chrony metrics, requires chronyc executable." } func (*Chrony) SampleConfig() string { return ` ## If true, chronyc tries to perform a DNS lookup for the time server. # dns_lookup = false ` } func (c *Chrony) Init() error { var err error c.path, err = exec.LookPath("chronyc") if err != nil { return errors.New("chronyc not found: verify that chrony is installed and that chronyc is in your PATH") } return nil } func (c *Chrony) Gather(acc telegraf.Accumulator) error { flags := []string{} if !c.DNSLookup { flags = append(flags, "-n") } flags = append(flags, "tracking") cmd := execCommand(c.path, flags...) 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)) } fields, tags, err := processChronycOutput(string(out)) if err != nil { return err } acc.AddFields("chrony", fields, tags) return nil } // processChronycOutput takes in a string output from the chronyc command, like: // // Reference ID : 192.168.1.22 (ntp.example.com) // Stratum : 3 // Ref time (UTC) : Thu May 12 14:27:07 2016 // System time : 0.000020390 seconds fast of NTP time // Last offset : +0.000012651 seconds // RMS offset : 0.000025577 seconds // Frequency : 16.001 ppm slow // Residual freq : -0.000 ppm // Skew : 0.006 ppm // Root delay : 0.001655 seconds // Root dispersion : 0.003307 seconds // Update interval : 507.2 seconds // Leap status : Normal // // The value on the left side of the colon is used as field name, if the first field on // the right side is a float. If it cannot be parsed as float, it is a tag name. // // Ref time is ignored and all names are converted to snake case. // // It returns (, ) func processChronycOutput(out string) (map[string]interface{}, map[string]string, error) { tags := map[string]string{} fields := map[string]interface{}{} lines := strings.Split(strings.TrimSpace(out), "\n") for _, line := range lines { stats := strings.Split(line, ":") if len(stats) < 2 { return nil, nil, fmt.Errorf("unexpected output from chronyc, expected ':' in %s", out) } name := strings.ToLower(strings.Replace(strings.TrimSpace(stats[0]), " ", "_", -1)) // ignore reference time if strings.Contains(name, "ref_time") { continue } valueFields := strings.Fields(stats[1]) if len(valueFields) == 0 { return nil, nil, fmt.Errorf("unexpected output from chronyc: %s", out) } if strings.Contains(strings.ToLower(name), "stratum") { tags["stratum"] = valueFields[0] continue } if strings.Contains(strings.ToLower(name), "reference_id") { tags["reference_id"] = valueFields[0] continue } value, err := strconv.ParseFloat(valueFields[0], 64) if err != nil { tags[name] = strings.ToLower(strings.Join(valueFields, " ")) continue } if strings.Contains(stats[1], "slow") { value = -value } fields[name] = value } return fields, tags, nil } func init() { inputs.Add("chrony", func() telegraf.Input { return &Chrony{} }) }