telegraf/plugins/inputs/chrony/chrony.go

170 lines
4.4 KiB
Go

//go:generate ../../../tools/readme_config_includer/generator
package chrony
import (
_ "embed"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
fbchrony "github.com/facebook/time/ntp/chrony"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
type Chrony struct {
Server string `toml:"server"`
Timeout config.Duration `toml:"timeout"`
DNSLookup bool `toml:"dns_lookup"`
Log telegraf.Logger `toml:"-"`
conn net.Conn
client *fbchrony.Client
}
func (*Chrony) SampleConfig() string {
return sampleConfig
}
func (c *Chrony) Init() error {
if c.Server != "" {
// Check the specified server address
u, err := url.Parse(c.Server)
if err != nil {
return fmt.Errorf("parsing server address failed: %w", err)
}
switch u.Scheme {
case "unix":
// Keep the server unmodified
case "udp":
// Check if we do have a port and add the default port if we don't
if u.Port() == "" {
u.Host += ":323"
}
// We cannot have path elements in an UDP address
if u.Path != "" {
return fmt.Errorf("path detected in UDP address %q", c.Server)
}
u = &url.URL{Scheme: "udp", Host: u.Host}
default:
return errors.New("unknown or missing address scheme")
}
c.Server = u.String()
}
return nil
}
func (c *Chrony) Start(_ telegraf.Accumulator) error {
if c.Server != "" {
// Create a connection
u, err := url.Parse(c.Server)
if err != nil {
return fmt.Errorf("parsing server address failed: %w", err)
}
switch u.Scheme {
case "unix":
conn, err := net.DialTimeout("unix", u.Path, time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing %q failed: %w", c.Server, err)
}
c.conn = conn
case "udp":
conn, err := net.DialTimeout("udp", u.Host, time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing %q failed: %w", c.Server, err)
}
c.conn = conn
}
} else {
// If no server is given, reproduce chronyc's behavior
if conn, err := net.DialTimeout("unix", "/run/chrony/chronyd.sock", time.Duration(c.Timeout)); err == nil {
c.Server = "unix:///run/chrony/chronyd.sock"
c.conn = conn
} else if conn, err := net.DialTimeout("udp", "127.0.0.1:323", time.Duration(c.Timeout)); err == nil {
c.Server = "udp://127.0.0.1:323"
c.conn = conn
} else {
conn, err := net.DialTimeout("udp", "[::1]:323", time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing server failed: %w", err)
}
c.Server = "udp://[::1]:323"
c.conn = conn
}
}
c.Log.Debugf("Connected to %q...", c.Server)
// Initialize the client
c.client = &fbchrony.Client{Connection: c.conn}
return nil
}
func (c *Chrony) Stop() {
if c.conn != nil {
if err := c.conn.Close(); err != nil {
c.Log.Errorf("Closing connection to %q failed: %v", c.Server, err)
}
}
}
func (c *Chrony) Gather(acc telegraf.Accumulator) error {
req := fbchrony.NewTrackingPacket()
resp, err := c.client.Communicate(req)
if err != nil {
return fmt.Errorf("querying tracking data failed: %w", err)
}
tracking, ok := resp.(*fbchrony.ReplyTracking)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for tracking data", resp)
}
// according to https://github.com/mlichvar/chrony/blob/e11b518a1ffa704986fb1f1835c425844ba248ef/ntp.h#L70
var leapStatus string
switch tracking.LeapStatus {
case 0:
leapStatus = "normal"
case 1:
leapStatus = "insert second"
case 2:
leapStatus = "delete second"
case 3:
leapStatus = "not synchronized"
}
tags := map[string]string{
"leap_status": leapStatus,
"reference_id": strings.ToUpper(strconv.FormatUint(uint64(tracking.RefID), 16)),
"stratum": strconv.FormatUint(uint64(tracking.Stratum), 10),
}
fields := map[string]interface{}{
"frequency": tracking.FreqPPM,
"system_time": tracking.CurrentCorrection,
"last_offset": tracking.LastOffset,
"residual_freq": tracking.ResidFreqPPM,
"rms_offset": tracking.RMSOffset,
"root_delay": tracking.RootDelay,
"root_dispersion": tracking.RootDispersion,
"skew": tracking.SkewPPM,
"update_interval": tracking.LastUpdateInterval,
}
acc.AddFields("chrony", fields, tags)
return nil
}
func init() {
inputs.Add("chrony", func() telegraf.Input {
return &Chrony{Timeout: config.Duration(3 * time.Second)}
})
}