2 # -*- coding: utf-8 -*-
4 ntpviz - visualizer for NTP log files
7 [-D DLVL | --debug DLVL]
16 [-w SIZE | --width SIZE]
25 --local-offset-histogram |
26 --local-offset-multiplot |
29 --peer-jitters=hosts |
30 --peer-offsets=hosts |
35 See the manual page for details.
37 Python by ESR, concept and gnuplot code by Dan Drown.
40 # Copyright the NTPsec project contributors
42 # SPDX-License-Identifier: BSD-2-Clause
44 from __future__
import print_function
, division
62 ntpviz: ERROR: can't find the Python argparse module
63 If your Python version is < 2.7, then manual installation is needed:
64 # pip install argparse
68 if sys
.version_info
[0] == 2:
71 # force UTF-8 strings, otherwise some systems crash on micro.
73 sys
.setdefaultencoding('utf8')
75 def open(file, mode
='r', buffering
=-1, encoding
=None, errors
=None):
77 return(codecs
.open(filename
=file, mode
=mode
, encoding
=encoding
,
78 errors
=errors
, buffering
=buffering
))
80 # believe it or not, Python has no way to make a simple constant!
81 MS_PER_S
= 1e3
# milliseconds per second
82 NS_PER_S
= 1e9
# nanoseconds per second
83 US_PER_S
= 1e6
# microseconds per second
84 S_PER_MS
= 1.0e-3 # seconds per millisecond
85 S_PER_NS
= 1.0e-9 # seconds per nanosecond
86 S_PER_US
= 1.0e-6 # seconds per microsecond
88 # table to translate refclock names
89 refclock_name
= {'127.127.20.0': 'NMEA(0)',
90 '127.127.20.1': 'NMEA(1)',
91 '127.127.20.2': 'NMEA(2)',
92 '127.127.20.3': 'NMEA(3)',
93 '127.127.22.0': 'PPS(0)',
94 '127.127.22.1': 'PPS(1)',
95 '127.127.22.2': 'PPS(2)',
96 '127.127.22.3': 'PPS(3)',
97 '127.127.28.0': 'SHM(0)',
98 '127.127.28.1': 'SHM(1)',
99 '127.127.28.2': 'SHM(2)',
100 '127.127.28.3': 'SHM(3)',
101 '127.127.46.0': 'GPS(0)',
102 '127.127.46.1': 'GPS(1)',
103 '127.127.46.2': 'GPS(2)',
104 '127.127.46.3': 'GPS(3)'}
107 # Gack, python before 3.2 has no defined tzinfo for utc...
109 class UTC(datetime
.tzinfo
):
112 def utcoffset(self
, dt
):
113 return datetime
.timedelta(0)
115 def tzname(self
, dt
):
119 return datetime
.timedelta(0)
125 except ImportError as e
:
126 sys
.stderr
.write("ntpviz: ERROR: can't find Python NTP library.\n%s\n"
127 "Check your PYTHONPATH\n" % e
)
130 # check Python version
132 if (3 > sys
.version_info
[0]) and (7 > sys
.version_info
[1]):
133 # running under Python version before 2.7
137 # overload ArgumentParser
138 class MyArgumentParser(argparse
.ArgumentParser
):
139 "class to parse arguments"
141 def convert_arg_line_to_args(self
, arg_line
):
142 '''Make options file more tolerant'''
143 # strip out trailing comments
144 arg_line
= re
.sub(r
'\s+#.*$', '', arg_line
)
149 # ignore comment lines
150 if '#' == arg_line
[0]:
153 return arg_line
.split()
157 """called by atexit() on normal exit to print profile data"""
159 pr
.print_stats('tottime')
160 pr
.print_stats('cumtime')
163 # standard deviation class
164 # use this until we can guarantee Python 3.4 and the statistics module
165 # http://stackoverflow.com/questions/15389768/standard-deviation-of-a-list#21505523
168 # Mean, Variance, Standard Deviation, Skewness and Kurtosis
170 class RunningStats(object):
171 "Calculate mean, variance, sigma, skewness and kurtosis"
173 def __init__(self
, values
):
174 self
.num
= len(values
) # number of samples
175 self
.mu
= 0.0 # simple arithmetic mean
176 self
.variance
= 0.0 # variance
177 self
.sigma
= 0.0 # aka standard deviation
185 self
.mu
= sum(values
) / self
.num
186 self
.variance
= sum(pow((v
-self
.mu
), 2) for v
in values
) / self
.num
187 self
.sigma
= math
.sqrt(self
.variance
)
189 if math
.isnan(self
.sigma
) or 1e-12 >= abs(self
.sigma
):
191 self
.skewness
= float('nan')
192 self
.kurtosis
= float('nan')
198 m3
+= pow(val
- self
.sigma
, 3)
199 m4
+= pow(val
- self
.sigma
, 4)
201 self
.skewness
= m3
/ (self
.num
* pow(self
.sigma
, 3))
202 self
.kurtosis
= m4
/ (self
.num
* pow(self
.sigma
, 4))
204 # end standard deviation class
207 # class for calced values
208 class VizStats(ntp
.statfiles
.NTPStats
):
209 "Class for calculated values"
211 percs
= {} # dictionary of percentages
213 unit
= 's' # display units: s, ppm, etc.
218 # observe RFC 4180, end lines with CRLF
219 csv_head
= ["Name", "Min", "1%", "5%", "50%", "95%", "99%", "Max", "",
220 "90% Range", "98% Range", "StdDev", "", "Mean", "Units",
221 "Skewness", "Kurtosis"]
227 <tr style="font-weight:bold;text-align:left;">
228 <td style="width:300px;"></td>
229 <td colspan=8> Percentiles......</td>
230 <td colspan=3> Ranges......</td>
232 <td style="text-align:right;">Skew-</td>
233 <td style="text-align:right;">Kurt-</td>
235 <tr style="font-weight:bold;text-align:right;">
236 <td style="text-align:left;">Name</td>
237 <td>Min</td><td>1%</td><td>5%</td><td>50%</td><td>95%</td>
238 <td>99%</td><td>Max</td> <td style="width:10px;"> </td>
239 <td>90%</td><td>98%</td><td>StdDev</td>
240 <td style="width:10px;"> </td><td>Mean</td><td>Units</td>
241 <td>ness</td><td>osis</td>
249 def __init__(self
, values
, title
, freq
=0, units
=''):
252 self
.percs
= self
.percentiles((100, 99, 95, 50, 5, 1, 0), values
)
254 # find the target for autoranging
256 # keep 99% and 1% under 999 in selected units
258 target
= max(self
.percs
["p99"], -self
.percs
["p1"])
260 # keep 99% and 1% under 999 in selected units
261 # but do not let 100% and 1% go over 5000 in selected units
262 target
= max(self
.percs
["p99"], -self
.percs
["p1"],
263 self
.percs
["p100"]/5, -self
.percs
["p0"]/5)
279 elif S_PER_MS
<= target
:
280 self
.multiplier
= MS_PER_S
288 elif S_PER_US
<= target
:
289 self
.multiplier
= US_PER_S
297 self
.multiplier
= NS_PER_S
304 sts
= RunningStats(values
)
305 self
.percs
["mu"] = sts
.mu
306 self
.percs
["pstd"] = sts
.sigma
311 self
.percs
["r90"] = self
.percs
["p95"] - self
.percs
["p5"]
312 self
.percs
["r98"] = self
.percs
["p99"] - self
.percs
["p1"]
314 # calculate mean +/- std dev
315 self
.percs
["m1sigma"] = self
.percs
["mu"] - self
.percs
["pstd"]
316 self
.percs
["p1sigma"] = self
.percs
["mu"] + self
.percs
["pstd"]
318 # pretty print the values
320 for k
, v
in self
.percs
.items():
323 self
.percs
[k
] = round(v
, 4)
324 if 'ppm' == self
.unit
and 0.020 > abs(self
.percs
[k
]):
329 # Python 2.6 does not undertand the comma format option
331 self
.percs_f
[k
] = format(v
, fmt
)
333 # don't scale skewness and kurtosis
334 self
.percs
["skew"] = sts
.skewness
335 self
.percs
["kurt"] = sts
.kurtosis
337 # skip for temperatures.
338 self
.percs_f
["skew"] = ''
339 self
.percs_f
["kurt"] = ''
341 self
.percs_f
["skew"] = format(self
.percs
["skew"], "6.4g")
342 self
.percs_f
["kurt"] = format(self
.percs
["kurt"], "6.4g")
345 self
.percs
["min_y"] = self
.percs
["p1"]
346 self
.percs
["max_y"] = self
.percs
["p99"]
347 self
.percs
["clipped"] = " (clipped)"
349 self
.percs
["min_y"] = self
.percs
["p0"]
350 self
.percs
["max_y"] = self
.percs
["p100"]
351 self
.percs
["clipped"] = ""
353 self
.fmt
= gnuplot_fmt(self
.percs
["min_y"], self
.percs
["max_y"])
355 # Python is stupid about nested objects, so add in some other stuff
356 self
.percs_f
["fmt"] = self
.percs
["fmt"] = self
.fmt
357 self
.percs_f
["multiplier"] = self
.percs
["multiplier"] = self
.multiplier
358 self
.percs_f
["title"] = self
.percs
["title"] = self
.title
359 self
.percs_f
["unit"] = self
.percs
["unit"] = self
.unit
361 s
= ["%(title)s", "%(p0)s", "%(p1)s", "%(p5)s", "%(p50)s", "%(p95)s",
362 " %(p99)s", "%(p100)s", "", "%(r90)s", "%(r98)s", "%(pstd)s",
363 "", "%(mu)s", "%(unit)s", "%(skew)s", "%(kurt)s", ]
365 # csv is raw, html table is autoranged
366 self
.csv
= [x
% self
.percs
for x
in s
]
367 self
.table
= [x
% self
.percs_f
for x
in s
]
368 self
.table
= "</td>\n <td>".join(self
.table
)
372 <td style="text-align:left;">%s</td>
379 def gnuplot_fmt(min_val
, max_val
):
380 "return optimal gnuplot format"
381 span
= max_val
- min_val
393 # end calc things now
395 # RMS frequency jitter - Deviation from root-mean-square linear approximation?
398 def gnuplot(template
, outfile
=None):
399 "Run a specified gnuplot program."
402 # silently ignore empty plots
408 if 2 <= args
.debug_level
:
409 sys
.stderr
.write("ntpviz: INFO: sending plot output "
411 out
= open(outfile
, "w", encoding
='utf-8')
414 # can be 30% faster to write to a tmp file than to pipe to gnuplot
415 # bonus, we can keep the plot file for debug.
416 if sys
.version_info
[0] == 2:
417 tmp_file
= tempfile
.NamedTemporaryFile(mode
='w',
418 suffix
='.plt', delete
=False)
420 tmp_file
= tempfile
.NamedTemporaryFile(mode
='w', encoding
='utf-8',
421 suffix
='.plt', delete
=False)
422 # note that tmp_file is a file handle, it is not a file object
423 tmp_file
.write(template
)
426 # shell=True is a security hazard, do not use
428 rcode
= subprocess
.call(['gnuplot', tmp_file
.name
], stdout
=out
)
430 if e
.errno
== os
.errno
.ENOENT
:
432 sys
.stderr
.write("ntpviz: ERROR: gnuplot not found in path\n")
434 # Something else went wrong while trying to run gnuplot
435 sys
.stderr
.write("ntpviz: ERROR: gnuplot failed\n")
439 sys
.stderr
.write("ntpviz: WARNING: plot returned %s\n" % rcode
)
440 sys
.stderr
.write("ntpviz: WARNING: plot file %s\n" % tmp_file
.name
)
441 elif 2 <= args
.debug_level
:
442 sys
.stderr
.write("ntpviz: INFO: plot file %s\n" % tmp_file
.name
)
445 os
.remove(tmp_file
.name
)
450 class NTPViz(ntp
.statfiles
.NTPStats
):
451 "Class for visualizing statistics from a single server."
453 # Python takes single quotes here. Since no % substitution
456 set autoscale xfixmin
457 set autoscale xfixmax
459 set xlabel "Time UTC"
460 set xtics format "%d %b\\n%H:%MZ"
466 def __init__(self
, statsdir
,
467 sitename
=None, period
=None, starttime
=None, endtime
=None):
468 ntp
.statfiles
.NTPStats
.__init
__(self
, statsdir
=statsdir
,
474 def plot_slice(self
, rows
, item1
, item2
=None):
475 "slice 0,item1, maybe item2, from rows, ready for gnuplot"
476 # speed up by only sending gnuplot the data it will actually use
477 # WARNING: this is hot code, only modify if you profile
478 # since we are looping the data, get the values too
486 val1
= float(row
[item1
])
487 val2
= float(row
[item2
])
488 if 2200000 < row
[0] - last_time
:
489 # more than 2,200 seconds between points
490 # data loss, add a break in the plot line
492 # fields: time, fld1, and fld2
493 plot_data
+= row
[1] + ' ' + row
[item1
] + ' ' \
500 # both values are good, append them.
506 values1
.append(float(row
[item1
]))
507 if 2200000 < row
[0] - last_time
:
508 # more than 2,200 seconds between points
509 # data loss, add a break in the plot line
512 plot_data
+= row
[1] + ' ' + row
[item1
] + '\n'
519 # I know you want to replace the plot_data string concat with
520 # or more join()s, do not do it, it is slower
521 # next you'll want to try %-substitution. it too is slower
524 return (plot_data
, values1
, values2
)
526 return (plot_data
, values1
)
528 def local_offset_gnuplot(self
):
529 "Generate gnuplot code graphing local clock loop statistics"
530 if not self
.loopstats
:
531 sys
.stderr
.write("ntpviz: WARNING: no loopstats to graph\n")
534 # speed up by only sending gnuplot the data it will actually use
535 # fields: time, time offset, freq offset
536 (plot_data
, values
, values_f
) = self
.plot_slice(self
.loopstats
, 2, 3)
538 # compute clock offset
539 stats
= VizStats(values
, "Local Clock Time Offset")
541 # compute frequency offset
542 stats_f
= VizStats(values_f
, "Local Clock Frequency Offset", freq
=1)
545 out
["fmt_f"] = stats_f
.percs
["fmt"]
546 out
["fmt"] = stats
.percs
["fmt"]
547 out
["max_y2"] = stats_f
.percs
["max_y"]
548 out
["min_y2"] = stats_f
.percs
["min_y"]
549 out
["multiplier_f"] = stats_f
.percs
["multiplier"]
550 out
["sitename"] = self
.sitename
551 out
['size'] = args
.img_size
552 out
['terminal'] = args
.terminal
553 out
["unit_f"] = stats_f
.percs
["unit"]
555 plot_template
= NTPViz
.Common
+ """\
556 set terminal %(terminal)s size %(size)s
557 set title "%(sitename)s: Local Clock Time/Frequency Offsets%(clipped)s"
558 set ytics format "%(fmt)s %(unit)s" nomirror textcolor rgb '#0060ad'
559 set yrange [%(min_y)s:%(max_y)s]
560 set y2tics format "%(fmt_f)s %(unit_f)s" nomirror textcolor rgb '#dd181f'
561 set y2range [%(min_y2)s:%(max_y2)s]
563 set style line 1 lc rgb '#0060ad' lt 1 lw 1 pt 7 ps 0 # --- blue
564 set style line 2 lc rgb '#dd181f' lt 1 lw 1 pt 5 ps 0 # --- red
566 "-" using 1:($2*%(multiplier)s) title "clock offset %(unit)s" \
567 with linespoints ls 1, \
568 "-" using 1:($3*%(multiplier_f)s) title "frequency offset %(unit_f)s" \
569 with linespoints ls 2 axis x1y2
573 <p>The time and frequency offsets between the ntpd calculated time
574 and the local system clock. Showing frequency offset (red, in parts
575 per million, scale on right) and the time offset (blue, in μs, scale
576 on left). Quick changes in time offset will lead to larger frequency
579 <p>These are fields 3 (time) and 4 (frequency) from the loopstats log
584 ret
= {'html': VizStats
.table_head
+ stats
.table
+
585 stats_f
.table
+ VizStats
.table_tail
+ exp
,
586 'plot': plot_template
+ plot_data
+ plot_data
,
587 'stats': [stats
, stats_f
],
588 'title': "Local Clock Time/Frequency Offsets"}
591 def local_freq_temps_plot(self
):
592 "Generate gnuplot code graphing local frequency and temps"
593 if not self
.loopstats
:
594 sys
.stderr
.write("ntpviz: WARNING: no loopstats to graph\n")
597 tempsmap
= self
.tempssplit()
598 tempslist
= list(tempsmap
.keys())
600 if not tempsmap
or not tempslist
:
601 sys
.stderr
.write("ntpviz: WARNING: no temps to graph\n")
604 # speed up by only sending gnuplot the data it will actually use
605 # fields: time, freq offset
606 (plot_data
, values_f
) = self
.plot_slice(self
.loopstats
, 3)
608 # compute frequency offset
609 stats_f
= VizStats(values_f
, "Local Clock Frequency Offset", freq
=1)
616 for key
in tempslist
:
617 # speed up by only sending gnuplot the data it will actually use
619 (p
, v
) = self
.plot_slice(tempsmap
[key
], 3)
621 s
= VizStats(v
, 'Temp %s' % key
, units
='°C')
622 max_temp
= max(s
.percs
["max_y"], max_temp
)
623 min_temp
= min(s
.percs
["min_y"], min_temp
)
630 out
["clipped"] = " (clipped)"
633 out
["fmt_f"] = stats_f
.percs
["fmt"]
634 out
['fmt'] = gnuplot_fmt(min_temp
, max_temp
)
635 out
["max_y2"] = stats_f
.percs
["max_y"]
636 out
["min_y2"] = stats_f
.percs
["min_y"]
637 out
["multiplier_f"] = stats_f
.percs
["multiplier"]
638 out
["sitename"] = self
.sitename
639 out
['size'] = args
.img_size
640 out
['terminal'] = args
.terminal
642 out
["unit_f"] = stats_f
.percs
["unit"]
645 # set yrange [%(min_y)s:%(max_y)s]
646 plot_template
= NTPViz
.Common
+ """\
647 set terminal %(terminal)s size %(size)s
648 set title "%(sitename)s: Local Frequency Offset/Temps%(clipped)s"
649 set ytics format "%(fmt)s %(unit)s" nomirror textcolor rgb '#0060ad'
650 set y2tics format "%(fmt_f)s %(unit_f)s" nomirror textcolor rgb '#dd181f'
651 set y2range [%(min_y2)s:%(max_y2)s]
653 set style line 1 lc rgb '#dd181f' lt 1 lw 1 pt 5 ps 0 # --- red
655 "-" using 1:($2*%(multiplier_f)s) title "frequency offset %(unit_f)s" \
656 with linespoints ls 1 axis x1y2, \
659 for key
in tempslist
:
661 plot_template
+= "'-' using 1:2 title '%(key)s' with line, \\\n" \
664 # strip trailing ", \n"
665 plot_template
= plot_template
[:-4] + "\n"
668 <p>The frequency offsets and temperatures.
669 Showing frequency offset (red, in parts
670 per million, scale on right) and the temperatures.</p>
672 <p>These are field 4 (frequency) from the loopstats log
673 file, and field 3 from the tempstats log file.</p>
677 ret
= {'html': VizStats
.table_head
+ stats_f
.table
+
678 table
+ VizStats
.table_tail
+ exp
,
679 'plot': plot_template
+ plot_data
+ plot_data_t
,
681 'title': "Local Frequency/Temp"}
684 def local_temps_gnuplot(self
):
685 "Generate gnuplot code graphing local temperature statistics"
686 sitename
= self
.sitename
687 tempsmap
= self
.tempssplit()
688 tempslist
= list(tempsmap
.keys())
691 if not tempsmap
or not tempslist
:
692 sys
.stderr
.write("ntpviz: WARNING: no temps to graph\n")
699 for key
in tempslist
:
700 # speed up by only sending gnuplot the data it will actually use
702 (p
, v
) = self
.plot_slice(tempsmap
[key
], 3)
703 s
= VizStats(v
, 'Temp %s' % key
, units
='°C')
704 max_temp
= max(s
.percs
["max_y"], max_temp
)
705 min_temp
= min(s
.percs
["min_y"], min_temp
)
709 out
['fmt'] = gnuplot_fmt(min_temp
, max_temp
)
710 out
['sitename'] = sitename
711 out
['size'] = args
.img_size
712 out
['terminal'] = args
.terminal
714 plot_template
= NTPViz
.Common
+ """\
715 set terminal %(terminal)s size %(size)s
716 set title "%(sitename)s: Local Temperatures"
717 set ytics format "%(fmt)s °C" nomirror textcolor rgb '#0060ad'
718 set style line 1 lc rgb '#0060ad' lt 1 lw 1 pt 7 ps 0 # --- blue
723 for key
in tempslist
:
725 plot_template
+= "'-' using 1:2 title '%(key)s' with line, \\\n" \
728 # strip the trailing ", \n"
729 plot_template
= plot_template
[:-4] + "\n"
731 <p>Local temperatures. These will be site-specific depending upon what
732 temperature sensors you collect data from.
733 Temperature changes affect the local clock crystal frequency and
734 stability. The math of how temperature changes frequency is
735 complex, and also depends on crystal aging. So there is no easy
736 way to correct for it in software. This is the single most important
737 component of frequency drift.</p>
738 <p>The Local Temperatures are from field 3 from the tempstats log file.</p>
741 ret
= {'html': exp
, 'stats': stats
}
742 ret
['title'] = "Local Temperatures"
743 ret
['plot'] = plot_template
+ plot_data
746 def local_gps_gnuplot(self
):
747 "Generate gnuplot code graphing local GPS statistics"
748 sitename
= self
.sitename
749 gpsmap
= self
.gpssplit()
750 gpslist
= list(gpsmap
.keys())
753 if not gpsmap
or not gpslist
:
754 if 1 <= args
.debug_level
:
755 sys
.stderr
.write("ntpviz: INFO: no GPS data to graph\n")
758 # build the output dictionary, because Python can not format
764 # fields: time, TDOP, nSats
765 (ps
, values_tdop
, values_nsat
) = self
.plot_slice(gpsmap
[key
], 3, 4)
768 stats
= VizStats(values_nsat
, "nSats", units
='nSat')
769 stats_tdop
= VizStats(values_tdop
, "TDOP", units
=' ')
771 out
= stats_tdop
.percs
772 out
['sitename'] = sitename
773 out
['size'] = args
.img_size
774 if out
['min_y'] == out
['max_y']:
775 # some GPS always output the same TDOP
776 if 0 == out
['min_y']:
781 out
['min_y'] = out
['max_y'] * 0.8
782 out
['max_y'] = out
['max_y'] * 1.2
783 elif 2 > out
['min_y']:
788 out
['fmt'] = gnuplot_fmt(out
["min_y"], out
["max_y"])
789 out
['terminal'] = args
.terminal
791 plot_template
= NTPViz
.Common
+ """\
792 set terminal %(terminal)s size %(size)s
793 set title "%(sitename)s: Local GPS%(clipped)s
794 set ytics format "%(fmt)s TDOP" nomirror textcolor rgb '#0060ad'
795 set yrange [%(min_y)s:%(max_y)s]
796 set y2tics format "%%4.1f nSat" nomirror textcolor rgb '#dd181f'
797 set style line 1 lc rgb '#0060ad' lt 1 lw 1 pt 7 ps 0 # --- blue
798 set style line 2 lc rgb '#dd181f' lt 1 lw 1 pt 5 ps 0 # --- red
804 plot_template
+= """\
805 '-' using 1:2 title '%s TDOP' with line ls 1, \\
806 '-' using 1:3 title '%s nSat' with line ls 2 axis x1y2, \\
809 # strip the trailing ", \\n"
810 plot_template
= plot_template
[:-4] + "\n"
812 <p>Local GPS. The Time Dilution of Precision (TDOP) is plotted in blue.
813 The number of visible satellites (nSat) is plotted in red.</p>
814 <p>TDOP is field 3, and nSats is field 4, from the gpsd log file. The
815 gpsd log file is created by the ntploggps program.</p>
816 <p>TDOP is a dimensionless error factor. Smaller numbers are better.
817 TDOP ranges from 1 (ideal), 2 to 5 (good), to greater than 20 (poor).
818 Some GNSS receivers report TDOP less than one which is theoretically
822 ret
= {'html': VizStats
.table_head
+ stats
.table
+
823 stats_tdop
.table
+ VizStats
.table_tail
+ exp
,
824 'stats': [stats
, stats_tdop
],
825 'title': "Local GPS",
826 'plot': plot_template
+ plot_data
+ plot_data
}
829 def local_error_gnuplot(self
):
830 "Plot the local clock frequency error."
831 if not self
.loopstats
:
832 sys
.stderr
.write("ntpviz: WARNING: no loopstats to graph\n")
835 # grab and sort the values, no need for the timestamp, etc.
837 # speed up by only sending gnuplot the data it will actually use
838 # fields: time, freq error
839 (plot_data
, values
) = self
.plot_slice(self
.loopstats
, 3)
841 # compute frequency offset
842 stats
= VizStats(values
, "Local Clock Frequency Offset", freq
=1,)
844 # build the output dictionary, because Python can not format
847 out
["fmt"] = stats
.percs
["fmt"]
848 out
["sitename"] = self
.sitename
849 out
['size'] = args
.img_size
850 out
['terminal'] = args
.terminal
852 plot_template
= NTPViz
.Common
+ """\
853 set terminal %(terminal)s size %(size)s
854 set title "%(sitename)s: Local Clock Frequency Offset%(clipped)s"
855 set ytics format "%(fmt)s %(unit)s" nomirror
856 set yrange [%(min_y)s:%(max_y)s]
858 set style line 1 lc rgb '#0060ad' lt 1 lw 1 pt 7 ps 0 # --- blue
859 set style line 2 lc rgb '#dd181f' lt 1 lw 1 pt 5 ps 0 # --- red
861 "-" using 1:($2 * %(multiplier)s) title "local clock error" \
862 with linespoints ls 2, \
863 %(p99)s title "99th percentile", \
864 %(p95)s title "95th percentile", \
865 %(p5)s title "5th percentile", \
866 %(p1)s title "1st percentile"
870 <p>The frequency offset of the local clock (aka drift). The
871 graph includes percentile data to show how much the frequency changes
872 over a longer period of time. The majority of this change should come
873 from temperature changes (ex: HVAC, the weather, CPU usage causing local
876 <p>Smaller changes are better. An ideal result would be a horizontal
877 line at 0ppm. Expected values of 99%-1% percentiles: 0.4ppm</p>
879 <p>The Frequency Offset comes from field 4 of the loopstats log file.</p>
881 ret
= {'html': VizStats
.table_head
+ stats
.table
+
882 VizStats
.table_tail
+ exp
,
883 'plot': plot_template
+ plot_data
,
885 'title': "Local Clock Frequency Offset"}
888 def loopstats_gnuplot(self
, fld
, title
, legend
, freq
):
889 "Generate gnuplot code of a given loopstats field"
890 if not self
.loopstats
:
891 sys
.stderr
.write("ntpviz: WARNING: no loopstats to graph\n")
894 # speed up by only sending gnuplot the data it will actually use
896 (plot_data
, values
) = self
.plot_slice(self
.loopstats
, fld
)
899 stats
= VizStats(values
, title
, freq
=freq
)
901 # build the output dictionary, because Python can not format
905 out
["fmt"] = stats
.percs
["fmt"]
906 out
["legend"] = legend
908 out
["sitename"] = self
.sitename
909 out
['size'] = args
.img_size
910 out
['terminal'] = args
.terminal
914 <p>The RMS Frequency Jitter (aka wander) of the local
915 clock's frequency. In other words, how fast the local clock changes
918 <p>Lower is better. An ideal clock would be a horizontal line at
921 <p> RMS Frequency Jitter is field 6 in the loopstats log file.</p>
925 <p>The RMS Jitter of the local clock offset. In other words,
926 how fast the local clock offset is changing.</p>
928 <p>Lower is better. An ideal system would be a horizontal line at 0μs.</p>
930 <p>RMS jitter is field 5 in the loopstats log file.</p>
933 plot_template
= NTPViz
.Common
+ """\
934 set terminal %(terminal)s size %(size)s
935 set title "%(sitename)s: %(title)s%(clipped)s"
936 set ytics format "%(fmt)s %(unit)s" nomirror
937 set yrange [%(min_y)s:%(max_y)s]
939 set style line 1 lc rgb '#0060ad' lt 1 lw 1 pt 7 ps 0 # --- blue
940 set style line 2 lc rgb '#dd181f' lt 1 lw 1 pt 5 ps 0 # --- red
942 "-" using 1:($2*%(multiplier)s) title "%(legend)s" with linespoints ls 1, \
943 %(p99)s title "99th percentile", \
944 %(p95)s title "95th percentile", \
945 %(p5)s title "5th percentile", \
946 %(p1)s title "1st percentile"
949 ret
= {'html': VizStats
.table_head
+ stats
.table
+
950 VizStats
.table_tail
+ exp
,
951 'plot': plot_template
+ plot_data
,
956 def local_offset_jitter_gnuplot(self
):
957 "Generate gnuplot code of local clock loop standard deviation"
958 return self
.loopstats_gnuplot(4, "Local RMS Time Jitter", "Jitter", 0)
960 def local_offset_stability_gnuplot(self
):
961 "Generate gnuplot code graphing local clock stability"
962 return self
.loopstats_gnuplot(5, "Local RMS Frequency Jitter",
965 def peerstats_gnuplot(self
, peerlist
, fld
, title
, ptype
):
966 "Plot a specified field from peerstats."
968 peerdict
= self
.peersplit()
970 peerlist
= list(peerdict
.keys())
972 sys
.stderr
.write("ntpviz: WARNING: no server data to graph\n")
974 peerlist
.sort() # For stability of output
975 namelist
= [] # peer names
979 # Trickiness - we allow peerlist elements to be DNS names.
980 # The socket.gethostbyname() call maps DNS names to IP addresses,
981 # passing through literal IPv4 addresses unaltered. However,
982 # it barfs on either literal IPv6 addresses or refclock names.
984 ip
= socket
.gethostbyname(key
)
989 # socket.getfqdn() is also flakey...
990 namelist
.append(socket
.getfqdn(key
))
995 # can this ever happen?
996 sys
.stderr
.write("ntpviz: ERROR: No such server as %s" % key
)
1002 if len(peerlist
) == 1:
1004 if "offset" == ptype
:
1005 # doing offset, not jitter
1007 if peerlist
[0].startswith("127.127."):
1008 # don't do rtt for reclocks
1010 title
= "Refclock Offset " + str(peerlist
[0])
1012 <p>The offset of a local refclock in seconds. This is
1013 useful to see how the measured offset is behaving.</p>
1015 <p>Closer to 0s is better. An ideal system would be a horizontal line
1016 at 0s. Typical 90% ranges may be: local serial GPS 200 ms; local PPS
1019 <p>Clock Offset is field 5 in the peerstats log file.</p>
1022 title
= "Server Offset " + str(peerlist
[0])
1024 <p>The offset of a server in seconds. This is
1025 useful to see how the measured offset is behaving.</p>
1027 <p>The chart also plots offset±rtt, where rtt is the round trip time
1028 to the server. NTP can not really know the offset of a remote chimer,
1029 NTP computes it by subtracting rtt/2 from the offset. Plotting the
1030 offset±rtt reverses this calculation to more easily see the effects of
1033 <p>Closer to 0s is better. An ideal system would be a horizontal line
1034 at 0s. Typical 90% ranges may be: local LAN server 80µs; 90% ranges for
1035 WAN server may be 4ms and much larger. </p>
1037 <p>Clock Offset is field 5 in the peerstats log file. The Round Trip
1038 Time (rtt) is field 6 in the peerstats log file.</p>
1042 # doing jitter, not offset
1043 if peerlist
[0].startswith("127.127."):
1044 title
= "Refclock RMS Jitter " + str(peerlist
[0])
1046 <p>The RMS Jitter of a local refclock. Jitter is the
1047 current estimated dispersion, in other words the variation in offset
1048 between samples.</p>
1050 <p>Closer to 0s is better. An ideal system would be a horizontal
1053 <p>RMS Jitter is field 8 in the peerstats log file.</p>
1056 title
= "Server Jitter " + str(peerlist
[0])
1058 <p>The RMS Jitter of a server. Jitter is the
1059 current estimated dispersion, in other words the variation in offset
1060 between samples.</p>
1062 <p>Closer to 0s is better. An ideal system would be a horizontal line
1065 <p>RMS Jitter is field 8 in the peerstats log file.</p>
1068 if namelist
[0] and peerlist
[0] != namelist
[0]:
1069 # append hostname, if we have it
1070 # after stats to keep summary short
1071 title
+= " (%s)" % namelist
[0]
1077 if "offset" == ptype
:
1078 title
= "Server Offsets"
1080 <p>The offset of all refclocks and servers.
1081 This can be useful to see if offset changes are happening in
1082 a single clock or all clocks together.</p>
1084 <p>Clock Offset is field 5 in the peerstats log file.</p>
1087 title
= "Server Jitters"
1089 <p>The RMS Jitter of all refclocks and servers. Jitter is the
1090 current estimated dispersion, in other words the variation in offset
1091 between samples.</p>
1093 <p>Closer to 0s is better. An ideal system would be a horizontal line
1096 <p>RMS Jitter is field 8 in the peerstats log file.</p>
1099 if len(peerlist
) == 1:
1100 if peerlist
[0] in refclock_name
:
1101 title
+= ' ' + refclock_name
[peerlist
[0]]
1105 # 20% speed up by only sending gnuplot the data it will
1108 # fields: time, fld, and rtt
1109 pt
= self
.plot_slice(peerdict
[ip
], fld
, 5)
1113 pt
= self
.plot_slice(peerdict
[ip
], fld
)
1116 stats
= VizStats(pt
[1], title
)
1117 if len(peerlist
) == 1:
1118 percentages
= " %(p50)s title '50th percentile', " % stats
.percs
1120 # skip stats on peers/offsets plots
1121 stats
.skip_summary
= True
1125 out
["fmt"] = stats
.percs
["fmt"]
1126 out
['sitename'] = self
.sitename
1127 out
['size'] = args
.img_size
1128 out
['terminal'] = args
.terminal
1129 out
['title'] = title
1131 if 6 >= len(peerlist
):
1132 out
['set_key'] = "set key top right"
1133 elif 12 >= len(peerlist
):
1135 out
['set_key'] = "set key bmargin"
1137 # too many keys to show
1138 out
['set_key'] = "set key off"
1140 plot_template
= NTPViz
.Common
+ """\
1141 set terminal %(terminal)s size %(size)s
1142 set title "%(sitename)s: %(title)s%(clipped)s"
1144 set ytics format "%(fmt)s %(unit)s" nomirror
1145 set yrange [%(min_y)s:%(max_y)s]
1150 plot_template
+= percentages
1151 for key
in peerlist
:
1152 out
['label'] = self
.ip_label(key
)
1153 plot_template
+= "'-' using 1:($2*%(multiplier)s) " \
1154 " title '%(label)s' with line, \\\n" % out
1157 plot_template
+= """\
1158 '-' using 1:(($2+$3/2)*%(multiplier)s) title 'offset+rtt/2' with line, \\
1159 '-' using 1:(($2-$3/2)*%(multiplier)s) title 'offset-rtt/2' with line
1161 # sadly, gnuplot needs 3 identical copies of the data.
1162 plot_template
+= plot_data
+ plot_data
1164 # strip the trailing ", \n"
1165 plot_template
= plot_template
[:-4] + "\n"
1167 if len(peerlist
) == 1:
1168 # skip stats for multiplots
1169 html
= VizStats
.table_head
+ stats
.table \
1170 + VizStats
.table_tail
+ exp
,
1174 ret
= {'html': html
,
1175 'plot': plot_template
+ plot_data
,
1180 def peer_offsets_gnuplot(self
, peerlist
=None):
1181 "gnuplot Peer Offsets"
1182 return self
.peerstats_gnuplot(peerlist
, 4, "Server Offset",
1185 def peer_jitters_gnuplot(self
, peerlist
=None):
1186 "gnuplot Peer Jitters"
1187 return self
.peerstats_gnuplot(peerlist
, 7, "Server Jitter",
1190 def local_offset_histogram_gnuplot(self
):
1191 "Plot a histogram of clock offset values from loopstats."
1192 if not self
.loopstats
:
1193 sys
.stderr
.write("ntpviz: WARNING: no loopstats to graph\n")
1196 # TODO normalize to 0 to 100?
1198 # grab and sort the values, no need for the timestamp, etc.
1199 values
= [float(row
[2]) for row
in self
.loopstats
]
1200 stats
= VizStats(values
, 'Local Clock Offset')
1202 out
["fmt_x"] = stats
.percs
["fmt"]
1203 out
['sitename'] = self
.sitename
1205 out
['min_x'] = out
['min_y']
1206 out
['max_x'] = out
['max_y']
1208 rnd1
= 7 # round to 100 ns boxes
1209 out
['boxwidth'] = 1e-7
1211 # between -10us and 10us
1212 if 1e-5 > stats
.percs
["p99"] and -1e-5 < stats
.percs
["p1"]:
1214 rnd1
= 9 # round to 1 ns boxes
1215 out
['boxwidth'] = S_PER_NS
1217 # Python 2.6 has no collections.Counter(), so fake it.
1218 cnt
= collections
.defaultdict(int)
1219 for value
in values
:
1221 # for a +/- 50 microSec range that is 1,000 buckets to plot
1222 cnt
[round(value
, rnd1
)] += 1
1226 if stats
.percs
['p1sigma'] > stats
.percs
['p99'] or \
1227 stats
.percs
['m1sigma'] < stats
.percs
['p1']:
1228 # sigma out of range, do not plot
1233 # plus/minus of one sigma range
1235 set style arrow 1 nohead
1236 set linestyle 1 linecolor rgb "#009900"
1237 set arrow from %(m1sigma)s,graph 0 to %(m1sigma)s,graph 0.90 as 1 ls 1
1238 set arrow from %(p1sigma)s,graph 0 to %(p1sigma)s,graph 0.90 as 1 ls 1
1239 set label 1 "-1σ" at %(m1sigma)s, graph 0.96 left front offset -1,-1 \
1240 textcolor rgb "#009900"
1241 set label 2 "+1σ" at %(p1sigma)s, graph 0.96 left front offset -1,-1 \
1242 textcolor rgb "#009900"
1245 out
['size'] = args
.img_size
1246 out
['terminal'] = args
.terminal
1248 # in 2016, 25% of screens are 1024x768, 42% are 1388x768
1249 # but leave some room for the browser frame
1250 plot_template
= '''\
1251 set terminal %(terminal)s size %(size)s
1252 set title "%(sitename)s: Local Clock Time Offset Histogram%(clipped)s"
1254 set boxwidth %(boxwidth)s
1255 set xtics format "%(fmt_x)s %(unit)s" nomirror
1256 set xrange [%(min_x)s:%(max_x)s]
1258 set style arrow 3 nohead
1259 set arrow from %(p99)s,graph 0 to %(p99)s,graph 0.30 as 3
1260 set style arrow 4 nohead
1261 set arrow from %(p95)s,graph 0 to %(p95)s,graph 0.45 as 4
1262 set style arrow 5 nohead
1263 set arrow from %(p5)s,graph 0 to %(p5)s,graph 0.45 as 5
1264 set style arrow 6 nohead
1265 set arrow from %(p1)s,graph 0 to %(p1)s,graph 0.30 as 6
1269 set style fill solid 0.5
1270 set label 3 "99%%" at %(p99)s, graph 0.35 left front offset -1,-1
1271 set label 4 "95%%" at %(p95)s, graph 0.50 left front offset -1,-1
1272 set label 5 "1%%" at %(p1)s, graph 0.35 left front offset -1,-1
1273 set label 6 "5%%" at %(p5)s, graph 0.50 left front offset -1,-1
1276 "-" using ($1 * %(multiplier)s):2 title "histogram" with boxes
1279 histogram_data
= ["%s %s\n" % (k
, v
) for k
, v
in cnt
.items()]
1282 <p>The clock offsets of the local clock as a histogram.</p>
1284 <p>The Local Clock Offset is field 3 from the loopstats log file.</p>
1286 # don't return stats, it's just a dupe
1287 ret
= {'html': VizStats
.table_head
+ stats
.table
+
1288 VizStats
.table_tail
+ exp
,
1289 'plot': plot_template
+ "".join(histogram_data
) + "e\n",
1291 'title': "Local Clock Time Offset Histogram"}
1295 # Multiplotting can't live inside NTPViz because it consumes a list
1296 # of such objects, not a single one.
1299 def local_offset_multiplot(statlist
):
1300 "Plot comparative local offsets for a list of NTPViz objects."
1303 out
['size'] = args
.img_size
1304 out
['terminal'] = args
.terminal
1306 plot
= NTPViz
.Common
+ '''\
1307 set terminal %(terminal)s size %(size)s
1308 set title "Multiplot Local Clock Offsets"
1309 set ytics format "%%1.2f μs" nomirror textcolor rgb "#0060ad"
1310 set key bottom right box
1313 # FIXME: probably need to be more flexible about computing the plot label
1314 sitenames
= [os
.path
.basename(os
.path
.dirname(dr
))
1315 for dr
in args
.statsdirs
]
1316 for (i
, stats
) in enumerate(statlist
):
1317 plot
+= '"-" using 1:($2*1000000) title "%s clock offset μs" ' \
1318 'with linespoints, \\\n' % (sitenames
[i
])
1319 plot
= plot
[:-4] + "\n"
1322 for stats
in statlist
:
1323 # speed up by only sending gnuplot the data it will actually use
1324 # fields: time, offset
1325 pt
= stats
.plot_slice(stats
.loopstats
, 2)
1328 ret
= {'html': '', 'stats': []}
1329 ret
['title'] = "Multiplot"
1330 ret
['plot'] = plot
+ plot_data
1334 # here is how to create the base64 from an image file:
1335 # with open("path/to/file.png", "rb") as f:
1337 # print data.encode("base64")
1340 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
1373 AAAA/wAAAP8AAAD/AAAAAAAAAP+fn59gn5+fYJ+fn2Cfn59gn5+fYJ+fn2Cfn59gn5+fYJ+fn2Cf
1374 n59gn5+fYJ+fn2B/f39/AAAA/wAAAAAAAAAAAAAA/5+fn2Cfn59gn5+fYJ+fn2Cfn59gn5+fYJ+f
1375 n2Cfn59gn5+fYJ+fn2Cfn59gAAAA/wAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA
1376 /wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAA/5+fn2Cfn59g
1377 n5+fYJ+fn2Cfn59gn5+fYJ+fn2Cfn59gn5+fYAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+f
1378 n59gn5+fYAAAAP8AAAD/AAAA/wAAAP8AAAD/n5+fYJ+fn2AAAAD/AAAAAAAAAAAAAAAAAAAAAAAA
1379 AAAAAAD/n5+fYAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fn59gAAAA/wAAAAAAAAAAAAAA
1380 AAAAAAAAAAAAAAAA/5+fn2AAAAD/AAAAAAAAAAAAAAD/AAAA/01NTWAAAAD/n5+fYAAAAP8AAAAA
1381 AAAAAAAAAAAAAAAAAAAAAAAAAP+fn59gAAAA/wAAAAAAAAAAAAAA/wAAAAAAAAAAAAAA/5+fn2AA
1382 AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/n5+fYAAAAP8AAAAAAAAAAE1NTWAAAAAAAAAAAAAA
1383 AP+fn59gAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/5+fn2Cfn59gAAAA/wAAAP8AAAD/AAAA
1384 /wAAAP+fn59gn5+fYAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAEBAf+fn59gn5+fYJ+fn2Cfn59g
1385 n5+fYJ+fn2Cfn59gn5+fYJ+fn2AAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/wAAAP8A
1386 AAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAA
1387 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAA
1388 /wAAAAAAAAD/AAAA/wAAAP8AAAAAAAAAAAAAAAAAAAD/AAAA/wAAAP8AAAAAAAAA/wAAAAAAAAAA
1389 AAAAAAAAAP8AAAD/AAAA/wAAAAAAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAAAAAAD/AAAA/wAAAP8A
1390 AAAAgAAAAL/+AADf/QAAwAEAAO/7AADsGwAA6+sAAOsrAADrawAA6+sAAOwbAADv+wAA4AMAAN/9
1394 if __name__
== '__main__':
1395 bin_ver
= "ntpsec-@NTPSEC_VERSION_EXTENDED@"
1396 ntp
.util
.stdversioncheck(bin_ver
)
1398 parser
= MyArgumentParser(description
="ntpd stats visualizer",
1399 fromfile_prefix_chars
='@',
1401 You can place command line options in a file, one per line.
1403 See the manual page for details.
1405 Python by ESR, concept and gnuplot code by Dan Drown.
1407 group
= parser
.add_mutually_exclusive_group()
1409 parser
.add_argument('-c', '--clip',
1410 action
="store_true",
1412 help="Clip plots at 1%% and 99%%")
1413 parser
.add_argument('-d', '--datadir',
1414 default
="/var/log/ntpstats",
1416 help="one or more log file directories to read",
1418 parser
.add_argument('-D', '--debug',
1421 help="debug level, 0 (none) to 9 (most)",
1423 parser
.add_argument('-e', '--endtime',
1425 help="End time in POSIX (seconds) or ISO 8601",
1427 parser
.add_argument('-g', '--generate',
1428 action
="store_true",
1430 help="Run through gnuplot to make plot images")
1431 parser
.add_argument('-n', '--name',
1432 default
=socket
.getfqdn(),
1434 help="sitename (title)",
1436 # some OS do not support os.nice()
1439 parser
.add_argument('-N', '--nice',
1440 action
="store_true",
1442 help="Run as lowest priority")
1446 parser
.add_argument('-o', '--outdir',
1447 default
="ntpgraphs",
1449 help="output directory",
1451 parser
.add_argument('-p', '--period',
1452 default
=7, # default to 7 days
1454 help="period in days to graph (float)",
1456 parser
.add_argument('-s', '--starttime',
1458 help="Start time in POSIX (seconds) or ISO 8601",
1460 parser
.add_argument('-T', '--terminal',
1463 help="gnuplot terminal type for graphs",
1465 parser
.add_argument('-w', '--width',
1466 choices
=['s', 'm', 'l'],
1469 help="Image width: s, m, or l",
1471 group
.add_argument('--all-peer-jitters',
1472 action
="store_true",
1473 dest
='show_peer_jitters',
1474 help="Plot all peer jitters")
1475 group
.add_argument('--all-peer-offsets',
1476 action
="store_true",
1477 dest
='show_peer_offsets',
1478 help="Plot all peer offsets")
1479 group
.add_argument('--local-error',
1480 action
="store_true",
1481 dest
='show_local_error',
1482 help="Plot local clock frequency offsets")
1483 group
.add_argument('--local-freq-temps',
1484 action
="store_true",
1485 dest
='show_freq_temps',
1486 help="Plot local frequency vs temperature data")
1487 group
.add_argument('--local-gps',
1488 action
="store_true",
1490 help="Plot gpsd TDOP and nSats")
1491 group
.add_argument('--local-jitter',
1492 action
="store_true",
1493 dest
='show_local_jitter',
1494 help="Plot clock time jitter")
1495 group
.add_argument('--local-offset',
1496 action
="store_true",
1497 dest
='show_local_offset',
1498 help="Plot Clock frequency offset")
1499 group
.add_argument('--local-offset-histogram',
1500 action
="store_true",
1501 dest
='show_local_offset_histogram',
1502 help="Plot histogram of loopstats time offsets")
1503 group
.add_argument('--local-offset-multiplot',
1504 action
="store_true",
1505 dest
='show_local_offset_multiplot',
1506 help="Plot comparative local offsets for "
1507 "multiple directories")
1508 group
.add_argument('--local-stability',
1509 action
="store_true",
1510 dest
='show_local_stability',
1511 help="Plot RMS frequency-jitter")
1512 group
.add_argument('--local-temps',
1513 action
="store_true",
1515 help="Plot local temperature data")
1516 group
.add_argument('--peer-jitters',
1518 dest
='peer_jitters',
1519 help="Plot peer jitters. Comma separated host list.",
1521 group
.add_argument('--peer-offsets',
1523 dest
='peer_offsets',
1524 help="Plot peer offsets. Comma separated host list.",
1526 parser
.add_argument('-V', '--version',
1528 version
="ntpviz %s" % ntp
.util
.stdversion())
1530 args
= parser
.parse_args()
1533 # run at lowest possible priority
1535 if args
.debug_level
:
1536 sys
.stderr
.write("ntpviz: INFO: Now running at nice %s\n" % nice
)
1538 if 's' == args
.width
:
1539 # fit in 1024x768 browser
1540 # in 2016 this is 22% of all browsers
1541 args
.img_size
= '1000,720'
1542 elif 'l' == args
.width
:
1543 # fit in 1920x1080 browser
1544 args
.img_size
= '1850,1000'
1546 # fit in 1388x768 browser
1547 # in 2016 this is 42% of all browsers
1548 args
.img_size
= '1340,720'
1550 # figure out plot image file extension
1551 term_map
= {'gif': '.gif',
1557 if args
.terminal
in term_map
:
1558 args
.img_ext
= term_map
[args
.terminal
]
1560 sys
.stderr
.write("ntpviz: ERROR: Unknown terminal type: %s\n" %
1564 args
.period
= int(float(args
.period
) * ntp
.statfiles
.NTPStats
.SecondsInDay
)
1565 if args
.endtime
is not None:
1566 args
.endtime
= ntp
.statfiles
.iso_to_posix(args
.endtime
)
1567 if args
.starttime
is not None:
1568 args
.starttime
= ntp
.statfiles
.iso_to_posix(args
.starttime
)
1570 args
.statsdirs
= [os
.path
.expanduser(path
)
1571 for path
in args
.statsdirs
.split(",")]
1573 if args
.show_peer_offsets
:
1574 args
.show_peer_offsets
= []
1575 elif args
.peer_offsets
:
1576 args
.show_peer_offsets
= args
.peer_offsets
.split(",")
1578 args
.show_peer_offsets
= None
1580 if args
.show_peer_jitters
:
1581 args
.show_peer_jitters
= []
1582 elif args
.peer_jitters
:
1583 args
.show_peer_jitters
= args
.peer_jitters
.split(",")
1585 args
.show_peer_jitters
= None
1587 if 0 < args
.debug_level
:
1588 sys
.stderr
.write("ntpviz: INFO: now running at debug: %s\n" %
1590 sys
.stderr
.write("ntpviz: INFO: Version: %s\n" % ntp
.util
.stdversion())
1591 sys
.stderr
.write("ntpviz: INFO: Parsed Options %s\n" % args
)
1593 if 9 == args
.debug_level
:
1594 # crazy debug, also profile
1596 pr
= cProfile
.Profile()
1599 # register to dump debug on all normal exits
1600 atexit
.register(print_profile
)
1602 nice
= 19 # always run nicely
1606 # set ionice() to idle
1607 p
= psutil
.Process(os
.getpid())
1609 p
.ionice(psutil
.IOPRIO_CLASS_IDLE
)
1610 except AttributeError:
1611 sys
.stderr
.write("ntpviz: INFO: ionice is not available\n")
1614 if 0 < args
.debug_level
:
1615 sys
.stderr
.write("ntpviz: INFO: psutil not found\n")
1618 nice
= os
.nice(nice
)
1619 if 2 < args
.debug_level
:
1620 sys
.stderr
.write("ntpviz: INFO: now running at nice: %s\n" % nice
)
1622 for fontpath
in ("@PREFIX@/share/fonts/liberation",
1623 "@PREFIX@/share/fonts/liberation-fonts",
1624 "@PREFIX@/share/fonts/truetype/liberation",
1625 "/usr/share/fonts/liberation",
1626 "/usr/share/fonts/liberation-fonts",
1627 "/usr/share/fonts/truetype/liberation"):
1629 if os
.path
.exists(fontpath
):
1630 os
.environ
["GDFONTPATH"] = fontpath
1634 "ntpviz: WARNING: liberation truetype fonts not found\n")
1635 os
.environ
["GNUPLOT_DEFAULT_GDFONT"] = "LiberationSans-Regular"
1639 if 1 == len(args
.statsdirs
):
1640 statlist
= [NTPViz(statsdir
=args
.statsdirs
[0], sitename
=args
.sitename
,
1641 period
=args
.period
, starttime
=args
.starttime
,
1642 endtime
=args
.endtime
)]
1644 statlist
= [NTPViz(statsdir
=d
, sitename
=d
,
1645 period
=args
.period
, starttime
=args
.starttime
,
1646 endtime
=args
.endtime
)
1647 for d
in args
.statsdirs
]
1649 if len(statlist
) == 1:
1652 if args
.show_local_offset
or \
1653 args
.show_local_error
or \
1654 args
.show_local_jitter
or \
1655 args
.show_local_stability
or \
1656 args
.show_local_offset_histogram
:
1657 if not stats
.loopstats
:
1658 sys
.stderr
.write("ntpviz: ERROR: missing loopstats data\n")
1661 if args
.show_local_offset
:
1662 plot
= stats
.local_offset_gnuplot()
1663 elif args
.show_local_error
:
1664 plot
= stats
.local_error_gnuplot()
1665 elif args
.show_local_jitter
:
1666 plot
= stats
.local_offset_jitter_gnuplot()
1667 elif args
.show_local_stability
:
1668 plot
= stats
.local_offset_stability_gnuplot()
1669 elif args
.show_local_offset_histogram
:
1670 plot
= stats
.local_offset_histogram_gnuplot()
1672 if args
.show_peer_offsets
is not None or \
1673 args
.show_peer_jitters
is not None:
1674 if not stats
.peerstats
:
1675 sys
.stderr
.write("ntpviz: ERROR: missing peerstats data\n")
1677 if args
.show_peer_offsets
is not None:
1678 plot
= stats
.peer_offsets_gnuplot(args
.show_peer_offsets
)
1679 if args
.show_peer_jitters
is not None:
1680 plot
= stats
.peer_jitters_gnuplot(args
.show_peer_jitters
)
1682 if args
.show_freq_temps
:
1684 sys
.stderr
.write("ntpviz: ERROR: missing temps data\n")
1686 plot
= stats
.local_freq_temps_plot()
1690 sys
.stderr
.write("ntpviz: ERROR: missing temps data\n")
1692 plot
= stats
.local_temps_gnuplot()
1696 sys
.stderr
.write("ntpviz: ERROR: missing gps data\n")
1698 plot
= stats
.local_gps_gnuplot()
1700 if args
.show_local_offset_multiplot
:
1701 plot
= local_offset_multiplot(statlist
)
1703 if plot
is not None:
1704 # finish up the plot, and exit
1706 gnuplot(plot
['plot'])
1708 sys
.stdout
.write(plot
['plot'])
1711 # Fall through to HTML code generation
1712 if not os
.path
.isdir(args
.outdir
):
1714 os
.mkdir(args
.outdir
)
1716 sys
.stderr
.write("ntpviz: ERROR: %s can't be created.\n"
1720 # if no ntpsec favicon.ico, write one.
1721 ico_filename
= os
.path
.join(args
.outdir
, "favicon.ico")
1722 if not os
.path
.lexists(ico_filename
):
1723 with
open(ico_filename
, "wb") as wp
:
1724 wp
.write(binascii
.a2b_base64(ntpsec_ico
))
1726 # if no ntpsec logo, write one.
1727 logo_filename
= os
.path
.join(args
.outdir
, "ntpsec-logo.png")
1728 if not os
.path
.lexists(logo_filename
):
1729 with
open(logo_filename
, "wb") as wp
:
1730 wp
.write(binascii
.a2b_base64(ntpsec_logo
))
1732 # report_time = datetime.datetime.utcnow() # the time now is...
1733 report_time
= datetime
.datetime
.now(UTC()) # the time now is...
1734 report_time
= report_time
.strftime("%c %Z") # format it nicely
1736 title
= args
.sitename
1742 <link rel="shortcut icon" href="favicon.ico">
1743 <meta charset="UTF-8">
1744 <meta http-equiv="refresh" content="1800">
1745 <meta name="expires" content="0">
1746 <title>%(title)s</title>
1754 margin-bottom: 10px;
1759 border-collapse: collapse;
1765 vertical-align: top;
1767 tbody tr:nth-child(6n+4),
1768 tbody tr:nth-child(6n+5),
1769 tbody tr:nth-child(6n+6) {
1770 background-color: rgba(0,255,0,0.2);
1774 text-decoration: none;
1776 .section .site-title:visited {
1782 <div style="width:910px">
1783 <a href='https://www.ntpsec.org/'>
1784 <img src="ntpsec-logo.png" alt="NTPsec" style="float:left;margin:20px 70px;">
1787 <h1 style="margin-bottom:10px;">%(title)s</h1>
1788 <b>Report generated:</b> %(report_time)s <br>
1791 # Ugh. Not clear what to do in the multiplot case
1792 if len(statlist
) == 1:
1793 start_time
= datetime
.datetime
.utcfromtimestamp(
1794 stats
.starttime
).strftime('%c')
1795 end_time
= datetime
.datetime
.utcfromtimestamp(
1796 stats
.endtime
).strftime('%c')
1798 index_header
+= '<b>Start Time:</b> %s UTC<br>\n' \
1799 '<b>End Time:</b> %s UTC<br>\n' \
1800 % (start_time
, end_time
)
1801 if 1 > stats
.period
:
1802 # less than a day, report hours
1803 index_header
+= ('<b>Report Period:</b> %1.1f hours <br>\n' %
1804 (float(stats
.period
) / (24 * 60)))
1806 # more than a day, report days
1807 index_header
+= ('<b>Report Period:</b> %1.1f days <br>\n' %
1808 (float(stats
.period
) /
1809 ntp
.statfiles
.NTPStats
.SecondsInDay
))
1812 index_header
+= """\
1813 <span style="color:red;font-weight:bold;">Warning: plots clipped</span><br>
1816 index_header
+= '</div>\n<div style="clear:both;"></div>'
1818 index_trailer
= '''\
1822 <dt>frequency offset:</dt>
1823 <dd>The difference between the ntpd calculated frequency and the local
1824 system clock frequency (usually in parts per million, ppm)</dd>
1826 <dt>jitter, dispersion:</dt>
1827 <dd>The short term change in a value. NTP measures Local Time Jitter,
1828 Refclock Jitter, and Server Jitter in seconds. Local Frequency Jitter is
1832 <dt>kurtosis, Kurt:</dt>
1833 <dd>The kurtosis of a random variable X is the fourth standardized
1834 moment and is a dimension-less ratio. ntpviz uses the Pearson's moment
1835 coefficient of kurtosis. A normal distribution has a kurtosis of three.
1836 NIST describes a kurtosis over three as "heavy tailed" and one under
1837 three as "light tailed".</dd>
1839 <dt>ms, millisecond:</dt>
1840 <dd>One thousandth of a second = 0.001 seconds, 1e-3 seconds</dd>
1843 <dd>The arithmetic mean: the sum of all the values divided by the
1845 The formula for mu is: "mu = (∑x<sub>i</sub>) / N".
1846 Where x<sub>i</sub> denotes the data points and N is the number of data
1849 <dt>ns, nanosecond:</dt>
1850 <dd>One billionth of a second, also one thousandth of a microsecond,
1851 0.000000001 seconds and 1e-9 seconds.</dd>
1853 <dt>percentile:</dt>
1854 <dd>The value below which a given percentage of values fall.</dd>
1856 <dt>ppb, parts per billion:</dt>
1857 <dd>Ratio between two values. These following are all the same:
1858 1 ppb, one in one billion, 1/1,000,000,000, 0.000,000,001, 1e-9 and
1861 <dt>ppm, parts per million:</dt>
1862 <dd>Ratio between two values. These following are all the same:
1863 1 ppm, one in one million, 1/1,000,000, 0.000,001, and 0.000,1%</dd>
1865 <dt>‰, parts per thousand:</dt>
1866 <dd>Ratio between two values. These following are all the same:
1867 1 ‰. one in one thousand, 1/1,000, 0.001, and 0.1%</dd>
1870 <dd>Reference clock, a local GPS module or other local source of time.</dd>
1872 <dt>remote clock:</dt>
1873 <dd>Any clock reached over the network, LAN or WAN. Also called a peer
1876 <dt>time offset:</dt>
1877 <dd>The difference between the ntpd calculated time and the local system
1878 clock's time. Also called phase offset.</dd>
1881 <dd>Sigma denotes the standard deviation (SD) and is centered on the
1882 arithmetic mean of the data set. The SD is simply the square root of
1883 the variance of the data set. Two sigma is simply twice the standard
1884 deviation. Three sigma is three times sigma. Smaller is better.<br>
1885 The formula for sigma is: "σ = √[ ∑(x<sub>i</sub>-mu)^2 / N ]".
1886 Where x<sub>i</sub> denotes the data points and N is the number of data
1889 <dt>skewness, Skew:</dt>
1890 <dd>The skewness of a random variable X is the third standardized
1891 moment and is a dimension-less ratio. ntpviz uses the Pearson's moment
1892 coefficient of skewness. Wikipedia describes it best: "The qualitative
1893 interpretation of the skew is complicated and unintuitive."<br> A normal
1894 distribution has a skewness of zero. </dd>
1896 <dt>upstream clock:</dt>
1897 <dd>Any server or reference clock used as a source of time.</dd>
1899 <dt>µs, us, microsecond:</dt>
1900 <dd>One millionth of a second, also one thousandth of a millisecond,
1901 0.000,001 seconds, and 1e-6 seconds.</dd>
1907 <div style="float:left">
1908 This page autogenerated by
1909 <a href="https://docs.ntpsec.org/latest/ntpviz.html">
1910 ntpviz</a>, part of the <a href="https://www.ntpsec.org/">NTPsec project</a>
1912 <div style="float:left;margin-left:350px;">
1913 <a href="https://validator.w3.org/nu/">
1914 <img src="https://www.w3.org/html/logo/downloads/HTML5_Logo_32.png"
1918 <a href="https://jigsaw.w3.org/css-validator/check/referer">
1919 <img style="border:0;width:88px;height:31px"
1920 src="https://jigsaw.w3.org/css-validator/images/vcss"
1924 <div style="clear:both;"></div>
1929 imagewrapper
= "<img src='%%s%s' alt='%%s plot'>\n" % args
.img_ext
1931 # buffer the index.html output so the index.html is not empty
1933 index_buffer
= index_header
1934 # if header file, add it to index.html
1935 header
= os
.path
.join(args
.outdir
, "header")
1936 if os
.path
.isfile(header
):
1938 header_file
= open(header
, 'r', encoding
='utf-8')
1939 header_txt
= header_file
.read()
1940 index_buffer
+= '<br>\n' + header_txt
+ '\n'
1944 if len(statlist
) > 1:
1945 index_buffer
+= local_offset_multiplot(statlist
)
1947 # imagepairs in the order of the html entries
1949 ("local-offset", stats
.local_offset_gnuplot()),
1950 # skipa next one, redundant to one above
1951 # ("local-error", stats.local_error_gnuplot()),
1952 ("local-jitter", stats
.local_offset_jitter_gnuplot()),
1953 ("local-stability", stats
.local_offset_stability_gnuplot()),
1954 ("local-offset-histogram", stats
.local_offset_histogram_gnuplot()),
1955 ("local-temps", stats
.local_temps_gnuplot()),
1956 ("local-freq-temps", stats
.local_freq_temps_plot()),
1957 ("local-gps", stats
.local_gps_gnuplot()),
1958 ("peer-offsets", stats
.peer_offsets_gnuplot()),
1961 peerlist
= list(stats
.peersplit().keys())
1962 # sort for output order stability
1964 for key
in peerlist
:
1965 imagepairs
.append(("peer-offset-" + key
,
1966 stats
.peer_offsets_gnuplot([key
])))
1968 imagepairs
.append(("peer-jitters",
1969 stats
.peer_jitters_gnuplot()))
1970 for key
in peerlist
:
1971 imagepairs
.append(("peer-jitter-" + key
,
1972 stats
.peer_jitters_gnuplot([key
])))
1975 for (imagename
, image
) in imagepairs
:
1978 if 1 <= args
.debug_level
:
1979 sys
.stderr
.write("ntpviz: plotting %s\n" % image
['title'])
1980 stats
.append(image
['stats'])
1981 # give each H2 an unique ID.
1982 div_id
= image
['title'].lower().replace(' ', '_').replace(':', '_')
1984 index_buffer
+= """\
1985 <div id="%s">\n<h2><a class="section" href="#%s">%s</a></h2>
1986 """ % (div_id
, div_id
, image
['title'])
1988 div_name
= imagename
.replace('-', ' ')
1989 # Windows hates colons in filename
1990 imagename
= imagename
.replace(':', '-')
1991 index_buffer
+= imagewrapper
% (imagename
, div_name
)
1994 index_buffer
+= "<div>\n%s</div>\n" % image
['html']
1995 index_buffer
+= "<br><br>\n"
1996 gnuplot(image
['plot'], os
.path
.join(args
.outdir
,
1997 imagename
+ args
.img_ext
))
1998 index_buffer
+= "</div>\n"
2003 stats_to_output
= {}
2008 if sta
.skip_summary
:
2010 # This removes duplicates
2011 stats_to_output
[sta
.title
] = sta
2013 index_buffer
+= '<div id="Summary">\n' \
2014 '<h2><a class="section" href="#Summary">Summary</a></h2>\n'
2015 index_buffer
+= VizStats
.table_head
2017 for key
in sorted(stats_to_output
.keys()):
2018 index_buffer
+= str(stats_to_output
[key
].table
)
2019 csvs
.append(stats_to_output
[key
].csv
)
2021 # RFC 4180 specifies the mime-type of a csv: text/csv
2022 # your webserver should be programmed the same
2023 index_buffer
+= VizStats
.table_tail
2024 index_buffer
+= """\
2025 <a href="summary.csv" target="_blank"
2026 type="text/csv;charset=UTF-8;header=present">Summary as CSV file</a><br>
2030 # if footer file, add it to index.html
2031 footer
= os
.path
.join(args
.outdir
, "footer")
2032 if os
.path
.isfile(footer
):
2034 footer_file
= open(footer
, 'r', encoding
='utf-8')
2035 footer_txt
= footer_file
.read()
2036 index_buffer
+= '<br>\n' + footer_txt
+ '\n'
2039 index_buffer
+= index_trailer
2041 # and send the file buffer
2042 index_filename
= os
.path
.join(args
.outdir
, "index.html")
2043 with
open(index_filename
+ ".tmp", "w", encoding
='utf-8') as ifile
:
2044 ifile
.write(index_buffer
)
2046 # create csv file, as a tmp file
2047 csv_filename
= os
.path
.join(args
.outdir
, "summary.csv")
2048 with
open(csv_filename
+ ".tmp", "w", encoding
='utf-8') as csv_file
:
2049 csv_ob
= csv
.writer(csv_file
)
2050 csv_ob
.writerow(VizStats
.csv_head
)
2052 csv_ob
.writerow(row
)
2054 # move new index and summary into place
2055 # windows python 2.7, 3.6 has no working rename, so delete and move
2057 os
.remove(csv_filename
)
2058 os
.remove(index_filename
)
2062 os
.rename(csv_filename
+ ".tmp", csv_filename
)
2063 os
.rename(index_filename
+ ".tmp", index_filename
)