From e8c44275b9a1937b5948010a042294d580d36d7c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 9 Nov 2024 09:36:07 +0100 Subject: Adding upstream version 2.0.0. Signed-off-by: Daniel Baumann --- src/go/plugin/go.d/modules/chrony/charts.go | 143 ++++++------------- src/go/plugin/go.d/modules/chrony/chrony.go | 60 ++++---- src/go/plugin/go.d/modules/chrony/chrony_test.go | 66 ++++----- src/go/plugin/go.d/modules/chrony/client.go | 152 ++++++--------------- src/go/plugin/go.d/modules/chrony/collect.go | 133 +++++++++--------- src/go/plugin/go.d/modules/chrony/exec.go | 46 +++++++ src/go/plugin/go.d/modules/chrony/init.go | 33 +++++ .../go.d/modules/chrony/integrations/chrony.md | 9 +- src/go/plugin/go.d/modules/chrony/metadata.yaml | 23 +++- 9 files changed, 311 insertions(+), 354 deletions(-) create mode 100644 src/go/plugin/go.d/modules/chrony/exec.go (limited to 'src/go/plugin/go.d/modules/chrony') diff --git a/src/go/plugin/go.d/modules/chrony/charts.go b/src/go/plugin/go.d/modules/chrony/charts.go index 37a6fa3e..00f3d053 100644 --- a/src/go/plugin/go.d/modules/chrony/charts.go +++ b/src/go/plugin/go.d/modules/chrony/charts.go @@ -20,10 +20,8 @@ const ( prioRefMeasurementTime prioLeapStatus prioActivity - //prioNTPPackets - //prioCommandPackets - //prioNKEConnections - //prioClientLogRecords + prioNTPPackets + prioCommandPackets ) var charts = module.Charts{ @@ -216,105 +214,42 @@ var ( } ) -//var serverStatsVer1Charts = module.Charts{ -// ntpPacketsChart.Copy(), -// commandPacketsChart.Copy(), -// clientLogRecordsChart.Copy(), -//} -// -//var serverStatsVer2Charts = module.Charts{ -// ntpPacketsChart.Copy(), -// commandPacketsChart.Copy(), -// clientLogRecordsChart.Copy(), -// nkeConnectionChart.Copy(), -//} -// -//var serverStatsVer3Charts = module.Charts{ -// ntpPacketsChart.Copy(), -// commandPacketsChart.Copy(), -// clientLogRecordsChart.Copy(), -// nkeConnectionChart.Copy(), -//} -// -//var serverStatsVer4Charts = module.Charts{ -// ntpPacketsChart.Copy(), -// commandPacketsChart.Copy(), -// clientLogRecordsChart.Copy(), -// nkeConnectionChart.Copy(), -//} +var serverStatsCharts = module.Charts{ + ntpPacketsChart.Copy(), + commandPacketsChart.Copy(), +} -// ServerStats charts -//var ( -// ntpPacketsChart = module.Chart{ -// ID: "ntp_packets", -// Title: "NTP packets", -// Units: "packets/s", -// Fam: "client requests", -// Ctx: "chrony.ntp_packets", -// Type: module.Stacked, -// Priority: prioNTPPackets, -// Dims: module.Dims{ -// {ID: "ntp_packets_received", Name: "received", Algo: module.Incremental}, -// {ID: "ntp_packets_dropped", Name: "dropped", Algo: module.Incremental}, -// }, -// } -// commandPacketsChart = module.Chart{ -// ID: "command_packets", -// Title: "Command packets", -// Units: "packets/s", -// Fam: "client requests", -// Ctx: "chrony.command_packets", -// Type: module.Stacked, -// Priority: prioCommandPackets, -// Dims: module.Dims{ -// {ID: "command_packets_received", Name: "received", Algo: module.Incremental}, -// {ID: "command_packets_dropped", Name: "dropped", Algo: module.Incremental}, -// }, -// } -// nkeConnectionChart = module.Chart{ -// ID: "nke_connections", -// Title: "NTS-KE connections", -// Units: "connections/s", -// Fam: "client requests", -// Ctx: "chrony.nke_connections", -// Type: module.Stacked, -// Priority: prioNKEConnections, -// Dims: module.Dims{ -// {ID: "nke_connections_accepted", Name: "accepted", Algo: module.Incremental}, -// {ID: "nke_connections_dropped", Name: "dropped", Algo: module.Incremental}, -// }, -// } -// clientLogRecordsChart = module.Chart{ -// ID: "client_log_records", -// Title: "Client log records", -// Units: "records/s", -// Fam: "client requests", -// Ctx: "chrony.client_log_records", -// Type: module.Stacked, -// Priority: prioClientLogRecords, -// Dims: module.Dims{ -// {ID: "client_log_records_dropped", Name: "dropped", Algo: module.Incremental}, -// }, -// } -//) +var ( + ntpPacketsChart = module.Chart{ + ID: "ntp_packets", + Title: "NTP packets", + Units: "packets/s", + Fam: "client requests", + Ctx: "chrony.ntp_packets", + Type: module.Line, + Priority: prioNTPPackets, + Dims: module.Dims{ + {ID: "ntp_packets_received", Name: "received", Algo: module.Incremental}, + {ID: "ntp_packets_dropped", Name: "dropped", Algo: module.Incremental}, + }, + } + commandPacketsChart = module.Chart{ + ID: "command_packets", + Title: "Command packets", + Units: "packets/s", + Fam: "client requests", + Ctx: "chrony.command_packets", + Type: module.Line, + Priority: prioCommandPackets, + Dims: module.Dims{ + {ID: "command_packets_received", Name: "received", Algo: module.Incremental}, + {ID: "command_packets_dropped", Name: "dropped", Algo: module.Incremental}, + }, + } +) -//func (c *Chrony) addServerStatsCharts(stats *serverStats) { -// var err error -// -// switch { -// case stats.v1 != nil: -// err = c.Charts().Add(*serverStatsVer1Charts.Copy()...) -// case stats.v2 != nil: -// err = c.Charts().Add(*serverStatsVer2Charts.Copy()...) -// case stats.v3 != nil: -// err = c.Charts().Add(*serverStatsVer3Charts.Copy()...) -// case stats.v4 != nil: -// err = c.Charts().Add(*serverStatsVer4Charts.Copy()...) -// default: -// err = errors.New("unknown stats chart") -// } -// -// if err != nil { -// c.Warning(err) -// } -//} +func (c *Chrony) addServerStatsCharts() { + if err := c.Charts().Add(*serverStatsCharts.Copy()...); err != nil { + c.Warning(err) + } +} diff --git a/src/go/plugin/go.d/modules/chrony/chrony.go b/src/go/plugin/go.d/modules/chrony/chrony.go index 0bdd3183..cfe3067c 100644 --- a/src/go/plugin/go.d/modules/chrony/chrony.go +++ b/src/go/plugin/go.d/modules/chrony/chrony.go @@ -5,13 +5,12 @@ package chrony import ( _ "embed" "errors" + "fmt" "sync" "time" "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" - "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" - - "github.com/facebook/time/ntp/chrony" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt" ) //go:embed "config_schema.json" @@ -29,38 +28,32 @@ func New() *Chrony { return &Chrony{ Config: Config{ Address: "127.0.0.1:323", - Timeout: web.Duration(time.Second), + Timeout: confopt.Duration(time.Second), }, - charts: charts.Copy(), - addStatsChartsOnce: &sync.Once{}, - newClient: newChronyClient, + charts: charts.Copy(), + addServerStatsChartsOnce: &sync.Once{}, + newConn: newChronyConn, } } type Config struct { - UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` - Address string `yaml:"address" json:"address"` - Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Address string `yaml:"address" json:"address"` + Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"` } -type ( - Chrony struct { - module.Base - Config `yaml:",inline" json:""` +type Chrony struct { + module.Base + Config `yaml:",inline" json:""` - charts *module.Charts - addStatsChartsOnce *sync.Once + charts *module.Charts + addServerStatsChartsOnce *sync.Once - client chronyClient - newClient func(c Config) (chronyClient, error) - } - chronyClient interface { - Tracking() (*chrony.ReplyTracking, error) - Activity() (*chrony.ReplyActivity, error) - ServerStats() (*serverStats, error) - Close() - } -) + exec chronyBinary + + conn chronyConn + newConn func(c Config) (chronyConn, error) +} func (c *Chrony) Configuration() any { return c.Config @@ -68,8 +61,12 @@ func (c *Chrony) Configuration() any { func (c *Chrony) Init() error { if err := c.validateConfig(); err != nil { - c.Errorf("config validation: %v", err) - return err + return fmt.Errorf("config validation: %v", err) + } + + var err error + if c.exec, err = c.initChronycBinary(); err != nil { + c.Warningf("chronyc binary init failed: %v (serverstats metrics collection is disabled)", err) } return nil @@ -78,7 +75,6 @@ func (c *Chrony) Init() error { func (c *Chrony) Check() error { mx, err := c.collect() if err != nil { - c.Error(err) return err } if len(mx) == 0 { @@ -105,8 +101,8 @@ func (c *Chrony) Collect() map[string]int64 { } func (c *Chrony) Cleanup() { - if c.client != nil { - c.client.Close() - c.client = nil + if c.conn != nil { + c.conn.close() + c.conn = nil } } diff --git a/src/go/plugin/go.d/modules/chrony/chrony_test.go b/src/go/plugin/go.d/modules/chrony/chrony_test.go index 407724e7..dc380c20 100644 --- a/src/go/plugin/go.d/modules/chrony/chrony_test.go +++ b/src/go/plugin/go.d/modules/chrony/chrony_test.go @@ -155,11 +155,13 @@ func TestChrony_Collect(t *testing.T) { prepare func() *Chrony expected map[string]int64 }{ - "tracking: success, activity: success": { + "tracking: success, activity: success, serverstats: success": { prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{}) }, expected: map[string]int64{ "burst_offline_sources": 3, "burst_online_sources": 4, + "command_packets_dropped": 1, + "command_packets_received": 652, "current_correction": 154872, "frequency": 51051185607, "last_offset": 3095, @@ -167,6 +169,8 @@ func TestChrony_Collect(t *testing.T) { "leap_status_insert_second": 1, "leap_status_normal": 0, "leap_status_unsynchronised": 0, + "ntp_packets_dropped": 1, + "ntp_packets_received": 1, "offline_sources": 2, "online_sources": 8, "ref_measurement_time": 63793323616, @@ -219,12 +223,13 @@ func TestChrony_Collect(t *testing.T) { c := test.prepare() require.NoError(t, c.Init()) + c.exec = &mockChronyc{} _ = c.Check() - collected := c.Collect() - copyRefMeasurementTime(collected, test.expected) + mx := c.Collect() + copyRefMeasurementTime(mx, test.expected) - assert.Equal(t, test.expected, collected) + assert.Equal(t, test.expected, mx) }) } } @@ -232,13 +237,32 @@ func TestChrony_Collect(t *testing.T) { func prepareChronyWithMock(m *mockClient) *Chrony { c := New() if m == nil { - c.newClient = func(_ Config) (chronyClient, error) { return nil, errors.New("mock.newClient error") } + c.newConn = func(_ Config) (chronyConn, error) { return nil, errors.New("mock.newClient error") } } else { - c.newClient = func(_ Config) (chronyClient, error) { return m, nil } + c.newConn = func(_ Config) (chronyConn, error) { return m, nil } } return c } +type mockChronyc struct{} + +func (m *mockChronyc) serverStats() ([]byte, error) { + data := ` +NTP packets received : 1 +NTP packets dropped : 1 +Command packets received : 652 +Command packets dropped : 1 +Client log records dropped : 1 +NTS-KE connections accepted: 1 +NTS-KE connections dropped : 1 +Authenticated NTP packets : 1 +Interleaved NTP packets : 1 +NTP timestamps held : 1 +NTP timestamp span : 0 +` + return []byte(data), nil +} + type mockClient struct { errOnTracking bool errOnActivity bool @@ -246,7 +270,7 @@ type mockClient struct { closeCalled bool } -func (m *mockClient) Tracking() (*chrony.ReplyTracking, error) { +func (m *mockClient) tracking() (*chrony.ReplyTracking, error) { if m.errOnTracking { return nil, errors.New("mockClient.Tracking call error") } @@ -271,7 +295,7 @@ func (m *mockClient) Tracking() (*chrony.ReplyTracking, error) { return &reply, nil } -func (m *mockClient) Activity() (*chrony.ReplyActivity, error) { +func (m *mockClient) activity() (*chrony.ReplyActivity, error) { if m.errOnActivity { return nil, errors.New("mockClient.Activity call error") } @@ -287,31 +311,7 @@ func (m *mockClient) Activity() (*chrony.ReplyActivity, error) { return &reply, nil } -func (m *mockClient) ServerStats() (*serverStats, error) { - if m.errOnServerStats { - return nil, errors.New("mockClient.ServerStats call error") - } - - reply := serverStats{ - v3: &chrony.ServerStats3{ - NTPHits: 10, - NKEHits: 10, - CMDHits: 10, - NTPDrops: 1, - NKEDrops: 1, - CMDDrops: 1, - LogDrops: 1, - NTPAuthHits: 10, - NTPInterleavedHits: 10, - NTPTimestamps: 0, - NTPSpanSeconds: 0, - }, - } - - return &reply, nil -} - -func (m *mockClient) Close() { +func (m *mockClient) close() { m.closeCalled = true } diff --git a/src/go/plugin/go.d/modules/chrony/client.go b/src/go/plugin/go.d/modules/chrony/client.go index 233e78f1..f07f4902 100644 --- a/src/go/plugin/go.d/modules/chrony/client.go +++ b/src/go/plugin/go.d/modules/chrony/client.go @@ -10,55 +10,40 @@ import ( "github.com/facebook/time/ntp/chrony" ) -func newChronyClient(c Config) (chronyClient, error) { - conn, err := net.DialTimeout("udp", c.Address, c.Timeout.Duration()) +type chronyConn interface { + tracking() (*chrony.ReplyTracking, error) + activity() (*chrony.ReplyActivity, error) + close() +} + +func newChronyConn(cfg Config) (chronyConn, error) { + conn, err := net.DialTimeout("udp", cfg.Address, cfg.Timeout.Duration()) if err != nil { return nil, err } - client := &simpleClient{ + client := &chronyClient{ conn: conn, - client: &chrony.Client{Connection: &connWithTimeout{ - Conn: conn, - timeout: c.Timeout.Duration(), - }}, + client: &chrony.Client{ + Connection: &connWithTimeout{ + Conn: conn, + timeout: cfg.Timeout.Duration(), + }, + }, } return client, nil } -type connWithTimeout struct { - net.Conn - timeout time.Duration -} - -func (c *connWithTimeout) Read(p []byte) (n int, err error) { - if err := c.Conn.SetReadDeadline(c.deadline()); err != nil { - return 0, err - } - return c.Conn.Read(p) -} - -func (c *connWithTimeout) Write(p []byte) (n int, err error) { - if err := c.Conn.SetWriteDeadline(c.deadline()); err != nil { - return 0, err - } - return c.Conn.Write(p) -} - -func (c *connWithTimeout) deadline() time.Time { - return time.Now().Add(c.timeout) -} - -type simpleClient struct { +type chronyClient struct { conn net.Conn client *chrony.Client } -func (sc *simpleClient) Tracking() (*chrony.ReplyTracking, error) { +func (c *chronyClient) tracking() (*chrony.ReplyTracking, error) { req := chrony.NewTrackingPacket() - reply, err := sc.client.Communicate(req) + reply, err := c.client.Communicate(req) if err != nil { return nil, err } @@ -67,13 +52,14 @@ func (sc *simpleClient) Tracking() (*chrony.ReplyTracking, error) { if !ok { return nil, fmt.Errorf("unexpected reply type, want=%T, got=%T", &chrony.ReplyTracking{}, reply) } + return tracking, nil } -func (sc *simpleClient) Activity() (*chrony.ReplyActivity, error) { +func (c *chronyClient) activity() (*chrony.ReplyActivity, error) { req := chrony.NewActivityPacket() - reply, err := sc.client.Communicate(req) + reply, err := c.client.Communicate(req) if err != nil { return nil, err } @@ -82,90 +68,36 @@ func (sc *simpleClient) Activity() (*chrony.ReplyActivity, error) { if !ok { return nil, fmt.Errorf("unexpected reply type, want=%T, got=%T", &chrony.ReplyActivity{}, reply) } + return activity, nil } -type serverStats struct { - v1 *chrony.ServerStats - v2 *chrony.ServerStats2 - v3 *chrony.ServerStats3 - v4 *chrony.ServerStats4 +func (c *chronyClient) close() { + if c.conn != nil { + _ = c.conn.Close() + c.conn = nil + } } -func (sc *simpleClient) ServerStats() (*serverStats, error) { - req := chrony.NewServerStatsPacket() +type connWithTimeout struct { + net.Conn + timeout time.Duration +} - reply, err := sc.client.Communicate(req) - if err != nil { - return nil, err +func (c *connWithTimeout) Read(p []byte) (n int, err error) { + if err := c.Conn.SetReadDeadline(c.deadline()); err != nil { + return 0, err } + return c.Conn.Read(p) +} - var stats serverStats - - switch v := reply.(type) { - case *chrony.ReplyServerStats: - stats.v1 = &chrony.ServerStats{ - NTPHits: v.NTPHits, - CMDHits: v.CMDHits, - NTPDrops: v.NTPDrops, - CMDDrops: v.CMDDrops, - LogDrops: v.LogDrops, - } - case *chrony.ReplyServerStats2: - stats.v2 = &chrony.ServerStats2{ - NTPHits: v.NTPHits, - NKEHits: v.NKEHits, - CMDHits: v.CMDHits, - NTPDrops: v.NTPDrops, - NKEDrops: v.NKEDrops, - CMDDrops: v.CMDDrops, - LogDrops: v.LogDrops, - NTPAuthHits: v.NTPAuthHits, - } - case *chrony.ReplyServerStats3: - stats.v3 = &chrony.ServerStats3{ - NTPHits: v.NTPHits, - NKEHits: v.NKEHits, - CMDHits: v.CMDHits, - NTPDrops: v.NTPDrops, - NKEDrops: v.NKEDrops, - CMDDrops: v.CMDDrops, - LogDrops: v.LogDrops, - NTPAuthHits: v.NTPAuthHits, - NTPInterleavedHits: v.NTPInterleavedHits, - NTPTimestamps: v.NTPTimestamps, - NTPSpanSeconds: v.NTPSpanSeconds, - } - case *chrony.ReplyServerStats4: - stats.v4 = &chrony.ServerStats4{ - NTPHits: v.NTPHits, - NKEHits: v.NKEHits, - CMDHits: v.CMDHits, - NTPDrops: v.NTPDrops, - NKEDrops: v.NKEDrops, - CMDDrops: v.CMDDrops, - LogDrops: v.LogDrops, - NTPAuthHits: v.NTPAuthHits, - NTPInterleavedHits: v.NTPInterleavedHits, - NTPTimestamps: v.NTPTimestamps, - NTPSpanSeconds: v.NTPSpanSeconds, - NTPDaemonRxtimestamps: v.NTPDaemonRxtimestamps, - NTPDaemonTxtimestamps: v.NTPDaemonTxtimestamps, - NTPKernelRxtimestamps: v.NTPKernelRxtimestamps, - NTPKernelTxtimestamps: v.NTPKernelTxtimestamps, - NTPHwRxTimestamps: v.NTPHwRxTimestamps, - NTPHwTxTimestamps: v.NTPHwTxTimestamps, - } - default: - return nil, fmt.Errorf("unexpected reply type, want=ReplyServerStats, got=%T", reply) +func (c *connWithTimeout) Write(p []byte) (n int, err error) { + if err := c.Conn.SetWriteDeadline(c.deadline()); err != nil { + return 0, err } - - return &stats, nil + return c.Conn.Write(p) } -func (sc *simpleClient) Close() { - if sc.conn != nil { - _ = sc.conn.Close() - sc.conn = nil - } +func (c *connWithTimeout) deadline() time.Time { + return time.Now().Add(c.timeout) } diff --git a/src/go/plugin/go.d/modules/chrony/collect.go b/src/go/plugin/go.d/modules/chrony/collect.go index 1a3a286f..c95b1b8a 100644 --- a/src/go/plugin/go.d/modules/chrony/collect.go +++ b/src/go/plugin/go.d/modules/chrony/collect.go @@ -3,19 +3,32 @@ package chrony import ( + "bufio" + "bytes" + "errors" "fmt" + "strconv" + "strings" "time" ) const scaleFactor = 1000000000 +const ( + // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/ntp.h#L70-L75 + leapStatusNormal = 0 + leapStatusInsertSecond = 1 + leapStatusDeleteSecond = 2 + leapStatusUnsynchronised = 3 +) + func (c *Chrony) collect() (map[string]int64, error) { - if c.client == nil { - client, err := c.newClient(c.Config) + if c.conn == nil { + client, err := c.newConn(c.Config) if err != nil { return nil, err } - c.client = client + c.conn = client } mx := make(map[string]int64) @@ -26,28 +39,20 @@ func (c *Chrony) collect() (map[string]int64, error) { if err := c.collectActivity(mx); err != nil { return mx, err } - //if strings.HasPrefix(c.Address, "/") { - // TODO: Allowed only through the Unix domain socket (requires "_chrony" group membership). - // See https://github.com/facebook/time/blob/18207c5d8ddc7242e8d4192985898b6dbe66932c/cmd/ntpcheck/checker/chrony.go#L38 - // ^^ For some reason doesn't work, Chrony doesn't respond. Additional configuration needed? - //if err := c.collectServerStats(mx); err != nil { - // return mx, err - //} - //} + if c.exec != nil { + if err := c.collectServerStats(mx); err != nil { + c.Warning(err) + c.exec = nil + } else { + c.addServerStatsChartsOnce.Do(c.addServerStatsCharts) + } + } return mx, nil } -const ( - // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/ntp.h#L70-L75 - leapStatusNormal = 0 - leapStatusInsertSecond = 1 - leapStatusDeleteSecond = 2 - leapStatusUnsynchronised = 3 -) - func (c *Chrony) collectTracking(mx map[string]int64) error { - reply, err := c.client.Tracking() + reply, err := c.conn.tracking() if err != nil { return fmt.Errorf("error on collecting tracking: %v", err) } @@ -76,7 +81,7 @@ func (c *Chrony) collectTracking(mx map[string]int64) error { } func (c *Chrony) collectActivity(mx map[string]int64) error { - reply, err := c.client.Activity() + reply, err := c.conn.activity() if err != nil { return fmt.Errorf("error on collecting activity: %v", err) } @@ -90,56 +95,42 @@ func (c *Chrony) collectActivity(mx map[string]int64) error { return nil } -//func (c *Chrony) collectServerStats(mx map[string]int64) error { -// stats, err := c.client.ServerStats() -// if err != nil { -// return fmt.Errorf("error on collecting server stats: %v", err) -// } -// -// switch { -// case stats.v4 != nil: -// mx["ntp_packets_received"] = int64(stats.v4.NTPHits) -// mx["ntp_packets_dropped"] = int64(stats.v4.NTPDrops) -// mx["command_packets_received"] = int64(stats.v4.CMDHits) -// mx["command_packets_dropped"] = int64(stats.v4.CMDDrops) -// mx["client_log_records_dropped"] = int64(stats.v4.LogDrops) -// mx["nke_connections_accepted"] = int64(stats.v4.NKEHits) -// mx["nke_connections_dropped"] = int64(stats.v4.NKEDrops) -// mx["authenticated_ntp_packets"] = int64(stats.v4.NTPAuthHits) -// mx["interleaved_ntp_packets"] = int64(stats.v4.NTPInterleavedHits) -// case stats.v3 != nil: -// mx["ntp_packets_received"] = int64(stats.v3.NTPHits) -// mx["ntp_packets_dropped"] = int64(stats.v3.NTPDrops) -// mx["command_packets_received"] = int64(stats.v3.CMDHits) -// mx["command_packets_dropped"] = int64(stats.v3.CMDDrops) -// mx["client_log_records_dropped"] = int64(stats.v3.LogDrops) -// mx["nke_connections_accepted"] = int64(stats.v3.NKEHits) -// mx["nke_connections_dropped"] = int64(stats.v3.NKEDrops) -// mx["authenticated_ntp_packets"] = int64(stats.v3.NTPAuthHits) -// mx["interleaved_ntp_packets"] = int64(stats.v3.NTPInterleavedHits) -// case stats.v2 != nil: -// mx["ntp_packets_received"] = int64(stats.v2.NTPHits) -// mx["ntp_packets_dropped"] = int64(stats.v2.NTPDrops) -// mx["command_packets_received"] = int64(stats.v2.CMDHits) -// mx["command_packets_dropped"] = int64(stats.v2.CMDDrops) -// mx["client_log_records_dropped"] = int64(stats.v2.LogDrops) -// mx["nke_connections_accepted"] = int64(stats.v2.NKEHits) -// mx["nke_connections_dropped"] = int64(stats.v2.NKEDrops) -// mx["authenticated_ntp_packets"] = int64(stats.v2.NTPAuthHits) -// case stats.v1 != nil: -// mx["ntp_packets_received"] = int64(stats.v1.NTPHits) -// mx["ntp_packets_dropped"] = int64(stats.v1.NTPDrops) -// mx["command_packets_received"] = int64(stats.v1.CMDHits) -// mx["command_packets_dropped"] = int64(stats.v1.CMDDrops) -// mx["client_log_records_dropped"] = int64(stats.v1.LogDrops) -// default: -// return errors.New("invalid server stats reply") -// } -// -// //c.addStatsChartsOnce.Do(func() { c.addServerStatsCharts(stats) }) -// -// return nil -//} +func (c *Chrony) collectServerStats(mx map[string]int64) error { + bs, err := c.exec.serverStats() + if err != nil { + return fmt.Errorf("error on collecting server stats: %v", err) + } + + sc := bufio.NewScanner(bytes.NewReader(bs)) + var n int + + for sc.Scan() { + key, value, ok := strings.Cut(sc.Text(), ":") + if !ok { + continue + } + + key, value = strings.TrimSpace(key), strings.TrimSpace(value) + + switch key { + case "NTP packets received", + "NTP packets dropped", + "Command packets received", + "Command packets dropped": + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + key = strings.ToLower(strings.ReplaceAll(key, " ", "_")) + mx[key] = v + n++ + } + } + } + + if n == 0 { + return errors.New("no server stats metrics found in the response") + } + + return nil +} func boolToInt(v bool) int64 { if v { diff --git a/src/go/plugin/go.d/modules/chrony/exec.go b/src/go/plugin/go.d/modules/chrony/exec.go new file mode 100644 index 00000000..c6792d84 --- /dev/null +++ b/src/go/plugin/go.d/modules/chrony/exec.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package chrony + +import ( + "context" + "fmt" + "os/exec" + "time" + + "github.com/netdata/netdata/go/plugins/logger" +) + +type chronyBinary interface { + serverStats() ([]byte, error) +} + +func newChronycExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *chronycExec { + return &chronycExec{ + Logger: log, + ndsudoPath: ndsudoPath, + timeout: timeout, + } +} + +type chronycExec struct { + *logger.Logger + + ndsudoPath string + timeout time.Duration +} + +func (e *chronycExec) serverStats() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), e.timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, e.ndsudoPath, "chronyc-serverstats") + e.Debugf("executing '%s'", cmd) + + bs, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error on '%s': %v", cmd, err) + } + + return bs, nil +} diff --git a/src/go/plugin/go.d/modules/chrony/init.go b/src/go/plugin/go.d/modules/chrony/init.go index 828112c9..2ad63ec6 100644 --- a/src/go/plugin/go.d/modules/chrony/init.go +++ b/src/go/plugin/go.d/modules/chrony/init.go @@ -4,6 +4,12 @@ package chrony import ( "errors" + "fmt" + "net" + "os" + "path/filepath" + + "github.com/netdata/netdata/go/plugins/pkg/executable" ) func (c *Chrony) validateConfig() error { @@ -12,3 +18,30 @@ func (c *Chrony) validateConfig() error { } return nil } + +func (c *Chrony) initChronycBinary() (chronyBinary, error) { + host, _, err := net.SplitHostPort(c.Address) + if err != nil { + return nil, err + } + + // 'serverstats' allowed only through the Unix domain socket + if !isLocalhost(host) { + return nil, nil + } + + ndsudoPath := filepath.Join(executable.Directory, "ndsudo") + + if _, err := os.Stat(ndsudoPath); err != nil { + return nil, fmt.Errorf("ndsudo executable not found: %v", err) + } + + chronyc := newChronycExec(ndsudoPath, c.Timeout.Duration(), c.Logger) + + return chronyc, nil +} + +func isLocalhost(host string) bool { + ip := net.ParseIP(host) + return host == "localhost" || (ip != nil && ip.IsLoopback()) +} diff --git a/src/go/plugin/go.d/modules/chrony/integrations/chrony.md b/src/go/plugin/go.d/modules/chrony/integrations/chrony.md index e9b9454d..6ef6cf18 100644 --- a/src/go/plugin/go.d/modules/chrony/integrations/chrony.md +++ b/src/go/plugin/go.d/modules/chrony/integrations/chrony.md @@ -23,7 +23,10 @@ Module: chrony This collector monitors the system's clock performance and peers activity status + It collects metrics by sending UDP packets to chronyd using the Chrony communication protocol v6. +Additionally, for data collection jobs that connect to localhost Chrony instances, it collects serverstats metrics (NTP packets, command packets received/dropped) by executing the 'chronyc serverstats' command. + This collector is supported on all platforms. @@ -80,6 +83,8 @@ Metrics: | chrony.ref_measurement_time | ref_measurement_time | seconds | | chrony.leap_status | normal, insert_second, delete_second, unsynchronised | status | | chrony.activity | online, offline, burst_online, burst_offline, unresolved | sources | +| chrony.ntp_packets | received, dropped | packets/s | +| chrony.command_packets | received, dropped | packets/s | @@ -101,8 +106,8 @@ No action required. The configuration file name for this integration is `go.d/chrony.conf`. -You can edit the configuration file using the `edit-config` script from the -Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). +You can edit the configuration file using the [`edit-config`](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration/README.md#edit-a-configuration-file-using-edit-config) script from the +Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). ```bash cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata diff --git a/src/go/plugin/go.d/modules/chrony/metadata.yaml b/src/go/plugin/go.d/modules/chrony/metadata.yaml index 18f9152e..b7842aff 100644 --- a/src/go/plugin/go.d/modules/chrony/metadata.yaml +++ b/src/go/plugin/go.d/modules/chrony/metadata.yaml @@ -20,8 +20,11 @@ modules: most_popular: false overview: data_collection: - metrics_description: This collector monitors the system's clock performance and peers activity status - method_description: It collects metrics by sending UDP packets to chronyd using the Chrony communication protocol v6. + metrics_description: | + This collector monitors the system's clock performance and peers activity status + method_description: | + It collects metrics by sending UDP packets to chronyd using the Chrony communication protocol v6. + Additionally, for data collection jobs that connect to localhost Chrony instances, it collects serverstats metrics (NTP packets, command packets received/dropped) by executing the 'chronyc serverstats' command. supported_platforms: include: [] exclude: [] @@ -206,3 +209,19 @@ modules: - name: burst_online - name: burst_offline - name: unresolved + - name: chrony.ntp_packets + availability: [] + description: NTP packets + unit: packets/s + chart_type: line + dimensions: + - name: received + - name: dropped + - name: chrony.command_packets + availability: [] + description: Command packets + unit: packets/s + chart_type: line + dimensions: + - name: received + - name: dropped -- cgit v1.2.3