1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,89 @@
# chrony Input Plugin
This plugin queries metrics from a [chrony NTP server][chrony]. For details on
the meaning of the gathered fields please check the [chronyc manual][manual].
⭐ Telegraf v0.13.1
🏷️ system
💻 all
[chrony]: https://chrony-project.org
[manual]: https://chrony-project.org/doc/4.4/chronyc.html
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Get standard chrony metrics.
[[inputs.chrony]]
## Server address of chronyd with address scheme
## If empty or not set, the plugin will mimic the behavior of chronyc and
## check "unixgram:///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
## Metrics to query named according to chronyc commands
## Available settings are:
## activity -- number of peers online or offline
## tracking -- information about system's clock performance
## serverstats -- chronyd server statistics
## sources -- extended information about peers
## sourcestats -- statistics on peers
# metrics = ["tracking"]
## Socket group & permissions
## If the user requests collecting metrics via unix socket, then it is created
## with the following group and permissions.
# socket_group = "chrony"
# socket_perms = "0660"
```
## Local socket permissions
To use the unix socket, telegraf must be able to talk to it. Please ensure that
the telegraf user is a member of the `chrony` group or telegraf won't be able to
use the socket!
The unix socket is needed in order to use the `serverstats` metrics. All other
metrics can be gathered using the udp connection.
## Metrics
- chrony
- system_time (float, seconds)
- last_offset (float, seconds)
- rms_offset (float, seconds)
- frequency (float, ppm)
- residual_freq (float, ppm)
- skew (float, ppm)
- root_delay (float, seconds)
- root_dispersion (float, seconds)
- update_interval (float, seconds)
### Tags
- All measurements have the following tags:
- reference_id
- stratum
- leap_status
## Example Output
```text
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
```

View file

@ -0,0 +1,531 @@
//go:generate ../../../tools/readme_config_includer/generator
package chrony
import (
"bytes"
_ "embed"
"encoding/binary"
"errors"
"fmt"
"net"
"net/url"
"os"
"os/user"
"path"
"strconv"
"syscall"
"time"
fbchrony "github.com/facebook/time/ntp/chrony"
"github.com/google/uuid"
"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"`
SocketGroup string `toml:"socket_group"`
SocketPerms string `toml:"socket_perms"`
Metrics []string `toml:"metrics"`
Log telegraf.Logger `toml:"-"`
conn net.Conn
client *fbchrony.Client
source string
local string
}
func (*Chrony) SampleConfig() string {
return sampleConfig
}
func (c *Chrony) Init() error {
// Use the configured server, if none set, we try to guess it in Start()
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 "unixgram":
// 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()
}
// Check the given metrics
if len(c.Metrics) == 0 {
c.Metrics = append(c.Metrics, "tracking")
}
for _, m := range c.Metrics {
switch m {
case "activity", "tracking", "serverstats", "sources", "sourcestats":
// Do nothing as those are valid
default:
return fmt.Errorf("invalid metric setting %q", m)
}
}
if c.SocketGroup == "" {
c.SocketGroup = "chrony"
}
if c.SocketPerms == "" {
c.SocketPerms = "0660"
}
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 "unixgram":
conn, err := c.dialUnix(u.Path)
if err != nil {
return fmt.Errorf("dialing %q failed: %w", c.Server, err)
}
c.conn = conn
c.source = u.Path
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
c.source = u.Host
}
} else {
// If no server is given, reproduce chronyc's behavior
if conn, err := c.dialUnix("/run/chrony/chronyd.sock"); 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) Gather(acc telegraf.Accumulator) error {
for _, m := range c.Metrics {
switch m {
case "activity":
acc.AddError(c.gatherActivity(acc))
case "tracking":
acc.AddError(c.gatherTracking(acc))
case "serverstats":
acc.AddError(c.gatherServerStats(acc))
case "sources":
acc.AddError(c.gatherSources(acc))
case "sourcestats":
acc.AddError(c.gatherSourceStats(acc))
default:
return fmt.Errorf("invalid metric setting %q", m)
}
}
return nil
}
func (c *Chrony) Stop() {
if c.conn != nil {
if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, syscall.EPIPE) {
c.Log.Errorf("Closing connection to %q failed: %v", c.Server, err)
}
}
if c.local != "" {
if err := os.Remove(c.local); err != nil {
c.Log.Errorf("Removing temporary socket %q failed: %v", c.local, err)
}
}
}
// dialUnix opens an unixgram connection with chrony
func (c *Chrony) dialUnix(address string) (*net.UnixConn, error) {
dir := path.Dir(address)
c.local = path.Join(dir, fmt.Sprintf("chrony-telegraf-%s.sock", uuid.New().String()))
conn, err := net.DialUnix("unixgram",
&net.UnixAddr{Name: c.local, Net: "unixgram"},
&net.UnixAddr{Name: address, Net: "unixgram"},
)
if err != nil {
return nil, err
}
filemode, err := strconv.ParseUint(c.SocketPerms, 8, 32)
if err != nil {
return nil, fmt.Errorf("parsing file mode %q failed: %w", c.SocketPerms, err)
}
if err := os.Chmod(c.local, os.FileMode(filemode)); err != nil {
return nil, fmt.Errorf("changing file mode of %q failed: %w", c.local, err)
}
group, err := user.LookupGroup(c.SocketGroup)
if err != nil {
return nil, fmt.Errorf("looking up group %q failed: %w", c.SocketGroup, err)
}
gid, err := strconv.Atoi(group.Gid)
if err != nil {
return nil, fmt.Errorf("parsing group ID %q failed: %w", group.Gid, err)
}
if err := os.Chown(c.local, os.Getuid(), gid); err != nil {
return nil, fmt.Errorf("changing group of %q failed: %w", c.local, err)
}
return conn, nil
}
func (c *Chrony) gatherActivity(acc telegraf.Accumulator) error {
req := fbchrony.NewActivityPacket()
r, err := c.client.Communicate(req)
if err != nil {
return fmt.Errorf("querying activity data failed: %w", err)
}
resp, ok := r.(*fbchrony.ReplyActivity)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for activity data", r)
}
tags := make(map[string]string, 1)
if c.source != "" {
tags["source"] = c.source
}
fields := map[string]interface{}{
"online": resp.Online,
"offline": resp.Offline,
"burst_online": resp.BurstOnline,
"burst_offline": resp.BurstOffline,
"unresolved": resp.Unresolved,
}
acc.AddFields("chrony_activity", fields, tags)
return nil
}
func (c *Chrony) gatherTracking(acc telegraf.Accumulator) error {
req := fbchrony.NewTrackingPacket()
r, err := c.client.Communicate(req)
if err != nil {
return fmt.Errorf("querying tracking data failed: %w", err)
}
resp, ok := r.(*fbchrony.ReplyTracking)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for tracking data", r)
}
// according to https://github.com/mlichvar/chrony/blob/e11b518a1ffa704986fb1f1835c425844ba248ef/ntp.h#L70
var leapStatus string
switch resp.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": fbchrony.RefidAsHEX(resp.RefID),
"stratum": strconv.FormatUint(uint64(resp.Stratum), 10),
}
if c.source != "" {
tags["source"] = c.source
}
fields := map[string]interface{}{
"frequency": resp.FreqPPM,
"system_time": resp.CurrentCorrection,
"last_offset": resp.LastOffset,
"residual_freq": resp.ResidFreqPPM,
"rms_offset": resp.RMSOffset,
"root_delay": resp.RootDelay,
"root_dispersion": resp.RootDispersion,
"skew": resp.SkewPPM,
"update_interval": resp.LastUpdateInterval,
}
acc.AddFields("chrony", fields, tags)
return nil
}
func (c *Chrony) gatherServerStats(acc telegraf.Accumulator) error {
req := fbchrony.NewServerStatsPacket()
r, err := c.client.Communicate(req)
if err != nil {
return fmt.Errorf("querying server statistics failed: %w", err)
}
tags := make(map[string]string, 1)
if c.source != "" {
tags["source"] = c.source
}
var fields map[string]interface{}
switch resp := r.(type) {
case *fbchrony.ReplyServerStats:
fields = map[string]interface{}{
"ntp_hits": resp.NTPHits,
"ntp_drops": resp.NTPDrops,
"cmd_hits": resp.CMDHits,
"cmd_drops": resp.CMDDrops,
"log_drops": resp.LogDrops,
}
case *fbchrony.ReplyServerStats2:
fields = map[string]interface{}{
"ntp_hits": resp.NTPHits,
"ntp_drops": resp.NTPDrops,
"ntp_auth_hits": resp.NTPAuthHits,
"cmd_hits": resp.CMDHits,
"cmd_drops": resp.CMDDrops,
"log_drops": resp.LogDrops,
"nke_hits": resp.NKEHits,
"nke_drops": resp.NKEDrops,
}
case *fbchrony.ReplyServerStats3:
fields = map[string]interface{}{
"ntp_hits": resp.NTPHits,
"ntp_drops": resp.NTPDrops,
"ntp_auth_hits": resp.NTPAuthHits,
"ntp_interleaved_hits": resp.NTPInterleavedHits,
"ntp_timestamps": resp.NTPTimestamps,
"ntp_span_seconds": resp.NTPSpanSeconds,
"cmd_hits": resp.CMDHits,
"cmd_drops": resp.CMDDrops,
"log_drops": resp.LogDrops,
"nke_hits": resp.NKEHits,
"nke_drops": resp.NKEDrops,
}
case *fbchrony.ReplyServerStats4:
fields = map[string]interface{}{
"ntp_hits": resp.NTPHits,
"ntp_drops": resp.NTPDrops,
"ntp_auth_hits": resp.NTPAuthHits,
"ntp_interleaved_hits": resp.NTPInterleavedHits,
"ntp_timestamps": resp.NTPTimestamps,
"ntp_span_seconds": resp.NTPSpanSeconds,
"cmd_hits": resp.CMDHits,
"cmd_drops": resp.CMDDrops,
"log_drops": resp.LogDrops,
"nke_hits": resp.NKEHits,
"nke_drops": resp.NKEDrops,
"ntp_daemon_rx_timestamps": resp.NTPDaemonRxtimestamps,
"ntp_daemon_tx_timestamps": resp.NTPDaemonTxtimestamps,
"ntp_kernel_rx_timestamps": resp.NTPKernelRxtimestamps,
"ntp_kernel_tx_timestamps": resp.NTPKernelTxtimestamps,
"ntp_hardware_rx_timestamps": resp.NTPHwRxTimestamps,
"ntp_hardware_tx_timestamps": resp.NTPHwTxTimestamps,
}
default:
return fmt.Errorf("got unexpected response type %T while waiting for server statistics", r)
}
acc.AddFields("chrony_serverstats", fields, tags)
return nil
}
func (c *Chrony) getSourceName(ip net.IP) (string, error) {
sourceNameReq := fbchrony.NewNTPSourceNamePacket(ip)
sourceNameRaw, err := c.client.Communicate(sourceNameReq)
if err != nil {
return "", fmt.Errorf("querying name of source %q failed: %w", ip, err)
}
sourceName, ok := sourceNameRaw.(*fbchrony.ReplyNTPSourceName)
if !ok {
return "", fmt.Errorf("got unexpected response type %T while waiting for source name", sourceNameRaw)
}
// Cut the string at null termination
if termidx := bytes.Index(sourceName.Name[:], []byte{0}); termidx >= 0 {
return string(sourceName.Name[:termidx]), nil
}
return string(sourceName.Name[:]), nil
}
func (c *Chrony) gatherSources(acc telegraf.Accumulator) error {
sourcesReq := fbchrony.NewSourcesPacket()
sourcesRaw, err := c.client.Communicate(sourcesReq)
if err != nil {
return fmt.Errorf("querying sources failed: %w", err)
}
sourcesResp, ok := sourcesRaw.(*fbchrony.ReplySources)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for sources", sourcesRaw)
}
for idx := int32(0); int(idx) < sourcesResp.NSources; idx++ {
// Getting the source data
sourceDataReq := fbchrony.NewSourceDataPacket(idx)
sourceDataRaw, err := c.client.Communicate(sourceDataReq)
if err != nil {
return fmt.Errorf("querying data for source %d failed: %w", idx, err)
}
sourceData, ok := sourceDataRaw.(*fbchrony.ReplySourceData)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for source data", sourceDataRaw)
}
// Trying to resolve the source name
var peer string
if sourceData.Mode == fbchrony.SourceModeRef && sourceData.IPAddr.To4() != nil {
// References of local sources (PPS, etc..) are encoded in the bits of the IPv4 address,
// instead of the RefId. Extract the correct name for those sources.
ipU32 := binary.BigEndian.Uint32(sourceData.IPAddr.To4())
peer = fbchrony.RefidToString(ipU32)
} else {
peer, err = c.getSourceName(sourceData.IPAddr)
if err != nil {
return err
}
}
if peer == "" {
peer = sourceData.IPAddr.String()
}
tags := map[string]string{
"peer": peer,
}
if c.source != "" {
tags["source"] = c.source
}
fields := map[string]interface{}{
"index": idx,
"ip": sourceData.IPAddr.String(),
"poll": sourceData.Poll,
"stratum": sourceData.Stratum,
"state": sourceData.State.String(),
"mode": sourceData.Mode.String(),
"flags": sourceData.Flags,
"reachability": sourceData.Reachability,
"sample": sourceData.SinceSample,
"latest_measurement": sourceData.LatestMeas,
"latest_measurement_error": sourceData.LatestMeasErr,
}
acc.AddFields("chrony_sources", fields, tags)
}
return nil
}
func (c *Chrony) gatherSourceStats(acc telegraf.Accumulator) error {
sourcesReq := fbchrony.NewSourcesPacket()
sourcesRaw, err := c.client.Communicate(sourcesReq)
if err != nil {
return fmt.Errorf("querying sources failed: %w", err)
}
sourcesResp, ok := sourcesRaw.(*fbchrony.ReplySources)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for sources", sourcesRaw)
}
for idx := int32(0); int(idx) < sourcesResp.NSources; idx++ {
// Getting the source data
sourceStatsReq := fbchrony.NewSourceStatsPacket(idx)
sourceStatsRaw, err := c.client.Communicate(sourceStatsReq)
if err != nil {
return fmt.Errorf("querying data for source %d failed: %w", idx, err)
}
sourceStats, ok := sourceStatsRaw.(*fbchrony.ReplySourceStats)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for source data", sourceStatsRaw)
}
// Trying to resolve the source name
var peer string
if sourceStats.IPAddr.String() == "::" {
// `::` IPs mean a local source directly connected to the server.
// For example a PPS source or a local GPS receiver.
// Then the name of the source is encoded in its RefID
peer = fbchrony.RefidToString(sourceStats.RefID)
} else {
peer, err = c.getSourceName(sourceStats.IPAddr)
if err != nil {
return err
}
}
if peer == "" {
peer = sourceStats.IPAddr.String()
}
tags := map[string]string{
"reference_id": fbchrony.RefidAsHEX(sourceStats.RefID),
"peer": peer,
}
if c.source != "" {
tags["source"] = c.source
}
fields := map[string]interface{}{
"index": idx,
"ip": sourceStats.IPAddr.String(),
"samples": sourceStats.NSamples,
"runs": sourceStats.NRuns,
"span_seconds": sourceStats.SpanSeconds,
"stddev": sourceStats.StandardDeviation,
"residual_frequency": sourceStats.ResidFreqPPM,
"skew": sourceStats.SkewPPM,
"offset": sourceStats.EstimatedOffset,
"offset_error": sourceStats.EstimatedOffsetErr,
}
acc.AddFields("chrony_sourcestats", fields, tags)
}
return nil
}
func init() {
inputs.Add("chrony", func() telegraf.Input {
return &Chrony{Timeout: config.Duration(3 * time.Second)}
})
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
# Get standard chrony metrics.
[[inputs.chrony]]
## Server address of chronyd with address scheme
## If empty or not set, the plugin will mimic the behavior of chronyc and
## check "unixgram:///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
## Metrics to query named according to chronyc commands
## Available settings are:
## activity -- number of peers online or offline
## tracking -- information about system's clock performance
## serverstats -- chronyd server statistics
## sources -- extended information about peers
## sourcestats -- statistics on peers
# metrics = ["tracking"]
## Socket group & permissions
## If the user requests collecting metrics via unix socket, then it is created
## with the following group and permissions.
# socket_group = "chrony"
# socket_perms = "0660"

View file

@ -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

17
plugins/inputs/chrony/testdata/start.sh vendored Normal file
View file

@ -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