Skip to content

Commit 9aad65e

Browse files
committed
chrony.py: add metrics from sources and sourcestats subcommands.
the peer labels where adapted from a node_exporter PR see prometheus/node_exporter#1317 Signed-off-by: Gordon Bleux <[email protected]>
1 parent b36a2ea commit 9aad65e

File tree

1 file changed

+149
-4
lines changed

1 file changed

+149
-4
lines changed

Diff for: chrony.py

+149-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,26 @@
33
# Description: Gather metrics from Chrony NTP.
44
#
55

6+
import math
67
import subprocess
78
import sys
89

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+
}
1026

1127

1228
def chronyc(*args, check=True):
@@ -17,15 +33,26 @@ def chronyc(*args, check=True):
1733
"""
1834
return subprocess.run(
1935
['chronyc', *args], stdout=subprocess.PIPE, check=check
20-
).stdout.decode('utf-8')
36+
).stdout.decode('utf-8').rstrip()
2137

2238

2339
def chronyc_tracking():
2440
return chronyc('-c', 'tracking').split(',')
2541

2642

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):
2956
chrony_tracking = chronyc_tracking()
3057

3158
if len(chrony_tracking) != 14:
@@ -58,6 +85,124 @@ def main():
5885
registry=registry)
5986
g.set(chrony_tracking[5])
6087

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)
61206
print(generate_latest(registry).decode("utf-8"), end='')
62207

63208

0 commit comments

Comments
 (0)