aboutsummaryrefslogtreecommitdiff
path: root/syslogck
blob: 0427a008d13eebaea4a9a4d3ddd111eef2ed90ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/usr/bin/perl

use strict;
use Sys::Syslog qw(:standard :macros);
use File::Basename;
use Pod::Usage;
use Pod::Man;
use Getopt::Long qw(:config gnu_getopt no_ignore_case);

our $VERSION = '0.99';

use constant {
    EX_OK           => 0,
    EX_USAGE        => 64,
    EX_DATAERR      => 65,
    EX_NOINPUT      => 66,
    EX_OSFILE       => 72,
    EX_TEMPFAIL     => 74,
    EX_NOPERM       => 77
};

=head1 NAME

syslogck - check if syslog properly delivers messages

=head1 SYNOPSIS

B<syslogck> [OPTIONS] [FILE...]

=head1 DESCRIPTION

Sends a test message via syslog and checks if it appears in the destination
log file.   By default, the message is sent via the B<user.notice> syslog
facility.  The log file is determined by analyzing the syslog configuration
file.  If several logs are configured to receive messages in the current
priority, the first of them that is readable to the current user will be
examined.

By default, syslog configuration is read from file F</etc/syslog.conf>.  This
can be changed by supplying the name of the file in the command line.  Multiple
arguments are allowed.

The configuration parser is very permissive.  It selects only lines that
can reliably be parsed as traditional syslog selector lines, and silently
ignores the rest.  This allows to use B<syslogck> with any flavor of syslog
that allows for traditional configuration (e.g. B<rsyslogd>).

On success (test message appeared in the log), B<syslogck> silently exits with
code B<0>.  If the message did not appear in the log within the time out period,
the program prints a descriptive diagnostics on the standard error and exits
with code 74 if the log file remained unmodified, or with code 65 if some
messages did appear in it, but the test message didn't.  Code 74 most often
means that B<syslogd> is not running at all, whereas 65 indicates that it is
misconfigured.  See B<EXIT CODES> for the exhaustive list of exit codes.    
    
=head1 OPTIONS

=over 4

=item B<-h>

Display a short option summary.

=item B<--help>

Display detailed manual.

=item B<--usage>

Display terse command line syntax summary.

=item B<-d>, B<--debug>

Print additional debugging information.    

=item B<-p>, B<--priority=>I<FACILITY>[B<.>I<PRIORITY>]

Select the syslog facility and, optionally, priority to use.  Default
is B<user.notice>.    

=item B<-n>, B<--dry-run>

Verbosely list what is being done (see B<-d>), but don't actually
send the test message.

=item B<-t>, B<--tag=>I<TAG>

Set syslog tag.

=item B<-T>, B<--timeout=>I<N>

Wait I<N> seconds for the message to appear in the log file.  Default is
10.    
    
=back
    
=head1 EXIT CODES

=over 4
    
=item 0

Success.  Syslog works properly.

=item 64

Command line usage error.

=item 65

Test message did not appear in the log file, but other messages did.
This means that syslog is probably working, but misconfigured.    
    
=item 66

Can't open the selected log file.
    
=item 72

No sutable log files found.  Try another priority.

=item 74

Timed out while waiting for the message to appear in the log file.  Most
probably, syslog is not running.
    
=item 77

None of the selected log files is readable for the current user.  Either
select another priority (see B<-p>), or run B<syslogck> via B<sudo>.

=back
    
=head1 AUTHOR

Sergey Poznyakoff, <gray@gnu.org>

=cut    
    
my $progname = basename($0);
my $progdescr = 'check if syslog properly delivers messages';
my $tag = $progname;
my $timeout = 10;

my $debug;
my $dry_run;

my $facility = 'user';

my %prio_order = ('debug'   => LOG_DEBUG,
		  'info'    => LOG_INFO,
		  'notice'  => LOG_NOTICE,
		  'warn'    => LOG_WARNING,
		  'warning' => LOG_WARNING,
		  'err'     => LOG_ERR,
		  'error'   => LOG_ERR,
		  'crit'    => LOG_CRIT,
		  'alert'   => LOG_ALERT,
		  'emerg'   => LOG_EMERG,
		  'panic'   => LOG_EMERG);

my $priority = $prio_order{notice};

sub error {
    my $msg = shift;
    local %_ = @_;
    print STDERR "$progname: " if defined($progname);
    print STDERR "$_{prefix}: " if defined($_{prefix});
        print STDERR "$msg\n"
}

sub debug {
    error(join(' ',@_), prefix => 'DEBUG') if $debug;
}

sub abend {
    my $code = shift;
    print STDERR "$progname: " if defined($progname);
    print STDERR "@_\n";
    exit $code;
}

sub match_selector {
    my ($sel, $file, $line, $lc) = @_;
    my $match;
    my @report;

    $sel =~ s/\s+//g;
    foreach my $ent (split /;/, $sel) {
	if ($ent =~ /^(?<fac>.*)\.(?<pri>.*)$/) {
	    if (match_facility($+{fac})) {
		if ($+{pri} eq 'none') {
		    push @report, [ $ent, 0 ] if $debug && $match;
		    $match = 0;
		} elsif (match_priority($+{pri}, \$match)) {
		    push @report, [ $ent, $match ] if $debug;
		}
	    }
	}
    }

    if (@report) {
	my $loc = "$file:$line";
	$loc .= "-".($line+$lc) if $lc;
	debug("$loc: "
	      . ($match ? "select" : "skip")
	      . " ("
	      . join('; ',
		     map { ($_->[1] ? "select" : "skip")." by $_->[0]" } @report)
	      .")");
    }
    
    return $match;
}

sub match_facility {
    my ($arg) = @_;
    return 1 if $arg eq '';
    foreach my $f (split /,/, $arg) {
	$f =~ s/\..*//;
	return 1 if $f eq '*' || $f eq $facility;
    }
    return 0;
}
	    
sub match_priority {
    my ($pri, $prev_match) = @_;
    my $match = $$prev_match;
    
    my $neg = $pri =~ s/^!(.+)/$1/;
    return 0 if $neg && !$match;
	
    my $eq = $pri =~ s/^=(.+)/$1/;
    if ($pri eq '*') {
	$match = 1;
    } else {
	next unless exists($prio_order{$pri});
	if ($eq) {
	    $match = $prio_order{$pri} == $priority;
	} else {
	    $match = $prio_order{$pri} >= $priority;
	}
    }
    $match = !$match if $neg;
    my $ret = $match != $$prev_match;
    $$prev_match = $match;
    return $ret;
}

sub find_actions {
    my ($file, $actions) = @_;
    my $line;    # current line number
    my $lc;      # counter of continuation lines
    if (open(my $fd, '<', $file)) {
	while (<$fd>) {
	    ++$line;
	    chomp;
	    s/^\s+//;
	    next if /^#/;
	    if (/\\$/) {
		chop;
		$_ .= <$fd>;
		++$lc;
		redo;
	    }
	    if (/^(?<sel>.+?)\s+(?<buf>-?)(?<stream>[^\s]+)$/) {
		push @{$actions}, $+{stream}
		    if match_selector($+{sel}, $file, $line - $lc, $lc);
	    }
	    $lc = 0;
	}
    } else {
	error("can't open $file: $!");
	return undef;
    }
    return 1;
}

GetOptions("h" => sub {
                    pod2usage(-message => "$progname: $progdescr",
                              -exitstatus => EX_OK);
           },
           "help" => sub {
                    pod2usage(-exitstatus => EX_OK, -verbose => 2);
           },
           "usage" => sub {
                    pod2usage(-exitstatus => EX_OK, -verbose => 0);
           },
	   "debug|d" => \$debug,
	   "priority|p=s" => sub {
	       my ($opt, $arg) = @_;
	       if ($arg =~ /^(.+?)\.(.+)$/) {
		   abend(EX_USAGE, "unknown priority")
		       unless exists($prio_order{$2});
		   $priority = $prio_order{$2};
		   $facility = $1;
	       } else {
		   $facility = $arg;
	       }
	   },
	   "dry-run|n" => \$dry_run,
	   "tag|t=s" => \$tag,
	   "timeout|T=n" => \$timeout
) or exit EX_USAGE;

$debug++ if $dry_run;

unshift @ARGV, "/etc/syslog.conf" unless @ARGV;

my @act;

foreach my $file (@ARGV) {
    find_actions($file, \@act);
}

if (@act) {
    debug("actions: ".join(', ', @act));
    @act = grep { m#^/# } @act;
    debug("files: ".join(', ', @act));
}

exit EX_OK if $dry_run;

abend(EX_OSFILE, "no sutable files found (try another priority)") unless @act;

my $file;
while ($file = shift @act) {
    last if -r $file;
}

abend(EX_NOPERM, "no readable files found") unless defined $file;

debug("selected file: $file");
open(my $fd, '<', $file)
    or abend(EX_NOINPUT, "cannot open file $file: $!");

seek $fd, 0, 2;
my $msg;

eval {
    use Time::HiRes qw(time);
};

$msg = time() . '-' . $$;

debug("sending \"$msg\"");
openlog($tag, 'pid', $facility)
    or die "openlog failed: $!";
syslog($priority, $msg);
closelog();

my $code = EX_TEMPFAIL; 
$SIG{ALRM} = sub {
    abend($code, "doesn't match");
};
alarm $timeout;
while (1) {
    while (<$fd>) {
	chomp;
	debug("read $_");
	s/\s+$//;
	exit(EX_OK) if /$msg$/;
	$code = EX_DATAERR;
    }
}


Return to:

Send suggestions and report system problems to the System administrator.