1#! @PATH_PERL@ -w
2# $Id$
3# Perl version of (summary.sh, loop.awk, peer.awk):
4# Create summaries from xntpd's loop and peer statistics.
5#
6# Copyright (c) 1997, 1999 by Ulrich Windl <Ulrich.Windl@rz.uni-regensburg.de>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16# General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22require 5.003; # "never tested with any other version of Perl"
23use strict;
24
25use Getopt::Long;
26
27my $log_date_pattern = '[12]\d{3}[01]\d[0-3]\d';
28my $statsdir = "/var/log/ntp";		# directory with input files
29my $outputdir = "/tmp";			# directory for output files
30my $skip_time_steps = 3600.0;		# ignore time offsets larger that this
31my $startdate = "19700101";		# first data file to use (YYYYMMDD)
32my $enddate=`date -u +%Y%m%d`; chomp $enddate; --$enddate;
33my $peer_dist_limit = 400.0;
34
35my %options = ("directory|input-directory=s" => \$statsdir,
36	       "output-directory=s" => \$outputdir,
37	       "skip-time-steps:f" => \$skip_time_steps,
38	       "start-date=s" => \$startdate,
39	       "end-date=s" => \$enddate,
40	       "peer-dist-limit=f" => \$peer_dist_limit);
41
42if ( !GetOptions(%options) )
43{
44    print STDERR "valid options for $0 are:\n";
45    my $opt;
46    foreach $opt (sort(keys %options)) {
47	print STDERR "\t--$opt\t(default is ";
48	if ( ref($options{$opt}) eq "ARRAY" ) {
49	    print STDERR join(", ",  map { "'$_'" } @{$options{$opt}});
50	} else {
51	    print STDERR "'${$options{$opt}}'";
52	}
53	print STDERR ")\n";
54    }
55    print STDERR "\n";
56    die;
57}
58
59# check possibly current values of options
60die "$statsdir: no such directory" unless (-d $statsdir);
61die "$outputdir: no such directory" unless (-d $outputdir);
62die "$skip_time_steps: skip-time-steps must be positive"
63    unless ($skip_time_steps >= 0.0);
64die "$startdate: invalid start date|$`|$&|$'"
65    unless ($startdate =~ m/.*$log_date_pattern$/);
66die "$enddate: invalid end date"
67    unless ($enddate =~ m/.*$log_date_pattern$/);
68
69$skip_time_steps = 0.128 if ($skip_time_steps == 0);
70
71sub min
72{
73    my ($result, @rest) = @_;
74    map { $result = $_ if ($_ < $result) } @rest;
75    return($result);
76}
77
78sub max
79{
80    my ($result, @rest) = @_;
81    map { $result = $_ if ($_ > $result) } @rest;
82    return($result);
83}
84
85# calculate mean, range, and standard deviation for offset and frequency
86sub do_loop
87{
88    my ($directory, $fname, $out_file) = @_;
89    print "$directory/$fname\n";
90    open INPUT, "$directory/$fname" or warn "can't open $directory/$fname: $!";
91    open OUTPUT, ">>$out_file" or die "can't open $out_file: $!";
92    print OUTPUT "$fname\n";
93    my ($loop_tmax, $loop_fmax) = (-1e9, -1e9);
94    my ($loop_tmin, $loop_fmin) = (1e9, 1e9);
95    my ($loop_time_rms, $loop_freq_rms) = (0, 0);
96    my $loop_count = 0;
97    my $loop_time = 0;
98    my $loop_freq = 0;
99    my ($freq, $offs);
100    my @Fld;
101    while (<INPUT>) {
102	chop;	# strip record separator
103	@Fld = split;
104	next if ($#Fld < 4);
105#NTPv3: 50529 74356.259 -0.000112 16.1230 8
106#NTPv3: day, sec.msec, offset, drift_comp, sys_poll
107#NTPv4: 51333 54734.582 0.000001648 16.981964 0.000001094 0.020938 6
108#NTPv4: day, sec.msec, offset, drift_comp, sys_error, clock_stabil, sys_poll
109	if ($Fld[2] > $skip_time_steps || $Fld[2] < -$skip_time_steps) {
110	    warn "ignoring loop offset $Fld[2] (file $fname, line $.)\n";
111	    next
112	}
113	$loop_count++;
114	($offs, $freq) = ($Fld[2], $Fld[3]);
115	$loop_tmax = max($loop_tmax, $offs);
116	$loop_tmin = min($loop_tmin, $offs);
117	$loop_fmax = max($loop_fmax, $freq);
118	$loop_fmin = min($loop_fmin, $freq);
119	$loop_time += $offs;
120	$loop_time_rms += $offs * $offs;
121	$loop_freq += $freq;
122	$loop_freq_rms += $freq * $freq;
123    }
124    close INPUT;
125    if ($loop_count > 1) {
126	$loop_time /= $loop_count;
127	$loop_time_rms = $loop_time_rms / $loop_count - $loop_time * $loop_time;
128	if ($loop_time_rms < 0) {
129	    warn "loop_time_rms: $loop_time_rms < 0";
130	    $loop_time_rms = 0;
131	}
132	$loop_time_rms = sqrt($loop_time_rms);
133	$loop_freq /= $loop_count;
134	$loop_freq_rms = $loop_freq_rms / $loop_count - $loop_freq * $loop_freq;
135	if ($loop_freq_rms < 0) {
136	    warn "loop_freq_rms: $loop_freq_rms < 0";
137	    $loop_freq_rms = 0;
138	}
139	$loop_freq_rms = sqrt($loop_freq_rms);
140	printf OUTPUT
141	    ("loop %d, %.0f+/-%.1f, rms %.1f, freq %.2f+/-%0.3f, var %.3f\n",
142	     $loop_count, ($loop_tmax + $loop_tmin) / 2 * 1e6,
143	     ($loop_tmax - $loop_tmin) / 2 * 1e6, $loop_time_rms * 1e6,
144	     ($loop_fmax + $loop_fmin) / 2, ($loop_fmax - $loop_fmin) / 2,
145	     $loop_freq_rms);
146    }
147    else {
148	warn "no valid lines in $directory/$fname";
149    }
150    close OUTPUT
151}
152
153# calculate mean, standard deviation, maximum offset, mean dispersion,
154# and maximum distance for each peer
155sub do_peer
156{
157    my ($directory, $fname, $out_file) = @_;
158    print "$directory/$fname\n";
159    open INPUT, "$directory/$fname" or warn "can't open $directory/$fname: $!";
160    open OUTPUT, ">>$out_file" or die "can't open $out_file: $!";
161    print OUTPUT "$fname\n";
162# we toss out all distances greater than one second on the assumption the
163# peer is in initial acquisition
164    my ($n, $MAXDISTANCE) = (0, 1.0);
165    my %peer_time;
166    my %peer_time_rms;
167    my %peer_count;
168    my %peer_delay;
169    my %peer_disp;
170    my %peer_dist;
171    my %peer_ident;
172    my %peer_tmin;
173    my %peer_tmax;
174    my @Fld;
175    my ($i, $j);
176    my ($dist, $offs);
177    while (<INPUT>) {
178	chop;	# strip record separator
179	@Fld = split;
180	next if ($#Fld < 6);
181#NTPv3: 50529 83316.249 127.127.8.1 9674 0.008628 0.00000 0.00700
182#NTPv3: day, sec.msec, addr, status, offset, delay, dispersion
183#NTPv4: 51333 56042.037 127.127.8.1 94f5 -0.000014657 0.000000000 0.000000000 0.000013214
184#NTPv4: day, sec.msec, addr, status, offset, delay, dispersion, skew
185
186	$dist = $Fld[6] + $Fld[5] / 2;
187	next if ($dist > $MAXDISTANCE);
188	$offs = $Fld[4];
189	if ($offs > $skip_time_steps || $offs < -$skip_time_steps) {
190	    warn "ignoring peer offset $offs (file $fname, line $.)\n";
191	    next
192	}
193	$i = $n;
194	for ($j = 0; $j < $n; $j++) {
195	    if ($Fld[2] eq $peer_ident{$j}) {
196		$i = $j;		# peer found
197		last;
198	    }
199	}
200	if ($i == $n) {		# add new peer
201	    $peer_ident{$i} = $Fld[2];
202	    $peer_tmax{$i} = $peer_dist{$i} = -1e9;
203	    $peer_tmin{$i} = 1e9;
204	    $peer_time{$i} = $peer_time_rms{$i} = 0;
205	    $peer_delay{$i} = $peer_disp{$i} = 0;
206	    $peer_count{$i} = 0;
207	    $n++;
208	}
209	$peer_count{$i}++;
210	$peer_tmax{$i} = max($peer_tmax{$i}, $offs);
211	$peer_tmin{$i} = min($peer_tmin{$i}, $offs);
212	$peer_dist{$i} = max($peer_dist{$i}, $dist);
213	$peer_time{$i} += $offs;
214	$peer_time_rms{$i} += $offs * $offs;
215	$peer_delay{$i} += $Fld[5];
216	$peer_disp{$i} += $Fld[6];
217    }
218    close INPUT;
219    print OUTPUT
220"       ident     cnt     mean     rms      max     delay     dist     disp\n";
221    print OUTPUT
222"==========================================================================\n";
223    my @lines = ();
224    for ($i = 0; $i < $n; $i++) {
225	next if $peer_count{$i} < 2;
226	$peer_time{$i} /= $peer_count{$i};
227	eval { $peer_time_rms{$i} = sqrt($peer_time_rms{$i} / $peer_count{$i} -
228					 $peer_time{$i} * $peer_time{$i}); };
229	$peer_time_rms{$i} = 0, warn $@ if $@;
230	$peer_delay{$i} /= $peer_count{$i};
231	$peer_disp{$i} /= $peer_count{$i};
232	$peer_tmax{$i} = $peer_tmax{$i} - $peer_time{$i};
233	$peer_tmin{$i} = $peer_time{$i} - $peer_tmin{$i};
234	if ($peer_tmin{$i} > $peer_tmax{$i}) {	# can this happen at all?
235	    $peer_tmax{$i} = $peer_tmin{$i};
236	}
237	push @lines, sprintf
238	    "%-15s %4d %8.3f %8.3f %8.3f %8.3f %8.3f %8.3f\n",
239	    $peer_ident{$i}, $peer_count{$i}, $peer_time{$i} * 1e3,
240	    $peer_time_rms{$i} * 1e3, $peer_tmax{$i} * 1e3,
241	    $peer_delay{$i} * 1e3, $peer_dist{$i} * 1e3, $peer_disp{$i} * 1e3;
242    }
243    print OUTPUT sort @lines;
244    close OUTPUT;
245}
246
247sub do_clock
248{
249    my ($directory, $fname, $out_file) = @_;
250    print "$directory/$fname\n";
251    open INPUT, "$directory/$fname";
252    open OUTPUT, ">>$out_file" or die "can't open $out_file: $!";
253    print OUTPUT "$fname\n";
254    close INPUT;
255    close OUTPUT;
256}
257
258sub peer_summary
259{
260    my $in_file = shift;
261    my ($i, $j, $n);
262    my (%peer_ident, %peer_count, %peer_mean, %peer_var, %peer_max);
263    my (%peer_1, %peer_2, %peer_3, %peer_4);
264    my $dist;
265    my $max;
266    open INPUT, "<$in_file" or die "can't open $in_file: $!";
267    my @Fld;
268    $n = 0;
269    while (<INPUT>) {
270	chop;	# strip record separator
271	@Fld = split;
272	next if ($#Fld < 7 || $Fld[0] eq 'ident');
273	$i = $n;
274	for ($j = 0; $j < $n; $j++) {
275	    if ($Fld[0] eq $peer_ident{$j}) {
276		$i = $j;
277		last;			# peer found
278	    }
279	}
280	if ($i == $n) {			# add new peer
281	    $peer_count{$i} = $peer_mean{$i} = $peer_var{$i} = 0;
282	    $peer_max{$i} = 0;
283 	    $peer_1{$i} = $peer_2{$i} = $peer_3{$i} = $peer_4{$i} = 0;
284	    $peer_ident{$i} = $Fld[0];
285	    ++$n;
286	}
287	$dist = $Fld[6] - $Fld[5] / 2;
288	if ($dist < $peer_dist_limit) {
289	    $peer_count{$i}++;
290	    $peer_mean{$i} += $Fld[2];
291	    $peer_var{$i} += $Fld[3] * $Fld[3];
292	    $max = $Fld[4];
293	    $peer_max{$i} = max($peer_max{$i}, $max);
294	    if ($max > 1) {
295		$peer_1{$i}++;
296		if ($max > 5) {
297		    $peer_2{$i}++;
298		    if ($max > 10) {
299			$peer_3{$i}++;
300			if ($max > 50) {
301			    $peer_4{$i}++;
302			}
303		    }
304		}
305	    }
306	}
307	else {
308	    warn "dist exceeds limit: $dist (file $in_file, line $.)\n";
309	}
310    }
311    close INPUT;
312    my @lines = ();
313    print
314	"       host     days    mean       rms       max   >1  >5 >10 >50\n";
315    print
316	"==================================================================\n";
317    for ($i = 0; $i < $n; $i++) {
318	next if ($peer_count{$i} < 2);
319	$peer_mean{$i} /= $peer_count{$i};
320	eval { $peer_var{$i} = sqrt($peer_var{$i} / $peer_count{$i} -
321				    $peer_mean{$i} * $peer_mean{$i}); };
322	$peer_var{$i} = 0, warn $@ if $@;
323	push @lines, sprintf
324	    "%-15s %3d %9.3f% 9.3f %9.3f %3d %3d %3d %3d\n",
325	    $peer_ident{$i}, $peer_count{$i}, $peer_mean{$i}, $peer_var{$i},
326	    $peer_max{$i}, $peer_1{$i}, $peer_2{$i}, $peer_3{$i}, $peer_4{$i};
327    }
328    print sort @lines;
329}
330
331my $loop_summary="$outputdir/loop_summary";
332my $peer_summary="$outputdir/peer_summary";
333my $clock_summary="$outputdir/clock_summary";
334my (@loopfiles, @peerfiles, @clockfiles);
335
336print STDERR "Creating summaries from $statsdir ($startdate to $enddate)\n";
337
338opendir SDIR, $statsdir or die "directory ${statsdir}: $!";
339rewinddir SDIR;
340@loopfiles=sort grep /loop.*$log_date_pattern/, readdir SDIR;
341rewinddir SDIR;
342@peerfiles=sort grep /peer.*$log_date_pattern/, readdir SDIR;
343rewinddir SDIR;
344@clockfiles=sort grep /clock.*$log_date_pattern/, readdir SDIR;
345closedir SDIR;
346
347# remove old summary files
348map { unlink $_ if -f $_ } ($loop_summary, $peer_summary, $clock_summary);
349
350my $date;
351map {
352    $date = $_; $date =~ s/.*($log_date_pattern)$/$1/;
353    if ($date ge $startdate && $date le $enddate) {
354	do_loop $statsdir, $_, $loop_summary;
355    }
356} @loopfiles;
357
358map {
359    $date = $_; $date =~ s/.*($log_date_pattern)$/$1/;
360    if ($date ge $startdate && $date le $enddate) {
361	do_peer $statsdir, $_, $peer_summary;
362    }
363} @peerfiles;
364
365map {
366    $date = $_; $date =~ s/.*($log_date_pattern)$/$1/;
367    if ($date ge $startdate && $date le $enddate) {
368	do_clock $statsdir, $_, $clock_summary;
369    }
370} @clockfiles;
371
372print STDERR "Creating peer summary with limit $peer_dist_limit\n";
373peer_summary $peer_summary if (-f $peer_summary);
374