@cryptotaxi247 / netdata-1 / commits / 1eedec034

go.d/chrony: collect serverstats using chronyc (#18685)

Ilya Mashchenko committed Oct 4, 2024 at 16:03 UTC 1eedec034ef41315d448693029c9cde2ee995240
11 files changed +309 -347
src/collectors/utils/ndsudo.c
+9
@@ -13,6 +13,15 @@ struct command {
13 const char *params;
14 const char *search[MAX_SEARCH];
15 } allowed_commands[] = {
16 + {
17 + .name = "chronyc-serverstats",
18 + .params = "serverstats",
19 + .search =
20 + {
21 + [0] = "chronyc",
22 + [1] = NULL,
23 + },
24 + },
25 {
26 .name = "varnishadm-backend-list",
27 .params = "backend.list",
src/go/go.mod
+1 -1
@@ -17,7 +17,7 @@ require (
17 github.com/cloudflare/cfssl v1.6.5
18 github.com/coreos/go-systemd/v22 v22.5.0
19 github.com/docker/docker v27.3.1+incompatible
20 - github.com/facebook/time v0.0.0-20240626113945-18207c5d8ddc
20 + github.com/facebook/time v0.0.0-20241003211043-324a0f2e94c0
21 github.com/fsnotify/fsnotify v1.7.0
22 github.com/go-ldap/ldap/v3 v3.4.8
23 github.com/go-sql-driver/mysql v1.8.1
src/go/go.sum
+2 -2
@@ -76,8 +76,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
76 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
77 github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
78 github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
79 -github.com/facebook/time v0.0.0-20240626113945-18207c5d8ddc h1:0VQsg5ZXW9MPUxzemUHW7UBK8gfIO8K+YJGbdv4kBIM=
80 -github.com/facebook/time v0.0.0-20240626113945-18207c5d8ddc/go.mod h1:2UFAomOuD2vAK1x68czUtCVjAqmyWCEnAXOlmGqf+G0=
79 +github.com/facebook/time v0.0.0-20241003211043-324a0f2e94c0 h1:Z/QF2fnP1QWi755nNDKxiOfnSboaF/lvsYlB2eh4Hiw=
80 +github.com/facebook/time v0.0.0-20241003211043-324a0f2e94c0/go.mod h1:ROiLXrJb1QHiB4rvK6Sqhl6SAgkjV47z5O/Oy5jKzgU=
81 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
82 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
83 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
src/go/plugin/go.d/modules/chrony/charts.go
+39 -104
@@ -20,10 +20,8 @@ const (
20 prioRefMeasurementTime
21 prioLeapStatus
22 prioActivity
23 - //prioNTPPackets
24 - //prioCommandPackets
25 - //prioNKEConnections
26 - //prioClientLogRecords
23 + prioNTPPackets
24 + prioCommandPackets
25 )
26
27 var charts = module.Charts{
@@ -216,105 +214,42 @@ var (
214 }
215 )
216
219 -//var serverStatsVer1Charts = module.Charts{
220 -// ntpPacketsChart.Copy(),
221 -// commandPacketsChart.Copy(),
222 -// clientLogRecordsChart.Copy(),
223 -//}
224 -//
225 -//var serverStatsVer2Charts = module.Charts{
226 -// ntpPacketsChart.Copy(),
227 -// commandPacketsChart.Copy(),
228 -// clientLogRecordsChart.Copy(),
229 -// nkeConnectionChart.Copy(),
230 -//}
231 -//
232 -//var serverStatsVer3Charts = module.Charts{
233 -// ntpPacketsChart.Copy(),
234 -// commandPacketsChart.Copy(),
235 -// clientLogRecordsChart.Copy(),
236 -// nkeConnectionChart.Copy(),
237 -//}
238 -//
239 -//var serverStatsVer4Charts = module.Charts{
240 -// ntpPacketsChart.Copy(),
241 -// commandPacketsChart.Copy(),
242 -// clientLogRecordsChart.Copy(),
243 -// nkeConnectionChart.Copy(),
244 -//}
217 +var serverStatsCharts = module.Charts{
218 + ntpPacketsChart.Copy(),
219 + commandPacketsChart.Copy(),
220 +}
221
246 -// ServerStats charts
247 -//var (
248 -// ntpPacketsChart = module.Chart{
249 -// ID: "ntp_packets",
250 -// Title: "NTP packets",
251 -// Units: "packets/s",
252 -// Fam: "client requests",
253 -// Ctx: "chrony.ntp_packets",
254 -// Type: module.Stacked,
255 -// Priority: prioNTPPackets,
256 -// Dims: module.Dims{
257 -// {ID: "ntp_packets_received", Name: "received", Algo: module.Incremental},
258 -// {ID: "ntp_packets_dropped", Name: "dropped", Algo: module.Incremental},
259 -// },
260 -// }
261 -// commandPacketsChart = module.Chart{
262 -// ID: "command_packets",
263 -// Title: "Command packets",
264 -// Units: "packets/s",
265 -// Fam: "client requests",
266 -// Ctx: "chrony.command_packets",
267 -// Type: module.Stacked,
268 -// Priority: prioCommandPackets,
269 -// Dims: module.Dims{
270 -// {ID: "command_packets_received", Name: "received", Algo: module.Incremental},
271 -// {ID: "command_packets_dropped", Name: "dropped", Algo: module.Incremental},
272 -// },
273 -// }
274 -// nkeConnectionChart = module.Chart{
275 -// ID: "nke_connections",
276 -// Title: "NTS-KE connections",
277 -// Units: "connections/s",
278 -// Fam: "client requests",
279 -// Ctx: "chrony.nke_connections",
280 -// Type: module.Stacked,
281 -// Priority: prioNKEConnections,
282 -// Dims: module.Dims{
283 -// {ID: "nke_connections_accepted", Name: "accepted", Algo: module.Incremental},
284 -// {ID: "nke_connections_dropped", Name: "dropped", Algo: module.Incremental},
285 -// },
286 -// }
287 -// clientLogRecordsChart = module.Chart{
288 -// ID: "client_log_records",
289 -// Title: "Client log records",
290 -// Units: "records/s",
291 -// Fam: "client requests",
292 -// Ctx: "chrony.client_log_records",
293 -// Type: module.Stacked,
294 -// Priority: prioClientLogRecords,
295 -// Dims: module.Dims{
296 -// {ID: "client_log_records_dropped", Name: "dropped", Algo: module.Incremental},
297 -// },
298 -// }
299 -//)
222 +var (
223 + ntpPacketsChart = module.Chart{
224 + ID: "ntp_packets",
225 + Title: "NTP packets",
226 + Units: "packets/s",
227 + Fam: "client requests",
228 + Ctx: "chrony.ntp_packets",
229 + Type: module.Line,
230 + Priority: prioNTPPackets,
231 + Dims: module.Dims{
232 + {ID: "ntp_packets_received", Name: "received", Algo: module.Incremental},
233 + {ID: "ntp_packets_dropped", Name: "dropped", Algo: module.Incremental},
234 + },
235 + }
236 + commandPacketsChart = module.Chart{
237 + ID: "command_packets",
238 + Title: "Command packets",
239 + Units: "packets/s",
240 + Fam: "client requests",
241 + Ctx: "chrony.command_packets",
242 + Type: module.Line,
243 + Priority: prioCommandPackets,
244 + Dims: module.Dims{
245 + {ID: "command_packets_received", Name: "received", Algo: module.Incremental},
246 + {ID: "command_packets_dropped", Name: "dropped", Algo: module.Incremental},
247 + },
248 + }
249 +)
250
301 -//func (c *Chrony) addServerStatsCharts(stats *serverStats) {
302 -// var err error
303 -//
304 -// switch {
305 -// case stats.v1 != nil:
306 -// err = c.Charts().Add(*serverStatsVer1Charts.Copy()...)
307 -// case stats.v2 != nil:
308 -// err = c.Charts().Add(*serverStatsVer2Charts.Copy()...)
309 -// case stats.v3 != nil:
310 -// err = c.Charts().Add(*serverStatsVer3Charts.Copy()...)
311 -// case stats.v4 != nil:
312 -// err = c.Charts().Add(*serverStatsVer4Charts.Copy()...)
313 -// default:
314 -// err = errors.New("unknown stats chart")
315 -// }
316 -//
317 -// if err != nil {
318 -// c.Warning(err)
319 -// }
320 -//}
251 +func (c *Chrony) addServerStatsCharts() {
252 + if err := c.Charts().Add(*serverStatsCharts.Copy()...); err != nil {
253 + c.Warning(err)
254 + }
255 +}
src/go/plugin/go.d/modules/chrony/chrony.go
+21 -24
@@ -10,8 +10,6 @@ import (
10
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
13 -
14 - "github.com/facebook/time/ntp/chrony"
13 )
14
15 //go:embed "config_schema.json"
@@ -31,9 +29,9 @@ func New() *Chrony {
29 Address: "127.0.0.1:323",
30 Timeout: confopt.Duration(time.Second),
31 },
34 - charts: charts.Copy(),
35 - addStatsChartsOnce: &sync.Once{},
36 - newClient: newChronyClient,
32 + charts: charts.Copy(),
33 + addServerStatsChartsOnce: &sync.Once{},
34 + newConn: newChronyConn,
35 }
36 }
37
@@ -43,24 +41,18 @@ type Config struct {
41 Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
42 }
43
46 -type (
47 - Chrony struct {
48 - module.Base
49 - Config `yaml:",inline" json:""`
44 +type Chrony struct {
45 + module.Base
46 + Config `yaml:",inline" json:""`
47
51 - charts *module.Charts
52 - addStatsChartsOnce *sync.Once
48 + charts *module.Charts
49 + addServerStatsChartsOnce *sync.Once
50
54 - client chronyClient
55 - newClient func(c Config) (chronyClient, error)
56 - }
57 - chronyClient interface {
58 - Tracking() (*chrony.ReplyTracking, error)
59 - Activity() (*chrony.ReplyActivity, error)
60 - ServerStats() (*serverStats, error)
61 - Close()
62 - }
63 -)
51 + exec chronyBinary
52 +
53 + conn chronyConn
54 + newConn func(c Config) (chronyConn, error)
55 +}
56
57 func (c *Chrony) Configuration() any {
58 return c.Config
@@ -72,6 +64,11 @@ func (c *Chrony) Init() error {
64 return err
65 }
66
67 + var err error
68 + if c.exec, err = c.initChronycBinary(); err != nil {
69 + c.Warningf("chronyc binary init failed: %v (serverstats metrics collection is disabled)", err)
70 + }
71 +
72 return nil
73 }
74
@@ -105,8 +102,8 @@ func (c *Chrony) Collect() map[string]int64 {
102 }
103
104 func (c *Chrony) Cleanup() {
108 - if c.client != nil {
109 - c.client.Close()
110 - c.client = nil
105 + if c.conn != nil {
106 + c.conn.close()
107 + c.conn = nil
108 }
109 }
src/go/plugin/go.d/modules/chrony/chrony_test.go
+33 -33
@@ -155,11 +155,13 @@ func TestChrony_Collect(t *testing.T) {
155 prepare func() *Chrony
156 expected map[string]int64
157 }{
158 - "tracking: success, activity: success": {
158 + "tracking: success, activity: success, serverstats: success": {
159 prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{}) },
160 expected: map[string]int64{
161 "burst_offline_sources": 3,
162 "burst_online_sources": 4,
163 + "command_packets_dropped": 1,
164 + "command_packets_received": 652,
165 "current_correction": 154872,
166 "frequency": 51051185607,
167 "last_offset": 3095,
@@ -167,6 +169,8 @@ func TestChrony_Collect(t *testing.T) {
169 "leap_status_insert_second": 1,
170 "leap_status_normal": 0,
171 "leap_status_unsynchronised": 0,
172 + "ntp_packets_dropped": 1,
173 + "ntp_packets_received": 1,
174 "offline_sources": 2,
175 "online_sources": 8,
176 "ref_measurement_time": 63793323616,
@@ -219,12 +223,13 @@ func TestChrony_Collect(t *testing.T) {
223 c := test.prepare()
224
225 require.NoError(t, c.Init())
226 + c.exec = &mockChronyc{}
227 _ = c.Check()
228
224 - collected := c.Collect()
225 - copyRefMeasurementTime(collected, test.expected)
229 + mx := c.Collect()
230 + copyRefMeasurementTime(mx, test.expected)
231
227 - assert.Equal(t, test.expected, collected)
232 + assert.Equal(t, test.expected, mx)
233 })
234 }
235 }
@@ -232,13 +237,32 @@ func TestChrony_Collect(t *testing.T) {
237 func prepareChronyWithMock(m *mockClient) *Chrony {
238 c := New()
239 if m == nil {
235 - c.newClient = func(_ Config) (chronyClient, error) { return nil, errors.New("mock.newClient error") }
240 + c.newConn = func(_ Config) (chronyConn, error) { return nil, errors.New("mock.newClient error") }
241 } else {
237 - c.newClient = func(_ Config) (chronyClient, error) { return m, nil }
242 + c.newConn = func(_ Config) (chronyConn, error) { return m, nil }
243 }
244 return c
245 }
246
247 +type mockChronyc struct{}
248 +
249 +func (m *mockChronyc) serverStats() ([]byte, error) {
250 + data := `
251 +NTP packets received : 1
252 +NTP packets dropped : 1
253 +Command packets received : 652
254 +Command packets dropped : 1
255 +Client log records dropped : 1
256 +NTS-KE connections accepted: 1
257 +NTS-KE connections dropped : 1
258 +Authenticated NTP packets : 1
259 +Interleaved NTP packets : 1
260 +NTP timestamps held : 1
261 +NTP timestamp span : 0
262 +`
263 + return []byte(data), nil
264 +}
265 +
266 type mockClient struct {
267 errOnTracking bool
268 errOnActivity bool
@@ -246,7 +270,7 @@ type mockClient struct {
270 closeCalled bool
271 }
272
249 -func (m *mockClient) Tracking() (*chrony.ReplyTracking, error) {
273 +func (m *mockClient) tracking() (*chrony.ReplyTracking, error) {
274 if m.errOnTracking {
275 return nil, errors.New("mockClient.Tracking call error")
276 }
@@ -271,7 +295,7 @@ func (m *mockClient) Tracking() (*chrony.ReplyTracking, error) {
295 return &reply, nil
296 }
297
274 -func (m *mockClient) Activity() (*chrony.ReplyActivity, error) {
298 +func (m *mockClient) activity() (*chrony.ReplyActivity, error) {
299 if m.errOnActivity {
300 return nil, errors.New("mockClient.Activity call error")
301 }
@@ -287,31 +311,7 @@ func (m *mockClient) Activity() (*chrony.ReplyActivity, error) {
311 return &reply, nil
312 }
313
290 -func (m *mockClient) ServerStats() (*serverStats, error) {
291 - if m.errOnServerStats {
292 - return nil, errors.New("mockClient.ServerStats call error")
293 - }
294 -
295 - reply := serverStats{
296 - v3: &chrony.ServerStats3{
297 - NTPHits: 10,
298 - NKEHits: 10,
299 - CMDHits: 10,
300 - NTPDrops: 1,
301 - NKEDrops: 1,
302 - CMDDrops: 1,
303 - LogDrops: 1,
304 - NTPAuthHits: 10,
305 - NTPInterleavedHits: 10,
306 - NTPTimestamps: 0,
307 - NTPSpanSeconds: 0,
308 - },
309 - }
310 -
311 - return &reply, nil
312 -}
313 -
314 -func (m *mockClient) Close() {
314 +func (m *mockClient) close() {
315 m.closeCalled = true
316 }
317
src/go/plugin/go.d/modules/chrony/client.go
+42 -110
@@ -10,55 +10,40 @@ import (
10 "github.com/facebook/time/ntp/chrony"
11 )
12
13 -func newChronyClient(c Config) (chronyClient, error) {
14 - conn, err := net.DialTimeout("udp", c.Address, c.Timeout.Duration())
13 +type chronyConn interface {
14 + tracking() (*chrony.ReplyTracking, error)
15 + activity() (*chrony.ReplyActivity, error)
16 + close()
17 +}
18 +
19 +func newChronyConn(cfg Config) (chronyConn, error) {
20 + conn, err := net.DialTimeout("udp", cfg.Address, cfg.Timeout.Duration())
21 if err != nil {
22 return nil, err
23 }
24
19 - client := &simpleClient{
25 + client := &chronyClient{
26 conn: conn,
21 - client: &chrony.Client{Connection: &connWithTimeout{
22 - Conn: conn,
23 - timeout: c.Timeout.Duration(),
24 - }},
27 + client: &chrony.Client{
28 + Connection: &connWithTimeout{
29 + Conn: conn,
30 + timeout: cfg.Timeout.Duration(),
31 + },
32 + },
33 }
34
35 return client, nil
36 }
37
30 -type connWithTimeout struct {
31 - net.Conn
32 - timeout time.Duration
33 -}
34 -
35 -func (c *connWithTimeout) Read(p []byte) (n int, err error) {
36 - if err := c.Conn.SetReadDeadline(c.deadline()); err != nil {
37 - return 0, err
38 - }
39 - return c.Conn.Read(p)
40 -}
41 -
42 -func (c *connWithTimeout) Write(p []byte) (n int, err error) {
43 - if err := c.Conn.SetWriteDeadline(c.deadline()); err != nil {
44 - return 0, err
45 - }
46 - return c.Conn.Write(p)
47 -}
48 -
49 -func (c *connWithTimeout) deadline() time.Time {
50 - return time.Now().Add(c.timeout)
51 -}
52 -
53 -type simpleClient struct {
38 +type chronyClient struct {
39 conn net.Conn
40 client *chrony.Client
41 }
42
58 -func (sc *simpleClient) Tracking() (*chrony.ReplyTracking, error) {
43 +func (c *chronyClient) tracking() (*chrony.ReplyTracking, error) {
44 req := chrony.NewTrackingPacket()
45
61 - reply, err := sc.client.Communicate(req)
46 + reply, err := c.client.Communicate(req)
47 if err != nil {
48 return nil, err
49 }
@@ -67,13 +52,14 @@ func (sc *simpleClient) Tracking() (*chrony.ReplyTracking, error) {
52 if !ok {
53 return nil, fmt.Errorf("unexpected reply type, want=%T, got=%T", &chrony.ReplyTracking{}, reply)
54 }
55 +
56 return tracking, nil
57 }
58
73 -func (sc *simpleClient) Activity() (*chrony.ReplyActivity, error) {
59 +func (c *chronyClient) activity() (*chrony.ReplyActivity, error) {
60 req := chrony.NewActivityPacket()
61
76 - reply, err := sc.client.Communicate(req)
62 + reply, err := c.client.Communicate(req)
63 if err != nil {
64 return nil, err
65 }
@@ -82,90 +68,36 @@ func (sc *simpleClient) Activity() (*chrony.ReplyActivity, error) {
68 if !ok {
69 return nil, fmt.Errorf("unexpected reply type, want=%T, got=%T", &chrony.ReplyActivity{}, reply)
70 }
71 +
72 return activity, nil
73 }
74
88 -type serverStats struct {
89 - v1 *chrony.ServerStats
90 - v2 *chrony.ServerStats2
91 - v3 *chrony.ServerStats3
92 - v4 *chrony.ServerStats4
75 +func (c *chronyClient) close() {
76 + if c.conn != nil {
77 + _ = c.conn.Close()
78 + c.conn = nil
79 + }
80 }
81
95 -func (sc *simpleClient) ServerStats() (*serverStats, error) {
96 - req := chrony.NewServerStatsPacket()
82 +type connWithTimeout struct {
83 + net.Conn
84 + timeout time.Duration
85 +}
86
98 - reply, err := sc.client.Communicate(req)
99 - if err != nil {
100 - return nil, err
87 +func (c *connWithTimeout) Read(p []byte) (n int, err error) {
88 + if err := c.Conn.SetReadDeadline(c.deadline()); err != nil {
89 + return 0, err
90 }
91 + return c.Conn.Read(p)
92 +}
93
103 - var stats serverStats
104 -
105 - switch v := reply.(type) {
106 - case *chrony.ReplyServerStats:
107 - stats.v1 = &chrony.ServerStats{
108 - NTPHits: v.NTPHits,
109 - CMDHits: v.CMDHits,
110 - NTPDrops: v.NTPDrops,
111 - CMDDrops: v.CMDDrops,
112 - LogDrops: v.LogDrops,
113 - }
114 - case *chrony.ReplyServerStats2:
115 - stats.v2 = &chrony.ServerStats2{
116 - NTPHits: v.NTPHits,
117 - NKEHits: v.NKEHits,
118 - CMDHits: v.CMDHits,
119 - NTPDrops: v.NTPDrops,
120 - NKEDrops: v.NKEDrops,
121 - CMDDrops: v.CMDDrops,
122 - LogDrops: v.LogDrops,
123 - NTPAuthHits: v.NTPAuthHits,
124 - }
125 - case *chrony.ReplyServerStats3:
126 - stats.v3 = &chrony.ServerStats3{
127 - NTPHits: v.NTPHits,
128 - NKEHits: v.NKEHits,
129 - CMDHits: v.CMDHits,
130 - NTPDrops: v.NTPDrops,
131 - NKEDrops: v.NKEDrops,
132 - CMDDrops: v.CMDDrops,
133 - LogDrops: v.LogDrops,
134 - NTPAuthHits: v.NTPAuthHits,
135 - NTPInterleavedHits: v.NTPInterleavedHits,
136 - NTPTimestamps: v.NTPTimestamps,
137 - NTPSpanSeconds: v.NTPSpanSeconds,
138 - }
139 - case *chrony.ReplyServerStats4:
140 - stats.v4 = &chrony.ServerStats4{
141 - NTPHits: v.NTPHits,
142 - NKEHits: v.NKEHits,
143 - CMDHits: v.CMDHits,
144 - NTPDrops: v.NTPDrops,
145 - NKEDrops: v.NKEDrops,
146 - CMDDrops: v.CMDDrops,
147 - LogDrops: v.LogDrops,
148 - NTPAuthHits: v.NTPAuthHits,
149 - NTPInterleavedHits: v.NTPInterleavedHits,
150 - NTPTimestamps: v.NTPTimestamps,
151 - NTPSpanSeconds: v.NTPSpanSeconds,
152 - NTPDaemonRxtimestamps: v.NTPDaemonRxtimestamps,
153 - NTPDaemonTxtimestamps: v.NTPDaemonTxtimestamps,
154 - NTPKernelRxtimestamps: v.NTPKernelRxtimestamps,
155 - NTPKernelTxtimestamps: v.NTPKernelTxtimestamps,
156 - NTPHwRxTimestamps: v.NTPHwRxTimestamps,
157 - NTPHwTxTimestamps: v.NTPHwTxTimestamps,
158 - }
159 - default:
160 - return nil, fmt.Errorf("unexpected reply type, want=ReplyServerStats, got=%T", reply)
94 +func (c *connWithTimeout) Write(p []byte) (n int, err error) {
95 + if err := c.Conn.SetWriteDeadline(c.deadline()); err != nil {
96 + return 0, err
97 }
162 -
163 - return &stats, nil
98 + return c.Conn.Write(p)
99 }
100
166 -func (sc *simpleClient) Close() {
167 - if sc.conn != nil {
168 - _ = sc.conn.Close()
169 - sc.conn = nil
170 - }
101 +func (c *connWithTimeout) deadline() time.Time {
102 + return time.Now().Add(c.timeout)
103 }
src/go/plugin/go.d/modules/chrony/collect.go
+62 -71
@@ -3,19 +3,32 @@
3 package chrony
4
5 import (
6 + "bufio"
7 + "bytes"
8 + "errors"
9 "fmt"
10 + "strconv"
11 + "strings"
12 "time"
13 )
14
15 const scaleFactor = 1000000000
16
17 +const (
18 + // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/ntp.h#L70-L75
19 + leapStatusNormal = 0
20 + leapStatusInsertSecond = 1
21 + leapStatusDeleteSecond = 2
22 + leapStatusUnsynchronised = 3
23 +)
24 +
25 func (c *Chrony) collect() (map[string]int64, error) {
13 - if c.client == nil {
14 - client, err := c.newClient(c.Config)
26 + if c.conn == nil {
27 + client, err := c.newConn(c.Config)
28 if err != nil {
29 return nil, err
30 }
18 - c.client = client
31 + c.conn = client
32 }
33
34 mx := make(map[string]int64)
@@ -26,28 +39,20 @@ func (c *Chrony) collect() (map[string]int64, error) {
39 if err := c.collectActivity(mx); err != nil {
40 return mx, err
41 }
29 - //if strings.HasPrefix(c.Address, "/") {
30 - // TODO: Allowed only through the Unix domain socket (requires "_chrony" group membership).
31 - // See https://github.com/facebook/time/blob/18207c5d8ddc7242e8d4192985898b6dbe66932c/cmd/ntpcheck/checker/chrony.go#L38
32 - // ^^ For some reason doesn't work, Chrony doesn't respond. Additional configuration needed?
33 - //if err := c.collectServerStats(mx); err != nil {
34 - // return mx, err
35 - //}
36 - //}
42 + if c.exec != nil {
43 + if err := c.collectServerStats(mx); err != nil {
44 + c.Warning(err)
45 + c.exec = nil
46 + } else {
47 + c.addServerStatsChartsOnce.Do(c.addServerStatsCharts)
48 + }
49 + }
50
51 return mx, nil
52 }
53
41 -const (
42 - // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/ntp.h#L70-L75
43 - leapStatusNormal = 0
44 - leapStatusInsertSecond = 1
45 - leapStatusDeleteSecond = 2
46 - leapStatusUnsynchronised = 3
47 -)
48 -
54 func (c *Chrony) collectTracking(mx map[string]int64) error {
50 - reply, err := c.client.Tracking()
55 + reply, err := c.conn.tracking()
56 if err != nil {
57 return fmt.Errorf("error on collecting tracking: %v", err)
58 }
@@ -76,7 +81,7 @@ func (c *Chrony) collectTracking(mx map[string]int64) error {
81 }
82
83 func (c *Chrony) collectActivity(mx map[string]int64) error {
79 - reply, err := c.client.Activity()
84 + reply, err := c.conn.activity()
85 if err != nil {
86 return fmt.Errorf("error on collecting activity: %v", err)
87 }
@@ -90,56 +95,42 @@ func (c *Chrony) collectActivity(mx map[string]int64) error {
95 return nil
96 }
97
93 -//func (c *Chrony) collectServerStats(mx map[string]int64) error {
94 -// stats, err := c.client.ServerStats()
95 -// if err != nil {
96 -// return fmt.Errorf("error on collecting server stats: %v", err)
97 -// }
98 -//
99 -// switch {
100 -// case stats.v4 != nil:
101 -// mx["ntp_packets_received"] = int64(stats.v4.NTPHits)
102 -// mx["ntp_packets_dropped"] = int64(stats.v4.NTPDrops)
103 -// mx["command_packets_received"] = int64(stats.v4.CMDHits)
104 -// mx["command_packets_dropped"] = int64(stats.v4.CMDDrops)
105 -// mx["client_log_records_dropped"] = int64(stats.v4.LogDrops)
106 -// mx["nke_connections_accepted"] = int64(stats.v4.NKEHits)
107 -// mx["nke_connections_dropped"] = int64(stats.v4.NKEDrops)
108 -// mx["authenticated_ntp_packets"] = int64(stats.v4.NTPAuthHits)
109 -// mx["interleaved_ntp_packets"] = int64(stats.v4.NTPInterleavedHits)
110 -// case stats.v3 != nil:
111 -// mx["ntp_packets_received"] = int64(stats.v3.NTPHits)
112 -// mx["ntp_packets_dropped"] = int64(stats.v3.NTPDrops)
113 -// mx["command_packets_received"] = int64(stats.v3.CMDHits)
114 -// mx["command_packets_dropped"] = int64(stats.v3.CMDDrops)
115 -// mx["client_log_records_dropped"] = int64(stats.v3.LogDrops)
116 -// mx["nke_connections_accepted"] = int64(stats.v3.NKEHits)
117 -// mx["nke_connections_dropped"] = int64(stats.v3.NKEDrops)
118 -// mx["authenticated_ntp_packets"] = int64(stats.v3.NTPAuthHits)
119 -// mx["interleaved_ntp_packets"] = int64(stats.v3.NTPInterleavedHits)
120 -// case stats.v2 != nil:
121 -// mx["ntp_packets_received"] = int64(stats.v2.NTPHits)
122 -// mx["ntp_packets_dropped"] = int64(stats.v2.NTPDrops)
123 -// mx["command_packets_received"] = int64(stats.v2.CMDHits)
124 -// mx["command_packets_dropped"] = int64(stats.v2.CMDDrops)
125 -// mx["client_log_records_dropped"] = int64(stats.v2.LogDrops)
126 -// mx["nke_connections_accepted"] = int64(stats.v2.NKEHits)
127 -// mx["nke_connections_dropped"] = int64(stats.v2.NKEDrops)
128 -// mx["authenticated_ntp_packets"] = int64(stats.v2.NTPAuthHits)
129 -// case stats.v1 != nil:
130 -// mx["ntp_packets_received"] = int64(stats.v1.NTPHits)
131 -// mx["ntp_packets_dropped"] = int64(stats.v1.NTPDrops)
132 -// mx["command_packets_received"] = int64(stats.v1.CMDHits)
133 -// mx["command_packets_dropped"] = int64(stats.v1.CMDDrops)
134 -// mx["client_log_records_dropped"] = int64(stats.v1.LogDrops)
135 -// default:
136 -// return errors.New("invalid server stats reply")
137 -// }
138 -//
139 -// //c.addStatsChartsOnce.Do(func() { c.addServerStatsCharts(stats) })
140 -//
141 -// return nil
142 -//}
98 +func (c *Chrony) collectServerStats(mx map[string]int64) error {
99 + bs, err := c.exec.serverStats()
100 + if err != nil {
101 + return fmt.Errorf("error on collecting server stats: %v", err)
102 + }
103 +
104 + sc := bufio.NewScanner(bytes.NewReader(bs))
105 + var n int
106 +
107 + for sc.Scan() {
108 + key, value, ok := strings.Cut(sc.Text(), ":")
109 + if !ok {
110 + continue
111 + }
112 +
113 + key, value = strings.TrimSpace(key), strings.TrimSpace(value)
114 +
115 + switch key {
116 + case "NTP packets received",
117 + "NTP packets dropped",
118 + "Command packets received",
119 + "Command packets dropped":
120 + if v, err := strconv.ParseInt(value, 10, 64); err == nil {
121 + key = strings.ToLower(strings.ReplaceAll(key, " ", "_"))
122 + mx[key] = v
123 + n++
124 + }
125 + }
126 + }
127 +
128 + if n == 0 {
129 + return errors.New("no server stats metrics found in the response")
130 + }
131 +
132 + return nil
133 +}
134
135 func boolToInt(v bool) int64 {
136 if v {
src/go/plugin/go.d/modules/chrony/exec.go new
+46
@@ -0,0 +1,46 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package chrony
4 +
5 +import (
6 + "context"
7 + "fmt"
8 + "os/exec"
9 + "time"
10 +
11 + "github.com/netdata/netdata/go/plugins/logger"
12 +)
13 +
14 +type chronyBinary interface {
15 + serverStats() ([]byte, error)
16 +}
17 +
18 +func newChronycExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *chronycExec {
19 + return &chronycExec{
20 + Logger: log,
21 + ndsudoPath: ndsudoPath,
22 + timeout: timeout,
23 + }
24 +}
25 +
26 +type chronycExec struct {
27 + *logger.Logger
28 +
29 + ndsudoPath string
30 + timeout time.Duration
31 +}
32 +
33 +func (e *chronycExec) serverStats() ([]byte, error) {
34 + ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 + defer cancel()
36 +
37 + cmd := exec.CommandContext(ctx, e.ndsudoPath, "chronyc-serverstats")
38 + e.Debugf("executing '%s'", cmd)
39 +
40 + bs, err := cmd.Output()
41 + if err != nil {
42 + return nil, fmt.Errorf("error on '%s': %v", cmd, err)
43 + }
44 +
45 + return bs, nil
46 +}
src/go/plugin/go.d/modules/chrony/init.go
+33
@@ -4,6 +4,12 @@ package chrony
4
5 import (
6 "errors"
7 + "fmt"
8 + "net"
9 + "os"
10 + "path/filepath"
11 +
12 + "github.com/netdata/netdata/go/plugins/pkg/executable"
13 )
14
15 func (c *Chrony) validateConfig() error {
@@ -12,3 +18,30 @@ func (c *Chrony) validateConfig() error {
18 }
19 return nil
20 }
21 +
22 +func (c *Chrony) initChronycBinary() (chronyBinary, error) {
23 + host, _, err := net.SplitHostPort(c.Address)
24 + if err != nil {
25 + return nil, err
26 + }
27 +
28 + // 'serverstats' allowed only through the Unix domain socket
29 + if !isLocalhost(host) {
30 + return nil, nil
31 + }
32 +
33 + ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
34 +
35 + if _, err := os.Stat(ndsudoPath); err != nil {
36 + return nil, fmt.Errorf("ndsudo executable not found: %v", err)
37 + }
38 +
39 + chronyc := newChronycExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
40 +
41 + return chronyc, nil
42 +}
43 +
44 +func isLocalhost(host string) bool {
45 + ip := net.ParseIP(host)
46 + return host == "localhost" || (ip != nil && ip.IsLoopback())
47 +}
src/go/plugin/go.d/modules/chrony/metadata.yaml
+21 -2
@@ -20,8 +20,11 @@ modules:
20 most_popular: false
21 overview:
22 data_collection:
23 - metrics_description: This collector monitors the system's clock performance and peers activity status
24 - method_description: It collects metrics by sending UDP packets to chronyd using the Chrony communication protocol v6.
23 + metrics_description: |
24 + This collector monitors the system's clock performance and peers activity status
25 + method_description: |
26 + It collects metrics by sending UDP packets to chronyd using the Chrony communication protocol v6.
27 + 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.
28 supported_platforms:
29 include: []
30 exclude: []
@@ -206,3 +209,19 @@ modules:
209 - name: burst_online
210 - name: burst_offline
211 - name: unresolved
212 + - name: chrony.ntp_packets
213 + availability: []
214 + description: NTP packets
215 + unit: packets/s
216 + chart_type: line
217 + dimensions:
218 + - name: received
219 + - name: dropped
220 + - name: chrony.command_packets
221 + availability: []
222 + description: Command packets
223 + unit: packets/s
224 + chart_type: line
225 + dimensions:
226 + - name: received
227 + - name: dropped