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