feat(inputs.chrony): Remove chronyc dependency (#14629)
This commit is contained in:
parent
43fd26a7a0
commit
120167501b
|
|
@ -128,6 +128,7 @@ following works:
|
|||
- github.com/eclipse/paho.golang [Eclipse Public License - v 2.0](https://github.com/eclipse/paho.golang/blob/master/LICENSE)
|
||||
- github.com/eclipse/paho.mqtt.golang [Eclipse Public License - v 2.0](https://github.com/eclipse/paho.mqtt.golang/blob/master/LICENSE)
|
||||
- github.com/emicklei/go-restful [MIT License](https://github.com/emicklei/go-restful/blob/v3/LICENSE)
|
||||
- github.com/facebook/time [Apache License 2.0](https://github.com/facebook/time/blob/main/LICENSE)
|
||||
- github.com/fatih/color [MIT License](https://github.com/fatih/color/blob/master/LICENSE.md)
|
||||
- github.com/felixge/httpsnoop [MIT License](https://github.com/felixge/httpsnoop/blob/master/LICENSE.txt)
|
||||
- github.com/form3tech-oss/jwt-go [MIT License](https://github.com/form3tech-oss/jwt-go/blob/master/LICENSE)
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -62,7 +62,7 @@ require (
|
|||
github.com/compose-spec/compose-go v1.20.2
|
||||
github.com/coocood/freecache v1.2.3
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/couchbase/go-couchbase v0.1.1
|
||||
github.com/datadope-io/go-zabbix/v2 v2.0.1
|
||||
|
|
@ -74,6 +74,7 @@ require (
|
|||
github.com/dynatrace-oss/dynatrace-metric-utils-go v0.5.0
|
||||
github.com/eclipse/paho.golang v0.11.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.4.3
|
||||
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
|
|
|
|||
5
go.sum
5
go.sum
|
|
@ -1010,8 +1010,9 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
|||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
|
|
@ -1108,6 +1109,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
|
||||
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e h1:PaVm1gMon1pkJdbzoyw2VUCn07cA6RDbGdTbsppjcY8=
|
||||
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e/go.mod h1:NBW7VY75BPCsOPrSsARAengTlgOICicHey4Bv70lCUI=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4 h1:fP04zlkPjAGpsduG7xN3rRkxjAqkJaIQnnkNYYw/pAk=
|
||||
|
|
|
|||
|
|
@ -1,63 +1,9 @@
|
|||
# chrony Input Plugin
|
||||
|
||||
Get standard chrony metrics, requires chronyc executable.
|
||||
This plugin queries metrics from a chrony NTP server. For details on the
|
||||
meaning of the gathered fields please check the [chronyc manual][]
|
||||
|
||||
Below is the documentation of the various headers returned by `chronyc
|
||||
tracking`.
|
||||
|
||||
- Reference ID - This is the refid and name (or IP address) if available, of the
|
||||
server to which the computer is currently synchronised. If this is 127.127.1.1
|
||||
it means the computer is not synchronised to any external source and that you
|
||||
have the ‘local’ mode operating (via the local command in chronyc (see section
|
||||
local), or the local directive in the ‘/etc/chrony.conf’ file (see section
|
||||
local)).
|
||||
- Stratum - The stratum indicates how many hops away from a computer with an
|
||||
attached reference clock we are. Such a computer is a stratum-1 computer, so
|
||||
the computer in the example is two hops away (i.e. a.b.c is a stratum-2 and is
|
||||
synchronised from a stratum-1).
|
||||
- Ref time - This is the time (UTC) at which the last measurement from the
|
||||
reference source was processed.
|
||||
- System time - In normal operation, chronyd never steps the system clock,
|
||||
because any jump in the timescale can have adverse consequences for certain
|
||||
application programs. Instead, any error in the system clock is corrected by
|
||||
slightly speeding up or slowing down the system clock until the error has been
|
||||
removed, and then returning to the system clock’s normal speed. A consequence
|
||||
of this is that there will be a period when the system clock (as read by other
|
||||
programs using the gettimeofday() system call, or by the date command in the
|
||||
shell) will be different from chronyd's estimate of the current true time
|
||||
(which it reports to NTP clients when it is operating in server mode). The
|
||||
value reported on this line is the difference due to this effect.
|
||||
- Last offset - This is the estimated local offset on the last clock update.
|
||||
- RMS offset - This is a long-term average of the offset value.
|
||||
- Frequency - The ‘frequency’ is the rate by which the system’s clock would be
|
||||
wrong if chronyd was not correcting it. It is expressed in ppm (parts per
|
||||
million). For example, a value of 1ppm would mean that when the system’s
|
||||
clock thinks it has advanced 1 second, it has actually advanced by 1.000001
|
||||
seconds relative to true time.
|
||||
- Residual freq - This shows the ‘residual frequency’ for the currently selected
|
||||
reference source. This reflects any difference between what the measurements
|
||||
from the reference source indicate the frequency should be and the frequency
|
||||
currently being used. The reason this is not always zero is that a smoothing
|
||||
procedure is applied to the frequency. Each time a measurement from the
|
||||
reference source is obtained and a new residual frequency computed, the
|
||||
estimated accuracy of this residual is compared with the estimated accuracy
|
||||
(see ‘skew’ next) of the existing frequency value. A weighted average is
|
||||
computed for the new frequency, with weights depending on these accuracies. If
|
||||
the measurements from the reference source follow a consistent trend, the
|
||||
residual will be driven to zero over time.
|
||||
- Skew - This is the estimated error bound on the frequency.
|
||||
- Root delay - This is the total of the network path delays to the stratum-1
|
||||
computer from which the computer is ultimately synchronised. In certain
|
||||
extreme situations, this value can be negative. (This can arise in a symmetric
|
||||
peer arrangement where the computers’ frequencies are not tracking each other
|
||||
and the network delay is very short relative to the turn-around time at each
|
||||
computer.)
|
||||
- Root dispersion - This is the total dispersion accumulated through all the
|
||||
computers back to the stratum-1 computer from which the computer is ultimately
|
||||
synchronised. Dispersion is due to system clock resolution, statistical
|
||||
measurement variations etc.
|
||||
- Leap status - This is the leap status, which can be Normal, Insert second,
|
||||
Delete second or Not synchronised.
|
||||
[chronyc manual]: https://chrony-project.org/doc/4.4/chronyc.html
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
|
|
@ -73,7 +19,17 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
|||
```toml @sample.conf
|
||||
# Get standard chrony metrics, requires chronyc executable.
|
||||
[[inputs.chrony]]
|
||||
## If true, chronyc tries to perform a DNS lookup for the time server.
|
||||
## Server address of chronyd with address scheme
|
||||
## If empty or not set, the plugin will mimic the behavior of chronyc and
|
||||
## check "unix:///run/chrony/chronyd.sock", "udp://127.0.0.1:323"
|
||||
## and "udp://[::1]:323".
|
||||
# server = ""
|
||||
|
||||
## Timeout for establishing the connection
|
||||
# timeout = "5s"
|
||||
|
||||
## Try to resolve received addresses to host-names via DNS lookups
|
||||
## Disabled by default to avoid DNS queries especially for slow DNS servers.
|
||||
# dns_lookup = false
|
||||
```
|
||||
|
||||
|
|
@ -100,5 +56,5 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
|||
## Example Output
|
||||
|
||||
```text
|
||||
chrony,leap_status=normal,reference_id=192.168.1.1,stratum=3 frequency=-35.657,system_time=0.000027073,last_offset=-0.000013616,residual_freq=-0,rms_offset=0.000027073,root_delay=0.000644,root_dispersion=0.003444,skew=0.001,update_interval=1031.2 1463750789687639161
|
||||
chrony,leap_status=not\ synchronized,reference_id=A29FC87B,stratum=3 frequency=-16.000999450683594,last_offset=0.000012651000361074694,residual_freq=0,rms_offset=0.000025576999178156257,root_delay=0.0016550000291317701,root_dispersion=0.00330700003542006,skew=0.006000000052154064,system_time=0.000020389999917824753,update_interval=507.1999816894531 1706271167571675297
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,26 +5,30 @@ import (
|
|||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
fbchrony "github.com/facebook/time/ntp/chrony"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
execCommand = exec.Command // execCommand is used to mock commands in tests.
|
||||
)
|
||||
|
||||
type Chrony struct {
|
||||
DNSLookup bool `toml:"dns_lookup"`
|
||||
path string
|
||||
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 {
|
||||
|
|
@ -32,98 +36,134 @@ func (*Chrony) SampleConfig() string {
|
|||
}
|
||||
|
||||
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")
|
||||
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 {
|
||||
flags := []string{}
|
||||
if !c.DNSLookup {
|
||||
flags = append(flags, "-n")
|
||||
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)
|
||||
}
|
||||
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 %q: %w - %s", strings.Join(cmd.Args, " "), err, string(out))
|
||||
// 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"
|
||||
}
|
||||
fields, tags, err := processChronycOutput(string(out))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 (<fields>, <tags>)
|
||||
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.ReplaceAll(strings.TrimSpace(stats[0]), " ", "_"))
|
||||
// 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{}
|
||||
return &Chrony{Timeout: config.Duration(3 * time.Second)}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +1,329 @@
|
|||
package chrony
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"math"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
fbchrony "github.com/facebook/time/ntp/chrony"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
c := Chrony{
|
||||
path: "chronyc",
|
||||
// Setup a mock server
|
||||
server := Server{
|
||||
TrackingInfo: &fbchrony.Tracking{
|
||||
RefID: 0xA29FC87B,
|
||||
IPAddr: net.ParseIP("192.168.1.22"),
|
||||
Stratum: 3,
|
||||
LeapStatus: 3,
|
||||
RefTime: time.Now(),
|
||||
CurrentCorrection: 0.000020390,
|
||||
LastOffset: 0.000012651,
|
||||
RMSOffset: 0.000025577,
|
||||
FreqPPM: -16.001,
|
||||
ResidFreqPPM: 0.0,
|
||||
SkewPPM: 0.006,
|
||||
RootDelay: 0.001655,
|
||||
RootDispersion: 0.003307,
|
||||
LastUpdateInterval: 507.2,
|
||||
},
|
||||
}
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommand
|
||||
defer func() { execCommand = exec.Command }()
|
||||
addr, err := server.Listen(t)
|
||||
require.NoError(t, err)
|
||||
defer server.Shutdown()
|
||||
|
||||
// Setup the plugin
|
||||
plugin := &Chrony{
|
||||
Server: "udp://" + addr,
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
// Start the plugin, do a gather and stop everything
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Start(&acc))
|
||||
defer plugin.Stop()
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
plugin.Stop()
|
||||
server.Shutdown()
|
||||
|
||||
err := c.Gather(&acc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// Do the comparison
|
||||
expected := []telegraf.Metric{
|
||||
metric.New(
|
||||
"chrony",
|
||||
map[string]string{
|
||||
"reference_id": "A29FC87B",
|
||||
"leap_status": "not synchronized",
|
||||
"stratum": "3",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"system_time": 0.000020390,
|
||||
"last_offset": 0.000012651,
|
||||
"rms_offset": 0.000025577,
|
||||
"frequency": -16.001,
|
||||
"residual_freq": 0.0,
|
||||
"skew": 0.006,
|
||||
"root_delay": 0.001655,
|
||||
"root_dispersion": 0.003307,
|
||||
"update_interval": 507.2,
|
||||
},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"reference_id": "192.168.1.22",
|
||||
"leap_status": "not synchronized",
|
||||
"stratum": "3",
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
"system_time": 0.000020390,
|
||||
"last_offset": 0.000012651,
|
||||
"rms_offset": 0.000025577,
|
||||
"frequency": -16.001,
|
||||
"residual_freq": 0.0,
|
||||
"skew": 0.006,
|
||||
"root_delay": 0.001655,
|
||||
"root_dispersion": 0.003307,
|
||||
"update_interval": 507.2,
|
||||
options := []cmp.Option{
|
||||
// tests on linux with go1.20 will add a warning about code coverage, ignore that tag
|
||||
testutil.IgnoreTags("warning"),
|
||||
testutil.IgnoreTime(),
|
||||
cmpopts.EquateApprox(0.001, 0),
|
||||
}
|
||||
|
||||
// tests on linux with go1.20 will add a warning about code coverage
|
||||
// due to the code coverage dir not being set
|
||||
delete(acc.Metrics[0].Tags, "warning")
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "chrony", fields, tags)
|
||||
|
||||
// test with dns lookup
|
||||
c.DNSLookup = true
|
||||
err = c.Gather(&acc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "chrony", fields, tags)
|
||||
actual := acc.GetTelegrafMetrics()
|
||||
testutil.RequireMetricsEqual(t, expected, actual, options...)
|
||||
}
|
||||
|
||||
// fakeExecCommand 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
|
||||
func TestIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Start the docker container
|
||||
container := testutil.Container{
|
||||
Image: "dockurr/chrony",
|
||||
ExposedPorts: []string{"323/udp"},
|
||||
Files: map[string]string{
|
||||
"/etc/telegraf-chrony.conf": "testdata/chrony.conf",
|
||||
"/start.sh": "testdata/start.sh",
|
||||
},
|
||||
Entrypoint: []string{"/start.sh"},
|
||||
WaitingFor: wait.ForLog("Selected source"),
|
||||
}
|
||||
require.NoError(t, container.Start(), "failed to start container")
|
||||
defer container.Terminate()
|
||||
|
||||
// Setup the plugin
|
||||
plugin := &Chrony{
|
||||
Server: "udp://" + container.Address + ":" + container.Ports["323"],
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
// Collect the metrics and compare
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Start(&acc))
|
||||
defer plugin.Stop()
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
|
||||
// Setup the expectations
|
||||
expected := []telegraf.Metric{
|
||||
metric.New(
|
||||
"chrony",
|
||||
map[string]string{
|
||||
"leap_status": "normal",
|
||||
"reference_id": "A29FC87B",
|
||||
"stratum": "4",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"frequency": float64(0),
|
||||
"last_offset": float64(0),
|
||||
"residual_freq": float64(0),
|
||||
"rms_offset": float64(0),
|
||||
"root_delay": float64(0),
|
||||
"root_dispersion": float64(0),
|
||||
"skew": float64(0),
|
||||
"system_time": float64(0),
|
||||
"update_interval": float64(0),
|
||||
},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
options := []cmp.Option{
|
||||
testutil.IgnoreTags("leap_status", "reference_id", "stratum"),
|
||||
testutil.IgnoreTime(),
|
||||
}
|
||||
|
||||
actual := acc.GetTelegrafMetrics()
|
||||
testutil.RequireMetricsStructureEqual(t, expected, actual, options...)
|
||||
}
|
||||
|
||||
// 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(_ *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
type Server struct {
|
||||
TrackingInfo *fbchrony.Tracking
|
||||
|
||||
conn net.PacketConn
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
lookup := "Reference ID : 192.168.1.22 (ntp.example.com)\n"
|
||||
noLookup := "Reference ID : 192.168.1.22 (192.168.1.22)\n"
|
||||
mockData := `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 : Not synchronized
|
||||
`
|
||||
|
||||
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 != "chronyc" {
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(1)
|
||||
func (s *Server) Listen(t *testing.T) (string, error) {
|
||||
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if args[0] == "tracking" {
|
||||
fmt.Fprint(os.Stdout, lookup+mockData)
|
||||
s.conn = conn
|
||||
addr := s.conn.LocalAddr().String()
|
||||
|
||||
go s.serve(t)
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func (s *Server) serve(t *testing.T) {
|
||||
defer s.conn.Close()
|
||||
|
||||
for {
|
||||
buf := make([]byte, 4096)
|
||||
n, addr, err := s.conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Logf("mock server: received %d bytes from %q\n", n, addr.String())
|
||||
|
||||
var header fbchrony.RequestHead
|
||||
data := bytes.NewBuffer(buf)
|
||||
if err := binary.Read(data, binary.BigEndian, &header); err != nil {
|
||||
t.Logf("mock server: reading request header failed: %v", err)
|
||||
return
|
||||
}
|
||||
seqno := header.Sequence + 1
|
||||
|
||||
switch header.Command {
|
||||
case 33: // tracking
|
||||
_, err := s.conn.WriteTo(s.encodeTrackingReply(seqno), addr)
|
||||
if err != nil {
|
||||
t.Logf("mock server [tracking]: writing reply failed: %v", err)
|
||||
} else {
|
||||
t.Log("mock server [tracking]: successfully wrote reply")
|
||||
}
|
||||
default:
|
||||
t.Logf("mock server: unhandled command %v", header.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) encodeTrackingReply(sequence uint32) []byte {
|
||||
t := s.TrackingInfo
|
||||
|
||||
// Encode the header
|
||||
buf := []byte{
|
||||
0x06, // version 6
|
||||
0x02, // packet type 2: tracking
|
||||
0x00, // res1
|
||||
0x00, // res2
|
||||
0x00, 0x21, // command 33: tracking request
|
||||
0x00, 0x05, // reply 5: tracking reply
|
||||
0x00, 0x00, // status 0: success
|
||||
0x00, 0x00, // pad1
|
||||
0x00, 0x00, // pad2
|
||||
0x00, 0x00, // pad3
|
||||
}
|
||||
buf = binary.BigEndian.AppendUint32(buf, sequence) // sequence number
|
||||
buf = append(buf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) // pad 4 & 5
|
||||
|
||||
// Encode data
|
||||
buf = binary.BigEndian.AppendUint32(buf, t.RefID)
|
||||
buf = append(buf, t.IPAddr.To16()...)
|
||||
if len(t.IPAddr) == 4 {
|
||||
buf = append(buf, 0x00, 0x01) // IPv4 address family
|
||||
} else {
|
||||
fmt.Fprint(os.Stdout, noLookup+mockData)
|
||||
buf = append(buf, 0x00, 0x02) // IPv6 address family
|
||||
}
|
||||
buf = append(buf, 0x00, 0x00) // padding
|
||||
buf = binary.BigEndian.AppendUint16(buf, t.Stratum)
|
||||
buf = binary.BigEndian.AppendUint16(buf, t.LeapStatus)
|
||||
sec := uint64(t.RefTime.Unix())
|
||||
nsec := uint32(t.RefTime.UnixNano() % t.RefTime.Unix() * int64(time.Second))
|
||||
buf = binary.BigEndian.AppendUint32(buf, uint32(sec>>32)) // seconds high part
|
||||
buf = binary.BigEndian.AppendUint32(buf, uint32(sec&0xffffffff)) // seconds low part
|
||||
buf = binary.BigEndian.AppendUint32(buf, nsec) // nanoseconds
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.CurrentCorrection))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.LastOffset))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RMSOffset))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.FreqPPM))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.ResidFreqPPM))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.SkewPPM))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RootDelay))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RootDispersion))
|
||||
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.LastUpdateInterval))
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// Modified based on https://github.com/mlichvar/chrony/blob/master/util.c
|
||||
const (
|
||||
floatExpBits = int32(7)
|
||||
floatCoeffBits = int32(25) // 32 - floatExpBits
|
||||
floatExpMin = int32(-(1 << (floatExpBits - 1)))
|
||||
floatExpMax = -floatExpMin - 1
|
||||
floatCoefMin = int32(-(1 << (floatCoeffBits - 1)))
|
||||
floatCoefMax = -floatCoefMin - 1
|
||||
)
|
||||
|
||||
func encodeFloat(x float64) uint32 {
|
||||
var neg int32
|
||||
|
||||
if math.IsNaN(x) {
|
||||
/* Save NaN as zero */
|
||||
x = 0.0
|
||||
} else if x < 0.0 {
|
||||
x = -x
|
||||
neg = 1
|
||||
}
|
||||
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(0)
|
||||
var exp, coef int32
|
||||
if x > 1.0e100 {
|
||||
exp = floatExpMax
|
||||
coef = floatCoefMax + neg
|
||||
} else if x > 1.0e-100 {
|
||||
exp = int32(math.Log2(x)) + 1
|
||||
coef = int32(x*math.Pow(2.0, float64(-exp+floatCoeffBits)) + 0.5)
|
||||
|
||||
if coef <= 0 {
|
||||
panic(fmt.Errorf("invalid coefficient %v for value %f", coef, x))
|
||||
}
|
||||
|
||||
/* we may need to shift up to two bits down */
|
||||
for coef > floatCoefMax+neg {
|
||||
coef >>= 1
|
||||
exp++
|
||||
}
|
||||
|
||||
if exp > floatExpMax {
|
||||
/* overflow */
|
||||
exp = floatExpMax
|
||||
coef = floatCoefMax + neg
|
||||
} else if exp < floatExpMin {
|
||||
/* underflow */
|
||||
if exp+floatCoeffBits >= floatExpMin {
|
||||
coef >>= floatExpMin - exp
|
||||
exp = floatExpMin
|
||||
} else {
|
||||
exp = 0
|
||||
coef = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* negate back */
|
||||
if neg != 0 {
|
||||
coef = int32(uint32(-coef) % (1 << floatCoeffBits))
|
||||
}
|
||||
|
||||
return uint32(exp<<floatCoeffBits) | uint32(coef)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
# Get standard chrony metrics, requires chronyc executable.
|
||||
[[inputs.chrony]]
|
||||
## If true, chronyc tries to perform a DNS lookup for the time server.
|
||||
## Server address of chronyd with address scheme
|
||||
## If empty or not set, the plugin will mimic the behavior of chronyc and
|
||||
## check "unix:///run/chrony/chronyd.sock", "udp://127.0.0.1:323"
|
||||
## and "udp://[::1]:323".
|
||||
# server = ""
|
||||
|
||||
## Timeout for establishing the connection
|
||||
# timeout = "5s"
|
||||
|
||||
## Try to resolve received addresses to host-names via DNS lookups
|
||||
## Disabled by default to avoid DNS queries especially for slow DNS servers.
|
||||
# dns_lookup = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
server 0.pool.ntp.org iburst
|
||||
server 1.pool.ntp.org iburst
|
||||
server 2.pool.ntp.org iburst
|
||||
server 3.pool.ntp.org iburst
|
||||
|
||||
driftfile /var/lib/chrony/chrony.drift
|
||||
makestep 0.1 3
|
||||
|
||||
bindcmdaddress 0.0.0.0
|
||||
cmdallow all
|
||||
allow all
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
# confirm correct permissions on chrony run directory
|
||||
if [ -d /run/chrony ]; then
|
||||
chown -R chrony:chrony /run/chrony
|
||||
chmod o-rx /run/chrony
|
||||
# remove previous pid file if it exist
|
||||
rm -f /var/run/chrony/chronyd.pid
|
||||
fi
|
||||
|
||||
# confirm correct permissions on chrony variable state directory
|
||||
if [ -d /var/lib/chrony ]; then
|
||||
chown -R chrony:chrony /var/lib/chrony
|
||||
fi
|
||||
|
||||
## startup chronyd in the foreground
|
||||
exec /usr/sbin/chronyd -u chrony -d -x -f /etc/telegraf-chrony.conf
|
||||
Loading…
Reference in New Issue