File Coverage

blib/lib/App/MonM.pm
Criterion Covered Total %
statement 69 181 38.1
branch 0 58 0.0
condition 0 49 0.0
subroutine 23 32 71.8
pod 6 6 100.0
total 98 326 30.0


line stmt bran cond sub pod time code
1             package App::MonM; # $Id: MonM.pm 136 2022-09-09 15:37:03Z abalama $
2 1     1   56248 use warnings;
  1         10  
  1         30  
3 1     1   4 use strict;
  1         3  
  1         17  
4 1     1   512 use utf8;
  1         12  
  1         4  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             App::MonM - Simple monitoring tool
11              
12             =head1 VERSION
13              
14             Version 1.08
15              
16             =head1 SYNOPSIS
17              
18             # monm checkit
19             # monm report
20             # monm show
21              
22             =head1 DESCRIPTION
23              
24             Simple monitoring tool
25              
26             =head2 FEATURES
27              
28             =over 4
29              
30             =item Checking availability of sites (http/https)
31              
32             =item Checking of database health (DBI)
33              
34             =item Checking internal and external counters using system commands and tools (command)
35              
36             =item Supports SMTP, POP3, FTP, SSH protocols, and etc.
37              
38             =item Interface for SMS sending
39              
40             =item Easy installation and configuration
41              
42             =item A small number of system dependencies
43              
44             =back
45              
46             =head2 SYSTEM REQUIREMENTS
47              
48             =over 4
49              
50             =item Perl v5.16+
51              
52             =item L
53              
54             =item L
55              
56             =item L
57              
58             =item L
59              
60             To use this module in full powerful, you must have Net-SNMP installed
61             on your system. More specifically you need the Perl modules that come with it.
62              
63             DO NOT INSTALL SNMP or Net::SNMP from CPAN!
64              
65             The SNMP module is matched to an install of net-snmp, and must be installed
66             from the net-snmp source tree.
67              
68             The Perl module C is found inside the net-snmp distribution. Go to the
69             F directory of the distribution to install it, or run
70             C<./configure --with-perl-modules> from the top directory of the net-snmp
71             distribution.
72              
73             Net-SNMP can be found at https://net-snmp.sourceforge.io/
74              
75             =back
76              
77             =head2 INSTALLATION
78              
79             # sudo cpan install App::MonM
80              
81             ...and then:
82              
83             # sudo monm configure
84              
85             =head2 CONFIGURATION
86              
87             By default configuration file located in C directory
88              
89             B each configuration option (directive) detailed describes in C file,
90             see also C file for example of MonM checkit configuration
91              
92             =head3 GENERAL DIRECTIVES
93              
94             =over 4
95              
96             =item B, B
97              
98             DaemonUser monmu
99             DaemonGroup monmu
100              
101             Defines a username and groupname for daemon working
102              
103             Default: monmu
104              
105             =item B
106              
107             Expires 1d
108              
109             Defines the lifetime of a record in the database.
110             After this time, the record from the database will be deleted automatically.
111              
112             Format for time can be in any of the following forms:
113              
114             20 -- in 20 seconds
115             180s -- in 180 seconds
116             2m -- in 2 minutes
117             12h -- in 12 hours
118             1d -- in 1 day
119             3M -- in 3 months
120             2y -- in 2 years
121             3m -- 3 minutes ago(!)
122              
123             Default: 1d (1 day)
124              
125             =item B
126              
127             Interval 20
128              
129             Defines worker interval. This interval determines how often
130             the cycle of checks will be started.
131              
132             Default: 20
133              
134             =item B
135              
136             LogEnable on
137              
138             Activate or deactivate the logging: on/off (yes/no)
139              
140             Default: off
141              
142             =item B
143              
144             LogFile /var/log/monm.log
145              
146             Defines path to custom log file
147              
148             Default: use syslog
149              
150             =item B
151              
152             LogIdent myProgramName
153              
154             Defines LogIdent string. We not recommended use it
155              
156             Default: none
157              
158             =item B
159              
160             LogLevel warning
161              
162             Defines log level
163              
164             Allowed levels: debug, info, notice, warning, error,
165             crit, alert, emerg, fatal, except
166              
167             Default: debug
168              
169             =item B
170              
171             Workers 3
172              
173             Defines workers number
174              
175             Default: 3
176              
177             =back
178              
179             =head3 USER AND GROUP DIRECTIVES
180              
181             =over 4
182              
183             =item B
184              
185             The "Group" section combines several users into named groups.
186             This allows you to reduce the lists of recipients of notifications
187              
188            
189             Enable on
190             User Bob, Alice
191             User Ted
192            
193              
194             Each group has a status - enabled/disabled (see Enable directive)
195              
196             =item B
197              
198             The User section allows you to define the user name and settings.
199              
200            
201             Enable on
202              
203             At Sun[off];Mon-Thu[08:30-12:30,13:30-18:00];Fri[10:00-20:30];Sat[off]
204              
205            
206             To bob@example.com
207            
208              
209            
210             To +1-424-254-5301
211             At Mon-Fri[08:30-18:30]
212            
213            
214              
215             Each user has a status - enabled/disabled (see Enable directive). User settings
216             are disabled by default. User settings contains channel sections, the settings
217             of which are taken either from globally defined channel sections or from those
218             defines within the scope of this user only
219              
220             =back
221              
222             =head3 CHANNEL DIRECTIVES
223              
224             See L
225              
226             =head3 CHECKIT DIRECTIVES
227              
228             See L
229              
230             =head2 CRONTAB
231              
232             To automatically launch the program, we recommend using standard scheduling tools, such as crontab
233              
234             * * * * * monm checkit >/dev/null 2>>/var/log/monm-error.log
235              
236             For daily reporting:
237              
238             0 8 * * * monm report >/dev/null 2>>/var/log/monm-error.log
239              
240             =head1 INTERNAL METHODS
241              
242             =over 4
243              
244             =item B
245              
246             The CTK method for classes extension. For internal use only!
247              
248             See L
249              
250             =item B
251              
252             my $notifier = $app->notifier;
253              
254             Returns the Notifier object
255              
256             =item B
257              
258             $app->notify();
259              
260             Sends notifications
261              
262             =item B
263              
264             return $app->raise("Red message");
265              
266             Sends message to STDERR and returns 0
267              
268             =item B
269              
270             my $store = $app->store();
271              
272             Returns store object
273              
274             =item B
275              
276             my @errors = $app->trigger();
277              
278             Runs triggers
279              
280             =back
281              
282             =head1 HISTORY
283              
284             See C file
285              
286             =head1 TO DO
287              
288             See C file
289              
290             =head1 SEE ALSO
291              
292             L, L
293              
294             =head1 AUTHOR
295              
296             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
297              
298             =head1 COPYRIGHT
299              
300             Copyright (C) 1998-2022 D&D Corporation. All Rights Reserved
301              
302             =head1 LICENSE
303              
304             This program is free software; you can redistribute it and/or
305             modify it under the same terms as Perl itself.
306              
307             See C file and L
308              
309             =cut
310              
311 1     1   72 use vars qw/ $VERSION /;
  1         2  
  1         52  
312             $VERSION = '1.08';
313              
314 1     1   5 use feature qw/ say /;
  1         1  
  1         101  
315              
316 1     1   5 use Carp;
  1         2  
  1         41  
317 1     1   411 use Text::SimpleTable;
  1         2076  
  1         27  
318 1     1   6 use File::Spec;
  1         1  
  1         19  
319 1     1   504 use File::stat qw//;
  1         8693  
  1         24  
320 1     1   373 use Text::ParseWords qw/shellwords quotewords/;
  1         1092  
  1         52  
321 1     1   410 use Text::Wrap qw/wrap/;
  1         2191  
  1         48  
322              
323 1     1   419 use CTK::Skel;
  1         212988  
  1         51  
324 1     1   8 use CTK::Util qw/ preparedir dformat execute dtf tz_diff sendmail variant_stf lf_normalize sharedstatedir /;
  1         1  
  1         63  
325 1     1   5 use CTK::ConfGenUtil;
  1         2  
  1         53  
326 1     1   5 use CTK::TFVals qw/ :ALL /;
  1         2  
  1         149  
327              
328 1     1   401 use App::MonM::Const;
  1         3  
  1         73  
329 1         107 use App::MonM::Util qw/
330             blue green red yellow cyan magenta gray
331             yep nope skip wow
332             getBit setBit
333             node2anode getCheckitByName
334             getExpireOffset getTimeOffset explain
335             slurp spurt
336             merge
337 1     1   406 /;
  1         3  
338 1     1   447 use App::MonM::Store;
  1         3  
  1         25  
339 1     1   381 use App::MonM::Checkit;
  1         3  
  1         32  
340 1     1   369 use App::MonM::QNotifier;
  1         4  
  1         31  
341 1     1   469 use App::MonM::Report;
  1         3  
  1         30  
342              
343 1     1   6 use parent qw/CTK::App/;
  1         2  
  1         10  
344              
345             use constant {
346 1         5404 TAB9 => " " x 9,
347             EXPIRES => 24*60*60, # 1 day
348             SMSSBJ => 'MONM CHECKIT REPORT',
349             DATE_FORMAT => '%YYYY-%MM-%DD %hh:%mm:%ss',
350             TABLE_HEADERS => [(
351             [32, 'NAME'],
352             [7, 'TYPE'],
353             [19, 'LAST CHECK DATE'],
354             [7, 'RESULT'],
355             )],
356              
357             # Markers
358             MARKER_OK => '[ OK ]',
359             MARKER_FAIL => '[ FAIL ]',
360             MARKER_SKIP => '[ SKIP ]',
361             MARKER_INFO => '[ INFO ]',
362 1     1   6209 };
  1         2  
363              
364             eval { require App::MonM::Notifier };
365             my $NOTIFIER_LOADED = 1 unless $@;
366             $NOTIFIER_LOADED = 0 if $NOTIFIER_LOADED && (App::MonM::Notifier->VERSION * 1) < 1.04;
367              
368              
369             sub again {
370 0     0 1   my $self = shift;
371 0           $self->SUPER::again(); # CTK::App again first!!
372              
373             # Datadir & Tempdir
374 0 0         if ($self->option("datadir")) {
    0          
375             # Prepare DataDir
376 0 0         preparedir( $self->datadir() ) or do {
377 0           $self->status(0);
378 0           $self->raise("Can't prepare directory %s", $self->datadir());
379             };
380             } elsif ($self->option("daemondir")) {
381 0           $self->datadir(File::Spec->catdir(sharedstatedir(), PREFIX));
382             } else {
383 0           $self->datadir($self->tempdir());
384             }
385             # Prepare TempDir
386 0 0         preparedir( $self->tempdir() ) or do {
387 0           $self->status(0);
388 0           $self->raise("Can't prepare directory %s", $self->tempdir());
389             };
390              
391             # Store
392 0           my $db_file = File::Spec->catfile($self->datadir, App::MonM::Store::DB_FILENAME());
393 0   0       my $store_conf = $self->config("store") || $self->config('dbi') || {file => $db_file};
394 0 0         $store_conf = {file => $db_file} unless is_hash($store_conf);
395 0           my %store_args = %$store_conf;
396 0 0 0       $store_args{file} = $db_file unless ($store_args{file} || $store_args{dsn});
397 0           my $store = App::MonM::Store->new(%store_args);
398 0           $self->{store} = $store;
399             #$self->debug(explain($store));
400              
401             # Notifier object init
402 0           my %nargs = (config => $self->configobj);
403 0 0 0       $self->{notifier} = $NOTIFIER_LOADED && lvalue($self->config("usemonotifier"))
404             ? App::MonM::Notifier->new(%nargs)
405             : App::MonM::QNotifier->new(%nargs);
406              
407             #$self->status($self->raise("Test error"));
408              
409 0           return $self; # CTK requires!
410             }
411             sub raise {
412 0     0 1   my $self = shift;
413 0           say STDERR red(@_);
414 0           $self->log_error(sprintf(shift, @_));
415 0           return 0;
416             }
417             sub store {
418 0     0 1   my $self = shift;
419 0           return $self->{store};
420             }
421             sub notifier {
422 0     0 1   my $self = shift;
423 0           return $self->{notifier};
424             }
425              
426             __PACKAGE__->register_handler(
427             handler => "info",
428             description => "Show statistic information",
429             code => sub {
430             ### CODE:
431             my ($self, $meta, @arguments) = @_;
432             my $store = $self->store;
433              
434             # General info
435             printf("Hostname : %s\n", HOSTNAME);
436             printf("MonM version : %s\n", $self->VERSION);
437             printf("Date : %s\n", _fdate());
438             printf("Data dir : %s\n", $self->datadir);
439             printf("Temp dir : %s\n", $self->tempdir);
440             printf("Config file : %s\n", $self->configfile);
441             printf("Config status : %s\n", $self->conf("loadstatus") ? green("OK") : magenta("ERROR: not loaded"));
442             $self->raise($self->configobj->error) if !$self->configobj->status and length($self->configobj->error);
443             printf("Notifier class : %s\n", ref($self->notifier) || magenta("not initialized"));
444             #$self->debug(explain($self->config)) if $self->conf("loadstatus") && $self->verbosemode;
445              
446             # DB status
447             printf("DB DSN : %s\n", $store->dsn);
448             printf("DB status : %s\n", $store->error ? red("ERROR") : green("OK"));
449             my $db_is_ok = $store->error ? 0 : 1;
450             if ($db_is_ok && $store->{file} && -e $store->{file}) {
451             my $s = File::stat::stat($store->{file})->size;
452             printf("DB file : %s\n", $store->{file});
453             printf("DB size : %s\n", sprintf("%s (%d bytes)", _fbytes($s), $s));
454             printf("DB modified : %s\n", _fdate(File::stat::stat($store->{file})->mtime || 0));
455             }
456             $self->raise($store->error) unless $db_is_ok;
457              
458             # Checkets
459             my @checkits = getCheckitByName($self->config("checkit"));
460             my $noc = scalar(@checkits);
461             printf("Checkits : %s\n", $noc ? $noc : yellow("none"));
462             if ($noc) {
463             #print explain(\@checkits);
464             my $tbl = Text::SimpleTable->new(
465             [20, 'CHECKIT NAME'], # name
466             [7, 'TYPE'], # type
467             [7, 'TARGET'], # target
468             [6, 'INTRVL'], # interval
469             [3, 'TRG'], # trigger
470             [27, 'RECIPIENTS'], # sendto
471             );
472             foreach my $ch (@checkits) {
473             my $triggers = array($ch, "trigger");
474             my $recipients = array($ch, "sendto");
475             $tbl->row( # variant_stf($v->{source} // '', $src_len),
476             variant_stf($ch->{name} // '', 20),
477             $ch->{type} // 'http',
478             $ch->{target} // 'status',
479             $ch->{interval} || 0,
480             scalar(@$triggers),
481             join(", ", @$recipients),
482             );
483             }
484             print $tbl->draw();
485             }
486              
487             # Scheduler
488             my $scheduler = App::MonM::Util::Scheduler->new;
489              
490             # Channels
491             my $channels = $self->notifier->{ch_def} || {};
492             my $chcnt = scalar(keys %$channels);
493             printf("Channels : %s\n", $chcnt ? $chcnt : yellow("none"));
494             if ($chcnt) {
495             my $tbl = Text::SimpleTable->new(
496             [20, 'CHANNEL NAME'],
497             [7, 'TYPE'],
498             [42, 'TO (FROM)'],
499             [7, 'ON/NOW'],
500             );
501             foreach my $ch_name (keys %$channels) {
502             my $ch = hash($channels, $ch_name);
503             $scheduler->add($ch_name, lvalue($ch, "at"));
504             $tbl->row(
505             $ch_name,
506             lvalue($ch, "type") || '',
507             lvalue($ch, "from")
508             ? sprintf("%s (%s)", lvalue($ch, "to") || '', lvalue($ch, "from"))
509             : lvalue($ch, "to") || '',
510             sprintf("%s/%s",
511             lvalue($ch, "enable") || lvalue($ch, "enabled") ? 'Yes' : 'No',
512             $scheduler->check($ch_name) ? "Yes" : "No",
513             ),
514             );
515             if ($self->verbosemode && $scheduler->getAtString($ch_name)) {
516             printf(" Ch=%s; At=%s\n", $ch_name, $scheduler->getAtString($ch_name));
517             }
518              
519             }
520             print $tbl->draw();
521             }
522              
523             # Users
524             my @users = $self->notifier->getUsers;
525             printf("Allowed users : %s\n", @users ? join(", ", @users) : yellow("none"));
526             if (@users) {
527             my $tbl = Text::SimpleTable->new(
528             [20, 'USERNAME'],
529             [20, 'CHANNEL (BASEDON)'],
530             [7, 'TYPE'],
531             [19, 'TO'],
532             [7, 'ON/NOW'],
533             );
534             my $old = "";
535             foreach my $u (sort {$a cmp $b} @users) {
536             # Get User node
537             my $usernode = node($self->conf("user"), $u);
538             next unless is_hash($usernode) && keys %$usernode;
539             #print App::MonM::Util::explain($usernode);
540              
541             # Get user channels
542             my $channels_usr = hash($usernode => "channel");
543             foreach my $ch_name (keys %$channels_usr) {
544             my $at = lvalue($channels_usr, $ch_name, "at") || lvalue($usernode, "at");
545             my $basedon = lvalue($channels_usr, $ch_name, "basedon") || lvalue($channels_usr, $ch_name, "baseon") || '';
546             my $ch = merge(
547             hash($self->notifier->{ch_def}, $basedon || $ch_name),
548             hash($channels_usr, $ch_name),
549             {$at ? (at => $at) : ()},
550             );
551             $scheduler->add($ch_name, lvalue($ch, "at"));
552             #print App::MonM::Util::explain($ch);
553             $tbl->row(
554             ($old eq $u) ? "" : $u,
555             $basedon ? sprintf("%s (%s)", $ch_name, $basedon): $ch_name,
556             lvalue($ch, "type") || '',
557             lvalue($ch, "to") || '',
558             sprintf("%s/%s",
559             lvalue($ch, "enable") || lvalue($ch, "enabled") ? 'Yes' : 'No',
560             $scheduler->check($ch_name) ? "Yes" : "No",
561             ),
562             );
563             if ($self->verbosemode && $scheduler->getAtString($ch_name)) {
564             printf(" Usr=%s; Ch=%s; At=%s\n", $u, $ch_name, $scheduler->getAtString($ch_name));
565             }
566             } continue {
567             $old = $u;
568             }
569             unless (%$channels_usr) {
570             $tbl->row( $u, '', '', '', '-------' );
571             }
572             }
573             print $tbl->draw();
574             }
575              
576             # Groups
577             my @groups = $self->notifier->getGroups;
578             printf("Allowed groups : %s\n", @groups ? join(", ", @groups) : yellow("none"));
579             if (@groups) {
580             my $tbl = Text::SimpleTable->new(
581             [20, 'GROUP NAME'],
582             [62, 'USERS'],
583             );
584             foreach my $g (sort {$a cmp $b} @groups) {
585             my @us = $self->notifier->getUsersByGroup($g);
586             $tbl->row(
587             $g,
588             join(", ", @us),
589             );
590             }
591             print $tbl->draw();
592             }
593              
594             #print explain([$self->notifier->getUsersByGroup("Bar")]);
595              
596             return 1;
597             });
598              
599             __PACKAGE__->register_handler(
600             handler => "configure",
601             description => "Generate configuration files",
602             code => sub {
603             ### CODE:
604             my ($self, $meta, @arguments) = @_;
605             my $store = $self->store;
606             my $dir = shift(@arguments) || $self->root;
607              
608             # Creating configuration
609             my $skel = CTK::Skel->new(
610             -name => PROJECTNAME,
611             -root => $dir,
612             -skels => {
613             config => 'App::MonM::ConfigSkel',
614             },
615             -vars => {
616             PROJECT => PROJECTNAME,
617             PROJECTNAME => PROJECTNAME,
618             PREFIX => PREFIX,
619             },
620             -debug => $self->verbosemode,
621             );
622             printf("Installing configuration to \"%s\"...\n", $dir);
623             if ($skel->build("config")) {
624             say green("Done. Configuration has been installed");
625             } else {
626             return $self->raise("Can't install configuration");
627             }
628              
629             return 1;
630             });
631              
632             __PACKAGE__->register_handler(
633             handler => "checkit",
634             description => "Checkit",
635             code => sub {
636             ### CODE:
637             my ($self, $meta, @arguments) = @_;
638             my $store = $self->store;
639             return $self->raise($store->error) if $store->error;
640              
641             # Check configuration
642             unless ($self->configobj->status) {
643             return length($self->configobj->error)
644             ? $self->raise($self->configobj->error)
645             : "Can't load configuration file";
646             }
647              
648             # Get checkits
649             my @checkits = getCheckitByName($self->config("checkit"), @arguments);
650             my $noc = scalar(@checkits);
651             unless ($noc) {
652             skip("No enabled configuration section found");
653             $self->log_info("No enabled configuration section found");
654             return 1;
655             }
656              
657             # Create Checkit object
658             my $checker = App::MonM::Checkit->new;
659              
660             # Get all records from DB
661             my %all;
662             foreach my $r ($store->getall) {
663             $all{$r->{name}} = $r;
664             }
665             return $self->raise($store->error) if $store->error;
666             # print explain(\@checkits);
667              
668             # Start
669             my $curtime = time;
670             my $status = 1;
671             my $passed = 0;
672             foreach my $checkit (sort {$a->{name} cmp $b->{name}} @checkits) {
673             my $result = 1; # Check result
674             my $name = $checkit->{name};
675             my $info = $all{$name} || {}; # from database
676             my $id = $info->{id} || 0;
677             my $old = $info->{status} || 0;
678             my $got = ($old << 1) & 15;
679             my $pub = $info->{'time'} || 0;
680             my $interval = getTimeOffset(lvalue($checkit, "interval") || 0);
681              
682             # Check interval first
683             if ($interval) {
684             if (($pub + $interval) >= $curtime) {
685             print gray MARKER_SKIP;
686             printf(" %s (%s)\n", $name, "Too little time has passed before a next check [delay $interval sec]");
687             next;
688             }
689             }
690              
691             # Check
692             $result = $checker->check($checkit);
693             if ($result) {
694             $got = setBit($got, 0); # Set first bit if result is PASSED
695             $passed++;
696             } else {
697             $status = 0; # General status
698             }
699              
700             # Show resulsts
701             print $result ? green(MARKER_OK) : red(MARKER_FAIL);
702             printf(" %s (%s >>> %s)\n", $name, $checker->source, $checker->message);
703             if ($self->verbosemode) {
704             printf "%sStatus=%s; Code=%s\n", TAB9,
705             $checker->status || 0, $checker->code // '';
706             say TAB9, $checker->note;
707             if (defined($checker->content) && length($checker->content)) {
708             $Text::Wrap::columns = SCREENWIDTH - 10;
709             say TAB9, "-----BEGIN CONTENT-----";
710             say wrap(TAB9, TAB9, lf_normalize($checker->content));
711             say TAB9, "-----END CONTENT-----";
712             }
713             }
714             if ($result && !$checker->status) {
715             wow("%s", $checker->error);
716             } elsif (!$result) {
717             nope("%s", $checker->error);
718             }
719              
720             # Save data to database
721             my %data = (
722             id => $id,
723             name => $name, # Checkit name
724             type => $checker->type, # Checkit type
725             result => $result, # Checkit result
726             source => $checker->source, # Source string
727             code => $checker->code, # Checkit code value
728             message => $checker->message, # Checkit message string
729             note => $checker->note, # Checkit note string
730             status => $got, # New status value (code of result) for store only!
731             subject => sprintf("%s: Available %s [%s]", $result ? 'OK' : 'PROBLEM', $name, HOSTNAME), # Subject
732             );
733             my $chst = $id ? $store->set(%data) : $store->add(%data);
734             unless ($chst) {
735             $self->raise($store->error) if $store->error;
736             $status = 0;
737             next;
738             }
739              
740             # Triggers and notifications
741             # GOT = [0-0-1-1] = 3 -- OK
742             # GOT = [1-1-0-0] = 12 -- PROBLEM
743             if ($got == 3 or $got == 12) {
744             my @errs;
745             push @errs, $checker->error if $checker->error; # Checkit error string
746             $data{status} = $checker->status; # Checkit status (NO RESULT!!);
747              
748             # Run triggers (FIRST)
749             push @errs, $self->trigger(%data, trigger => array($checkit, "trigger"));
750              
751             # Send message via notifier (SECOND)
752             $self->notify(%data, sendto => array($checkit, "sendto"), errors => \@errs);
753             }
754             }
755              
756             # Show Total resulsts
757             print $status ? green(MARKER_OK) : red(MARKER_FAIL);
758             printf(" Total passed %d checks of %d in %s\n", $passed, $noc, $self->tms());
759              
760             # Cleaning DB
761             my $expire = getExpireOffset(lvalue($self->config("expires"))
762             || lvalue($self->config("expire")) || EXPIRES);
763             $store->clean(period => $expire) or do {
764             return $store->error ? $self->raise($store->error) : 0;
765             };
766              
767             return $status;
768             });
769              
770             __PACKAGE__->register_handler(
771             handler => "remind",
772             description => "Retries sending notifies",
773             code => sub {
774             ### CODE:
775             my ($self, $meta, @arguments) = @_;
776             return $self->raise(($self->notifier->error)) unless $self->notifier->remind;
777             return 1;
778             });
779              
780             __PACKAGE__->register_handler(
781             handler => "report",
782             description => "Checkit report",
783             code => sub {
784             ### CODE:
785             my ($self, $meta, @arguments) = @_;
786             my $store = $self->store;
787              
788             # Init
789             my (@errors, @table);
790             my $status = 1;
791             my $tbl = Text::SimpleTable->new(@{(TABLE_HEADERS)});
792              
793             # Header
794             my @header;
795             push @header, ["Hostname", HOSTNAME];
796             push @header, ["Database DSN", $store->dsn];
797             my $db_is_ok = $store->error ? 0 : 1;
798             push @header, ["Database status", $db_is_ok ? "OK" : "ERROR"];
799             unless ($db_is_ok) {
800             push @errors, $store->dsn, $store->error, "";
801             $status = $self->raise("%s: %s", $store->dsn, $store->error);
802             }
803              
804             # Get checkits from config
805             my @checkits = getCheckitByName($self->config("checkit"));
806             my $noc = scalar(@checkits);
807             push @header, ["Number of checks", $noc ? $noc : "no checks"];
808             unless ($noc) {
809             skip("No enabled configuration section found");
810             $self->log_info("No enabled configuration section found");
811             $status = 0;
812             }
813              
814             # Get all records from DB
815             my %all;
816             if ($db_is_ok) {
817             foreach my $r ($store->getall) {
818             $all{$r->{name}} = $r;
819             }
820             if ($store->error) {
821             push @errors, $store->dsn, $store->error, "";
822             $status = $self->raise("%s: %s", $store->dsn, $store->error);
823             }
824             }
825              
826             # Checkits
827             if ($status) {
828             foreach my $checkit (sort {$a->{name} cmp $b->{name}} @checkits) {
829             my $name = $checkit->{name};
830             my $info = $all{$name} || {};
831             my $last = $info->{"time"} || 0;
832             my $v = $info->{status} || 0;
833             my $ostat = -1;
834             if (getBit($v, 0) && getBit($v, 1) && getBit($v, 2)) { # Ok
835             $ostat = 1;
836             } elsif ((getBit($v, 0) + getBit($v, 1)) == 0) { # Problem
837             $ostat = 0;
838             $status = 0;
839             }
840             $tbl->row($name, $info->{type} || 'http',
841             $last ? dtf(DATE_FORMAT, $last) : "",
842             $ostat ? $ostat > 0 ? 'PASSED' : 'UNKNOWN' : 'FAILED',
843             );
844             unless ($ostat) {
845             push @errors, sprintf("%s (%s >>> %s)", $name, $info->{source} || '', $info->{message} || ''), "";
846             }
847             #say(explain($info));
848             }
849             $tbl->hr;
850             }
851             $tbl->row('SUMMARY', "", "", $noc ? $status ? 'PASSED' : 'FAILED' : 'UNKNOWN');
852              
853             # Get SendMail config
854             my $sendmail = hash($self->config('channel'), "SendMail");
855              
856             # Get output file
857             my $outfile = $self->option("outfile");
858             if ($outfile) {
859             unless (File::Spec->file_name_is_absolute($outfile)) {
860             $outfile = File::Spec->catfile($self->datadir, $outfile);
861             }
862             }
863              
864             # Get To value
865             my $to = scalar(@arguments)
866             ? join(", ", @arguments)
867             : uv2null(value($sendmail, "to"));
868             my $send_report = 1 if $to && $to !~ /\@example.com$/;
869             $send_report = 0 if $outfile;
870             push @header, ["Send report to", $to] if $send_report;
871             push @header, ["Summary result", $status ? 'PASSED' : 'FAILED'];
872              
873             # Report generate
874             my $report = App::MonM::Report->new(name => "last checks", configfile => $self->configfile);
875             my $report_title = $status ? "checking report" : "error report";
876             $report->common(@header); # Add common information
877             $report->summary( # Add summary table
878             $status ? "All last checks successful" : "Errors occurred while checking",
879             $tbl->draw(), # Add report table
880             );
881             $report->errors(@errors); # Add list of occurred errors
882             if ($outfile) {
883             $report->abstract(sprintf("The %s for last checks on %s\n", $report_title, HOSTNAME));
884             if (my $err = spurt($outfile, $report->as_string)) {
885             nope($err);
886             $self->log_error($err);
887             } else {
888             my $msg = sprintf("The report successfully saved to file: %s", $outfile);
889             yep($msg);
890             $self->log_debug($msg);
891             }
892             return $status;
893             } elsif ($self->verbosemode) { # Draw to STDOUT
894             printf("%s BEGIN REPORT ~~~\n", "~" x (SCREENWIDTH()-17)) if IS_TTY;
895             printf("The %s for last checks on %s\n\n", $report_title, HOSTNAME);
896             print $report->as_string;
897             printf("%s END REPORT ~~~\n", "~" x (SCREENWIDTH()-15)) if IS_TTY;
898             }
899              
900             # Send report
901             if ($send_report) {
902             $report->title($report_title);
903             $report->footer($self->tms);
904              
905             # Send
906             my $ns = $self->notifier->notify(
907             to => $to,
908             subject => sprintf("%s %s (%s on %s)", PROJECTNAME, $report_title, "last checks", HOSTNAME),
909             message => $report->as_string,
910             after => sub {
911             my $this = shift;
912             my $message = shift;
913             my $sent = shift;
914              
915             if ($sent) {
916             my $msg = $this->channel->error
917             ? sprintf("Report was not sent to %s: %s", $message->recipient, $this->channel->error)
918             : sprintf("Report has been sent to %s", $message->recipient);
919             if ($this->channel->error) { skip($msg) }
920             else { yep($msg) }
921             $self->log_debug($msg);
922             } else {
923             my $err = sprintf("Report was not sent to %s: %s", $message->recipient, $this->channel->error || "unknown error");
924             nope($err);
925             $self->log_warning($err);
926             }
927              
928             1;
929             },
930             );
931             unless ($ns) {
932             my $err = sprintf("Report was not sent to %s: %s", $to, $self->notifier->error);
933             nope($err);
934             $self->log_warning($err);
935             }
936             }
937              
938             return $status;
939             });
940              
941             __PACKAGE__->register_handler(
942             handler => "show",
943             description => "Show table data",
944             code => sub {
945             ### CODE:
946             my ($self, $meta, @arguments) = @_;
947             my $store = $self->store;
948             return $self->raise($store->error) if $store->error;
949              
950             # Get all records from DB
951             my %all;
952             foreach my $r ($store->getall) {
953             $all{$r->{name}} = $r;
954             }
955             return $self->raise($store->error) if $store->error;
956              
957             # Check data
958             my $n = scalar(keys %all) || 0;
959             if ($n) {
960             printf("Number of records: %d\n", $n);
961             } else {
962             return skip("No data");
963             }
964              
965             # Show dump
966             if ($self->verbosemode) {
967             print(explain(\%all));
968             return 1;
969             }
970              
971             # Checkets
972             my @checkits = getCheckitByName($self->config("checkit"));
973             my %chckts = ();
974             foreach my $ch (@checkits) {
975             $chckts{$ch->{name}} = $ch;
976             }
977              
978             # Generate table
979             my $src_len = (SCREENWIDTH() - 88);
980             $src_len = 32 if $src_len < 32;
981             my $tbl = Text::SimpleTable->new(
982             [20, 'CHECKIT'],
983             [7, 'TYPE'],
984             [7, 'TARGET'],
985             [$src_len, 'SOURCE STRING'],
986             [19, 'LAST CHECK DATE'],
987             [6, 'INTRVL'], # interval
988             [7, 'RESULT']
989             );
990              
991             # Show table
992             my $status = 1;
993             foreach my $v (sort {$a->{name} cmp $b->{name}} values %all) {
994             my $stv = $v->{status} || 0;
995             my $ostat = -1;
996             if (getBit($stv, 0) && getBit($stv, 1) && getBit($stv, 2)) { # Ok
997             $ostat = 1;
998             } elsif ((getBit($stv, 0) + getBit($stv, 1)) == 0) { # Problem
999             $ostat = 0;
1000             $status = 0;
1001             }
1002              
1003             $tbl->row(
1004             variant_stf($v->{name} // '', 20),
1005             $v->{type} || 'http',
1006             lvalue(\%chckts, $v->{name} // '__default', "target") // 'status',
1007             variant_stf($v->{source} // '', $src_len),
1008             $v->{"time"} ? dtf(DATE_FORMAT, $v->{"time"}) : '',
1009             lvalue(\%chckts, $v->{name} // '__default', "interval") || 0,
1010             $ostat ? $ostat > 0 ? 'PASSED' : 'UNKNOWN' : 'FAILED'
1011             );
1012              
1013             }
1014             $tbl->hr;
1015             $tbl->row('SUMMARY', "", "", "", "", "", $status ? 'PASSED' : 'FAILED');
1016             say $tbl->draw();
1017              
1018             return $status;
1019             });
1020              
1021             sub trigger {
1022 0     0 1   my $self = shift;
1023 0           my %args = @_;
1024 0   0       my $name = $args{name} || 'virtual';
1025 0   0       my $message = $args{message} // "";
1026 0   0       my $source = $args{source} // "";
1027 0           my $subject = $args{subject};
1028              
1029             # Execute triggers
1030 0   0       my $triggers = $args{trigger} || [];
1031 0           my @errs;
1032 0           foreach my $trg (@$triggers) {
1033 0 0         next unless $trg;
1034             my $cmd = dformat($trg, {
1035             SUBJECT => $subject, SUBJ => $subject, SBJ => $subject,
1036             MESSAGE => $message, MSG => $message,
1037             SOURCE => $source, SRC => $source,
1038             NAME => $name,
1039             TYPE => $args{type} // "http",
1040             CODE => $args{code} // '',
1041             STATUS => $args{status} ? 'OK' : 'ERROR',
1042             RESULT => $args{result} ? 'PASSED' : 'FAILED',
1043 0 0 0       NOTE => $args{note} // '',
    0 0        
      0        
1044             });
1045 0           my $exe_err = '';
1046 0           my $exe_out = execute($cmd, undef, \$exe_err);
1047 0 0         my $exe_stt = ($? >> 8) ? 0 : 1;
1048 0 0         if ($exe_stt) {
1049 0           my $msg = sprintf("# %s", $cmd);
1050 0           print cyan MARKER_INFO;
1051 0           say " ", $msg;
1052 0           $self->log_info($msg);
1053 0 0 0       if (defined($exe_out) && length($exe_out) && $self->verbosemode) {
      0        
1054 0           say $exe_out if IS_TTY;
1055 0           $self->log_info($exe_out);
1056             }
1057             } else {
1058 0           my $msg = sprintf("Can't execute trigger %s", $cmd);
1059 0           print red MARKER_FAIL;
1060 0           say " ", $msg;
1061 0           $self->log_error($msg);
1062 0           push @errs, $msg;
1063 0 0         if ($exe_err) {
1064 0           chomp($exe_err);
1065 0           nope($exe_err);
1066 0           $self->log_error($exe_err);
1067 0           push @errs, $exe_err;
1068             }
1069             }
1070             }
1071              
1072 0           return @errs;
1073             }
1074             sub notify {
1075 0     0 1   my $self = shift;
1076 0           my %args = @_;
1077 0   0       my $name = $args{name} || 'virtual';
1078 0   0       my $sendto = $args{sendto} || [];
1079 0           my $subject = $args{subject};
1080 0           my @errors;
1081 0           my $errs = $args{errors};
1082 0 0         push @errors, @$errs if is_array($errs);
1083             #say(explain(\%args));
1084              
1085             # Header
1086 0           my @header;
1087             push @header, (
1088             ["Checkit", $name], # Checkit name
1089             ["Type", $args{type} || 'http'], # Checkit type
1090             ["Result", $args{result} ? 'PASSED' : 'FAILED'], # Checkit result
1091             ["Source", $args{source} || "UNKNOWN"], # Source string
1092             ["Status", $args{status} ? 'OK' : 'ERROR'], # Checkit status (NO RESULT!!);
1093             ["Code", $args{code} // "UNKNOWN"], # Checkit code value
1094             ["Note", $args{note} // "No comments"], # Checkit note string
1095 0 0 0       ["Message", $args{message} // ""], # Checkit message string
    0 0        
      0        
      0        
      0        
1096             );
1097              
1098             # Report
1099 0           my $report = App::MonM::Report->new(name => $name, configfile => $self->configfile);
1100 0 0         $report->title($args{result} ? "checking report" : "error report");
1101 0           $report->common(@header); # Common information
1102 0 0         $report->summary($args{result} ? "All checks successful" : "Errors occurred while checking"); # Summary
1103 0 0         $report->errors(@errors) if @errors; # List of occurred errors
1104 0           $report->footer($self->tms);
1105              
1106             # Send
1107             my $notify_status = $self->notifier->notify(
1108             to => $sendto,
1109             subject => $subject,
1110             message => $report->as_string,
1111             after => sub {
1112 0     0     my $this = shift; # App::MonM::QNotifier object (this)
1113 0           my $message = shift; # App::MonM::Message object
1114 0           my $sent = shift; # Status of sending
1115              
1116 0 0         if ($sent) {
1117 0 0         my $msg = $this->channel->error
1118             ? sprintf("Message was not sent to %s: %s", $message->recipient, $this->channel->error)
1119             : sprintf("Message has been sent to %s", $message->recipient);
1120 0 0         if ($this->channel->error) { print red MARKER_FAIL }
  0            
1121 0           else { print cyan MARKER_INFO }
1122 0           say " ", $msg;
1123 0           $self->log_debug($msg);
1124             } else {
1125 0   0       my $err = sprintf("Message was not sent to %s: %s", $message->recipient, $this->channel->error || "unknown error");
1126 0           print red MARKER_FAIL;
1127 0           print " ";
1128 0           nope($err);
1129 0           $self->log_warning($err);
1130             }
1131              
1132 0           return 1;
1133             },
1134 0           );
1135 0 0         unless ($notify_status) {
1136 0           print red MARKER_FAIL;
1137 0           print " ";
1138 0           nope($self->notifier->error);
1139 0           $self->log_error($self->notifier->error);
1140             }
1141              
1142 0           return 1;
1143             }
1144              
1145             # Private methods
1146             sub _fbytes {
1147 0     0     my $n = int(shift);
1148 0 0         if ($n >= 1024 ** 3) {
    0          
    0          
1149 0           return sprintf "%.3g GB", $n / (1024 ** 3);
1150             } elsif ($n >= 1024 ** 2) {
1151 0           return sprintf "%.3g MB", $n / (1024.0 * 1024);
1152             } elsif ($n >= 1024) {
1153 0           return sprintf "%.3g KB", $n / 1024.0;
1154             } else {
1155 0           return "$n B";
1156             }
1157             }
1158             sub _fdate {
1159 0   0 0     my $d = shift || time;
1160 0   0       my $g = shift || 0;
1161 0 0         return "unknown" unless $d;
1162 0 0         return dtf(DATETIME_GMT_FORMAT, $d, 1) if $g;
1163 0           return dtf(DATETIME_FORMAT . " " . tz_diff(), $d);
1164             }
1165              
1166             1;
1167              
1168             __END__