3
3
# Description: Gather metrics from Chrony NTP.
4
4
#
5
5
6
+ import math
6
7
import subprocess
7
8
import sys
8
9
9
- from prometheus_client import CollectorRegistry , Gauge , generate_latest
10
+ from prometheus_client import CollectorRegistry , Gauge , generate_latest , Info
11
+
12
+
13
+ SOURCE_STATUS_LABELS = {
14
+ "*" : "synchronized (system peer)" ,
15
+ "+" : "synchronized" ,
16
+ "?" : "unreachable" ,
17
+ "x" : "Falseticker" ,
18
+ "-" : "reference clock" ,
19
+ }
20
+
21
+ SOURCE_MODE_LABELS = {
22
+ '^' : "server" ,
23
+ '=' : "peer" ,
24
+ "#" : "reference clock" ,
25
+ }
10
26
11
27
12
28
def chronyc (* args , check = True ):
@@ -17,15 +33,26 @@ def chronyc(*args, check=True):
17
33
"""
18
34
return subprocess .run (
19
35
['chronyc' , * args ], stdout = subprocess .PIPE , check = check
20
- ).stdout .decode ('utf-8' )
36
+ ).stdout .decode ('utf-8' ). rstrip ()
21
37
22
38
23
39
def chronyc_tracking ():
24
40
return chronyc ('-c' , 'tracking' ).split (',' )
25
41
26
42
27
- def main ():
28
- registry = CollectorRegistry ()
43
+ def chronyc_sources ():
44
+ lines = chronyc ('-c' , 'sources' ).split ('\n ' )
45
+
46
+ return [line .split (',' ) for line in lines ]
47
+
48
+
49
+ def chronyc_sourcestats ():
50
+ lines = chronyc ('-c' , 'sourcestats' ).split ('\n ' )
51
+
52
+ return [line .split (',' ) for line in lines ]
53
+
54
+
55
+ def tracking_metrics (registry ):
29
56
chrony_tracking = chronyc_tracking ()
30
57
31
58
if len (chrony_tracking ) != 14 :
@@ -58,6 +85,124 @@ def main():
58
85
registry = registry )
59
86
g .set (chrony_tracking [5 ])
60
87
88
+
89
+ def sources_metrics (registry ):
90
+ chrony_sources = chronyc_sources ()
91
+
92
+ peer = Info ('chrony_source_peer' ,
93
+ 'Peer information' ,
94
+ registry = registry )
95
+ poll = Gauge ('chrony_source_poll_rate_seconds' ,
96
+ 'The rate at which the source is being polled' ,
97
+ ['ref_host' ],
98
+ registry = registry )
99
+ reach = Gauge ('chrony_source_reach_register' ,
100
+ 'The source reachability register' ,
101
+ ['ref_host' ],
102
+ registry = registry )
103
+ received = Gauge ('chrony_source_last_received_seconds' ,
104
+ 'Number of seconds ago the last sample was received from the source' ,
105
+ ['ref_host' ],
106
+ registry = registry )
107
+ original = Gauge ('chrony_source_original_offset_seconds' ,
108
+ 'The adjusted offset between ' +
109
+ 'the local clock and the source' ,
110
+ ['ref_host' ],
111
+ registry = registry )
112
+ measured = Gauge ('chrony_source_measured_offset_seconds' ,
113
+ 'The actual measured offset between ' +
114
+ 'the local clock and the source' ,
115
+ ['ref_host' ],
116
+ registry = registry )
117
+ margin = Gauge ('chrony_source_offset_margin_seconds' ,
118
+ 'The error margin in the offset measurement between ' +
119
+ 'the local clock and the source' ,
120
+ ['ref_host' ],
121
+ registry = registry )
122
+
123
+ for source in chrony_sources :
124
+ if len (source ) != 10 :
125
+ print ("ERROR: Unable to parse chronyc sources CSV" , file = sys .stderr )
126
+ sys .exit (1 )
127
+
128
+ mode = source [0 ]
129
+ status = source [1 ]
130
+ ref_host = source [2 ]
131
+ stratum = source [3 ]
132
+ rate = float (source [4 ])
133
+
134
+ if status not in SOURCE_STATUS_LABELS :
135
+ print ("ERROR: Invalid chrony source status '%s'" % status , file = sys .stderr )
136
+ sys .exit (1 )
137
+
138
+ if mode not in SOURCE_MODE_LABELS :
139
+ print ("ERROR: Invalid chrony source mode '%s'" % mode , file = sys .stderr )
140
+ sys .exit (1 )
141
+
142
+ peer .info ({
143
+ 'ref_host' : ref_host ,
144
+ 'stratum' : stratum ,
145
+ 'mode' : SOURCE_MODE_LABELS [mode ],
146
+ 'status' : SOURCE_STATUS_LABELS [status ],
147
+ })
148
+ poll .labels (ref_host ).set (math .pow (2.0 , rate ))
149
+ reach .labels (ref_host ).set (source [5 ])
150
+ received .labels (ref_host ).set (source [6 ])
151
+ original .labels (ref_host ).set (source [7 ])
152
+ measured .labels (ref_host ).set (source [8 ])
153
+ margin .labels (ref_host ).set (source [9 ])
154
+
155
+
156
+ def sourcestats_metrics (registry ):
157
+ chrony_sourcestats = chronyc_sourcestats ()
158
+
159
+ samples = Gauge ('chrony_source_sample_points' ,
160
+ 'The number of sample points currently being retained for the server' ,
161
+ ['ref_host' ],
162
+ registry = registry )
163
+ residuals = Gauge ('chrony_source_residual_runs' ,
164
+ 'The number of runs of residuals having the same ' +
165
+ 'sign following the last regression' ,
166
+ ['ref_host' ],
167
+ registry = registry )
168
+ span = Gauge ('chrony_source_sample_interval_span_seconds' ,
169
+ 'The interval between the oldest and newest samples' ,
170
+ ['ref_host' ],
171
+ registry = registry )
172
+ frequency = Gauge ('chrony_source_frequency_ppm' ,
173
+ 'The estimated residual frequency for the server' ,
174
+ ['ref_host' ],
175
+ registry = registry )
176
+ skew = Gauge ('chrony_source_frequency_skew_ppm' ,
177
+ 'The estimated error bounds on the residual frequency estimation' ,
178
+ ['ref_host' ],
179
+ registry = registry )
180
+ stddev = Gauge ('chrony_source_std_dev_seconds' ,
181
+ 'The estimated sample standard deviation.' ,
182
+ ['ref_host' ],
183
+ registry = registry )
184
+
185
+ for source in chrony_sourcestats :
186
+ if len (source ) != 8 :
187
+ print ("ERROR: Unable to parse chronyc sourcestats CSV" , file = sys .stderr )
188
+ sys .exit (1 )
189
+
190
+ ref_host = source [0 ]
191
+
192
+ samples .labels (ref_host ).set (source [1 ])
193
+ residuals .labels (ref_host ).set (source [2 ])
194
+ span .labels (ref_host ).set (source [3 ])
195
+ frequency .labels (ref_host ).set (source [4 ])
196
+ skew .labels (ref_host ).set (source [5 ])
197
+ stddev .labels (ref_host ).set (source [6 ])
198
+
199
+
200
+ def main ():
201
+ registry = CollectorRegistry ()
202
+
203
+ tracking_metrics (registry )
204
+ sources_metrics (registry )
205
+ sourcestats_metrics (registry )
61
206
print (generate_latest (registry ).decode ("utf-8" ), end = '' )
62
207
63
208
0 commit comments