App-MonM
view release on metacpan or search on metacpan
lib/App/MonM.pm view on Meta::CPAN
Returns the Notifier object
=item B<notify>
$app->notify();
Sends notifications
=item B<raise>
return $app->raise("Red message");
Sends message to STDERR and returns 0
=item B<store>
my $store = $app->store();
Returns store object
=item B<trigger>
my @errors = $app->trigger();
Runs triggers
=back
=head1 HISTORY
See C<Changes> file
=head1 TO DO
See C<TODO> file
=head1 SEE ALSO
L<CTK>, L<Email::MIME>
=head1 AUTHOR
Serż Minus (Sergey Lepenkov) L<https://www.serzik.com> E<lt>abalama@cpan.orgE<gt>
=head1 COPYRIGHT
Copyright (C) 1998-2022 D&D Corporation. All Rights Reserved
=head1 LICENSE
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See C<LICENSE> file and L<https://dev.perl.org/licenses/>
=cut
use vars qw/ $VERSION /;
$VERSION = '1.09';
use feature qw/ say /;
use Text::SimpleTable;
use File::Spec;
use File::stat qw//;
use Text::ParseWords qw/shellwords quotewords/;
use Text::Wrap qw/wrap/;
use CTK::Skel;
use CTK::Util qw/ preparedir dformat execute dtf tz_diff sendmail variant_stf lf_normalize sharedstatedir /;
use CTK::ConfGenUtil;
use CTK::TFVals qw/ :ALL /;
use App::MonM::Const;
use App::MonM::Util qw/
blue green red yellow cyan magenta gray
yep nope skip wow
getBit setBit
node2anode getCheckitByName
getExpireOffset getTimeOffset explain
slurp spurt
merge
/;
use App::MonM::Store;
use App::MonM::Checkit;
use App::MonM::QNotifier;
use App::MonM::Report;
use parent qw/CTK::App/;
use constant {
TAB9 => " " x 9,
EXPIRES => 24*60*60, # 1 day
SMSSBJ => 'MONM CHECKIT REPORT',
DATE_FORMAT => '%YYYY-%MM-%DD %hh:%mm:%ss',
TABLE_HEADERS => [(
[32, 'NAME'],
[7, 'TYPE'],
[19, 'LAST CHECK DATE'],
[7, 'RESULT'],
)],
# Markers
MARKER_OK => '[ OK ]',
MARKER_FAIL => '[ FAIL ]',
MARKER_SKIP => '[ SKIP ]',
MARKER_INFO => '[ INFO ]',
};
eval { require App::MonM::Notifier };
my $NOTIFIER_LOADED = 1 unless $@;
$NOTIFIER_LOADED = 0 if $NOTIFIER_LOADED && (App::MonM::Notifier->VERSION * 1) < 1.04;
sub again {
my $self = shift;
$self->SUPER::again(); # CTK::App again first!!
# Datadir & Tempdir
if ($self->option("datadir")) {
# Prepare DataDir
preparedir( $self->datadir() ) or do {
$self->status(0);
$self->raise("Can't prepare directory %s", $self->datadir());
};
} elsif ($self->option("daemondir")) {
$self->datadir(File::Spec->catdir(sharedstatedir(), PREFIX));
} else {
$self->datadir($self->tempdir());
}
# Prepare TempDir
preparedir( $self->tempdir() ) or do {
$self->status(0);
$self->raise("Can't prepare directory %s", $self->tempdir());
};
# Store
my $db_file = File::Spec->catfile($self->datadir, App::MonM::Store::DB_FILENAME());
my $store_conf = $self->config("store") || $self->config('dbi') || {file => $db_file};
$store_conf = {file => $db_file} unless is_hash($store_conf);
my %store_args = %$store_conf;
$store_args{file} = $db_file unless ($store_args{file} || $store_args{dsn});
my $store = App::MonM::Store->new(%store_args);
$self->{store} = $store;
#$self->debug(explain($store));
# Notifier object init
my %nargs = (config => $self->configobj);
$self->{notifier} = $NOTIFIER_LOADED && lvalue($self->config("usemonotifier"))
? App::MonM::Notifier->new(%nargs)
: App::MonM::QNotifier->new(%nargs);
#$self->status($self->raise("Test error"));
return $self; # CTK requires!
}
sub raise {
my $self = shift;
say STDERR red(@_);
$self->log_error(sprintf(shift, @_));
return 0;
}
sub store {
my $self = shift;
return $self->{store};
}
sub notifier {
my $self = shift;
return $self->{notifier};
}
__PACKAGE__->register_handler(
handler => "info",
description => "Show statistic information",
code => sub {
### CODE:
my ($self, $meta, @arguments) = @_;
my $store = $self->store;
# General info
printf("Hostname : %s\n", HOSTNAME);
printf("MonM version : %s\n", $self->VERSION);
printf("Date : %s\n", _fdate());
printf("Data dir : %s\n", $self->datadir);
printf("Temp dir : %s\n", $self->tempdir);
printf("Config file : %s\n", $self->configfile);
printf("Config status : %s\n", $self->conf("loadstatus") ? green("OK") : magenta("ERROR: not loaded"));
$self->raise($self->configobj->error) if !$self->configobj->status and length($self->configobj->error);
printf("Notifier class : %s\n", ref($self->notifier) || magenta("not initialized"));
#$self->debug(explain($self->config)) if $self->conf("loadstatus") && $self->verbosemode;
# DB status
printf("DB DSN : %s\n", $store->dsn);
printf("DB status : %s\n", $store->error ? red("ERROR") : green("OK"));
my $db_is_ok = $store->error ? 0 : 1;
if ($db_is_ok && $store->{file} && -e $store->{file}) {
my $s = File::stat::stat($store->{file})->size;
printf("DB file : %s\n", $store->{file});
printf("DB size : %s\n", sprintf("%s (%d bytes)", _fbytes($s), $s));
printf("DB modified : %s\n", _fdate(File::stat::stat($store->{file})->mtime || 0));
}
$self->raise($store->error) unless $db_is_ok;
# Checkets
my @checkits = getCheckitByName($self->config("checkit"));
my $noc = scalar(@checkits);
printf("Checkits : %s\n", $noc ? $noc : yellow("none"));
if ($noc) {
#print explain(\@checkits);
my $tbl = Text::SimpleTable->new(
[20, 'CHECKIT NAME'], # name
[7, 'TYPE'], # type
[7, 'TARGET'], # target
[6, 'INTRVL'], # interval
[3, 'TRG'], # trigger
[27, 'RECIPIENTS'], # sendto
);
foreach my $ch (@checkits) {
my $triggers = array($ch, "trigger");
lib/App/MonM.pm view on Meta::CPAN
printf(" Usr=%s; Ch=%s; At=%s\n", $u, $ch_name, $scheduler->getAtString($ch_name));
}
} continue {
$old = $u;
}
unless (%$channels_usr) {
$tbl->row( $u, '', '', '', '-------' );
}
}
print $tbl->draw();
}
# Groups
my @groups = $self->notifier->getGroups;
printf("Allowed groups : %s\n", @groups ? join(", ", @groups) : yellow("none"));
if (@groups) {
my $tbl = Text::SimpleTable->new(
[20, 'GROUP NAME'],
[62, 'USERS'],
);
foreach my $g (sort {$a cmp $b} @groups) {
my @us = $self->notifier->getUsersByGroup($g);
$tbl->row(
$g,
join(", ", @us),
);
}
print $tbl->draw();
}
#print explain([$self->notifier->getUsersByGroup("Bar")]);
return 1;
});
__PACKAGE__->register_handler(
handler => "configure",
description => "Generate configuration files",
code => sub {
### CODE:
my ($self, $meta, @arguments) = @_;
my $store = $self->store;
my $dir = shift(@arguments) || $self->root;
# Creating configuration
my $skel = CTK::Skel->new(
-name => PROJECTNAME,
-root => $dir,
-skels => {
config => 'App::MonM::ConfigSkel',
},
-vars => {
PROJECT => PROJECTNAME,
PROJECTNAME => PROJECTNAME,
PREFIX => PREFIX,
},
-debug => $self->verbosemode,
);
printf("Installing configuration to \"%s\"...\n", $dir);
if ($skel->build("config")) {
say green("Done. Configuration has been installed");
} else {
return $self->raise("Can't install configuration");
}
return 1;
});
__PACKAGE__->register_handler(
handler => "checkit",
description => "Checkit",
code => sub {
### CODE:
my ($self, $meta, @arguments) = @_;
my $store = $self->store;
return $self->raise($store->error) if $store->error;
# Check configuration
unless ($self->configobj->status) {
return length($self->configobj->error)
? $self->raise($self->configobj->error)
: "Can't load configuration file";
}
# Get checkits
my @checkits = getCheckitByName($self->config("checkit"), @arguments);
my $noc = scalar(@checkits);
unless ($noc) {
skip("No enabled <Checkit> configuration section found");
$self->log_info("No enabled <Checkit> configuration section found");
return 1;
}
# Create Checkit object
my $checker = App::MonM::Checkit->new;
# Get all records from DB
my %all;
foreach my $r ($store->getall) {
$all{$r->{name}} = $r;
}
return $self->raise($store->error) if $store->error;
# print explain(\@checkits);
# Start
my $curtime = time;
my $status = 1;
my $passed = 0;
foreach my $checkit (sort {$a->{name} cmp $b->{name}} @checkits) {
my $result = 1; # Check result
my $name = $checkit->{name};
my $info = $all{$name} || {}; # from database
my $id = $info->{id} || 0;
my $old = $info->{status} || 0;
my $got = ($old << 1) & 15;
my $pub = $info->{'time'} || 0;
my $interval = getTimeOffset(lvalue($checkit, "interval") || 0);
# Check interval first
if ($interval) {
if (($pub + $interval) >= $curtime) {
print gray MARKER_SKIP;
printf(" %s (%s)\n", $name, "Too little time has passed before a next check [delay $interval sec]");
next;
}
}
# Check
$result = $checker->check($checkit);
if ($result) {
$got = setBit($got, 0); # Set first bit if result is PASSED
$passed++;
} else {
$status = 0; # General status
}
# Show resulsts
print $result ? green(MARKER_OK) : red(MARKER_FAIL);
printf(" %s (%s >>> %s)\n", $name, $checker->source, $checker->message);
if ($self->verbosemode) {
printf "%sStatus=%s; Code=%s\n", TAB9,
$checker->status || 0, $checker->code // '';
say TAB9, $checker->note;
if (defined($checker->content) && length($checker->content)) {
$Text::Wrap::columns = SCREENWIDTH - 10;
say TAB9, "-----BEGIN CONTENT-----";
say wrap(TAB9, TAB9, lf_normalize($checker->content));
say TAB9, "-----END CONTENT-----";
}
}
if ($result && !$checker->status) {
wow("%s", $checker->error);
} elsif (!$result) {
nope("%s", $checker->error);
}
# Save data to database
my %data = (
id => $id,
name => $name, # Checkit name
type => $checker->type, # Checkit type
result => $result, # Checkit result
source => $checker->source, # Source string
code => $checker->code, # Checkit code value
message => $checker->message, # Checkit message string
note => $checker->note, # Checkit note string
status => $got, # New status value (code of result) for store only!
subject => sprintf("%s: Available %s [%s]", $result ? 'OK' : 'PROBLEM', $name, HOSTNAME), # Subject
);
my $chst = $id ? $store->set(%data) : $store->add(%data);
unless ($chst) {
$self->raise($store->error) if $store->error;
$status = 0;
next;
}
# Triggers and notifications
# GOT = [0-0-1-1] = 3 -- OK
# GOT = [1-1-0-0] = 12 -- PROBLEM
if ($got == 3 or $got == 12) {
my @errs;
push @errs, $checker->error if $checker->error; # Checkit error string
$data{status} = $checker->status; # Checkit status (NO RESULT!!);
# Run triggers (FIRST)
push @errs, $self->trigger(%data, trigger => array($checkit, "trigger"));
# Send message via notifier (SECOND)
$self->notify(%data, sendto => array($checkit, "sendto"), errors => \@errs);
}
}
# Show Total resulsts
print $status ? green(MARKER_OK) : red(MARKER_FAIL);
printf(" Total passed %d checks of %d in %s\n", $passed, $noc, $self->tms());
# Cleaning DB
my $expire = getExpireOffset(lvalue($self->config("expires"))
|| lvalue($self->config("expire")) || EXPIRES);
$store->clean(period => $expire) or do {
return $store->error ? $self->raise($store->error) : 0;
};
return $status;
});
__PACKAGE__->register_handler(
handler => "remind",
lib/App/MonM.pm view on Meta::CPAN
# Init
my (@errors, @table);
my $status = 1;
my $tbl = Text::SimpleTable->new(@{(TABLE_HEADERS)});
# Header
my @header;
push @header, ["Hostname", HOSTNAME];
push @header, ["Database DSN", $store->dsn];
my $db_is_ok = $store->error ? 0 : 1;
push @header, ["Database status", $db_is_ok ? "OK" : "ERROR"];
unless ($db_is_ok) {
push @errors, $store->dsn, $store->error, "";
$status = $self->raise("%s: %s", $store->dsn, $store->error);
}
# Get checkits from config
my @checkits = getCheckitByName($self->config("checkit"));
my $noc = scalar(@checkits);
push @header, ["Number of checks", $noc ? $noc : "no checks"];
unless ($noc) {
skip("No enabled <Checkit> configuration section found");
$self->log_info("No enabled <Checkit> configuration section found");
$status = 0;
}
# Get all records from DB
my %all;
if ($db_is_ok) {
foreach my $r ($store->getall) {
$all{$r->{name}} = $r;
}
if ($store->error) {
push @errors, $store->dsn, $store->error, "";
$status = $self->raise("%s: %s", $store->dsn, $store->error);
}
}
# Checkits
if ($status) {
foreach my $checkit (sort {$a->{name} cmp $b->{name}} @checkits) {
my $name = $checkit->{name};
my $info = $all{$name} || {};
my $last = $info->{"time"} || 0;
my $v = $info->{status} || 0;
my $ostat = -1;
if (getBit($v, 0) && getBit($v, 1) && getBit($v, 2)) { # Ok
$ostat = 1;
} elsif ((getBit($v, 0) + getBit($v, 1)) == 0) { # Problem
$ostat = 0;
$status = 0;
}
$tbl->row($name, $info->{type} || 'http',
$last ? dtf(DATE_FORMAT, $last) : "",
$ostat ? $ostat > 0 ? 'PASSED' : 'UNKNOWN' : 'FAILED',
);
unless ($ostat) {
push @errors, sprintf("%s (%s >>> %s)", $name, $info->{source} || '', $info->{message} || ''), "";
}
#say(explain($info));
}
$tbl->hr;
}
$tbl->row('SUMMARY', "", "", $noc ? $status ? 'PASSED' : 'FAILED' : 'UNKNOWN');
# Get SendMail config
my $sendmail = hash($self->config('channel'), "SendMail");
# Get output file
my $outfile = $self->option("outfile");
if ($outfile) {
unless (File::Spec->file_name_is_absolute($outfile)) {
$outfile = File::Spec->catfile($self->datadir, $outfile);
}
}
# Get To value
my $to = scalar(@arguments)
? join(", ", @arguments)
: uv2null(value($sendmail, "to"));
my $send_report = 1 if $to && $to !~ /\@example.com$/;
$send_report = 0 if $outfile;
push @header, ["Send report to", $to] if $send_report;
push @header, ["Summary result", $status ? 'PASSED' : 'FAILED'];
# Report generate
my $report = App::MonM::Report->new(name => "last checks", configfile => $self->configfile);
my $report_title = $status ? "checking report" : "error report";
$report->common(@header); # Add common information
$report->summary( # Add summary table
$status ? "All last checks successful" : "Errors occurred while checking",
$tbl->draw(), # Add report table
);
$report->errors(@errors); # Add list of occurred errors
if ($outfile) {
$report->abstract(sprintf("The %s for last checks on %s\n", $report_title, HOSTNAME));
if (my $err = spurt($outfile, $report->as_string)) {
nope($err);
$self->log_error($err);
} else {
my $msg = sprintf("The report successfully saved to file: %s", $outfile);
yep($msg);
$self->log_debug($msg);
}
return $status;
} elsif ($self->verbosemode) { # Draw to STDOUT
printf("%s BEGIN REPORT ~~~\n", "~" x (SCREENWIDTH()-17)) if IS_TTY;
printf("The %s for last checks on %s\n\n", $report_title, HOSTNAME);
print $report->as_string;
printf("%s END REPORT ~~~\n", "~" x (SCREENWIDTH()-15)) if IS_TTY;
}
# Send report
if ($send_report) {
$report->title($report_title);
$report->footer($self->tms);
# Send
my $ns = $self->notifier->notify(
to => $to,
lib/App/MonM.pm view on Meta::CPAN
# Check data
my $n = scalar(keys %all) || 0;
if ($n) {
printf("Number of records: %d\n", $n);
} else {
return skip("No data");
}
# Show dump
if ($self->verbosemode) {
print(explain(\%all));
return 1;
}
# Checkets
my @checkits = getCheckitByName($self->config("checkit"));
my %chckts = ();
foreach my $ch (@checkits) {
$chckts{$ch->{name}} = $ch;
}
# Generate table
my $src_len = (SCREENWIDTH() - 88);
$src_len = 32 if $src_len < 32;
my $tbl = Text::SimpleTable->new(
[20, 'CHECKIT'],
[7, 'TYPE'],
[7, 'TARGET'],
[$src_len, 'SOURCE STRING'],
[19, 'LAST CHECK DATE'],
[6, 'INTRVL'], # interval
[7, 'RESULT']
);
# Show table
my $status = 1;
foreach my $v (sort {$a->{name} cmp $b->{name}} values %all) {
my $stv = $v->{status} || 0;
my $ostat = -1;
if (getBit($stv, 0) && getBit($stv, 1) && getBit($stv, 2)) { # Ok
$ostat = 1;
} elsif ((getBit($stv, 0) + getBit($stv, 1)) == 0) { # Problem
$ostat = 0;
$status = 0;
}
$tbl->row(
variant_stf($v->{name} // '', 20),
$v->{type} || 'http',
lvalue(\%chckts, $v->{name} // '__default', "target") // 'status',
variant_stf($v->{source} // '', $src_len),
$v->{"time"} ? dtf(DATE_FORMAT, $v->{"time"}) : '',
lvalue(\%chckts, $v->{name} // '__default', "interval") || 0,
$ostat ? $ostat > 0 ? 'PASSED' : 'UNKNOWN' : 'FAILED'
);
}
$tbl->hr;
$tbl->row('SUMMARY', "", "", "", "", "", $status ? 'PASSED' : 'FAILED');
say $tbl->draw();
return $status;
});
sub trigger {
my $self = shift;
my %args = @_;
my $name = $args{name} || 'virtual';
my $message = $args{message} // "";
my $source = $args{source} // "";
my $subject = $args{subject};
# Execute triggers
my $triggers = $args{trigger} || [];
my @errs;
foreach my $trg (@$triggers) {
next unless $trg;
my $cmd = dformat($trg, {
SUBJECT => $subject, SUBJ => $subject, SBJ => $subject,
MESSAGE => $message, MSG => $message,
SOURCE => $source, SRC => $source,
NAME => $name,
TYPE => $args{type} // "http",
CODE => $args{code} // '',
STATUS => $args{status} ? 'OK' : 'ERROR',
RESULT => $args{result} ? 'PASSED' : 'FAILED',
NOTE => $args{note} // '',
});
my $exe_err = '';
my $exe_out = execute($cmd, undef, \$exe_err);
my $exe_stt = ($? >> 8) ? 0 : 1;
if ($exe_stt) {
my $msg = sprintf("# %s", $cmd);
print cyan MARKER_INFO;
say " ", $msg;
$self->log_info($msg);
if (defined($exe_out) && length($exe_out) && $self->verbosemode) {
say $exe_out if IS_TTY;
$self->log_info($exe_out);
}
} else {
my $msg = sprintf("Can't execute trigger %s", $cmd);
print red MARKER_FAIL;
say " ", $msg;
$self->log_error($msg);
push @errs, $msg;
if ($exe_err) {
chomp($exe_err);
nope($exe_err);
$self->log_error($exe_err);
push @errs, $exe_err;
}
}
}
return @errs;
}
sub notify {
my $self = shift;
my %args = @_;
my $name = $args{name} || 'virtual';
my $sendto = $args{sendto} || [];
my $subject = $args{subject};
my @errors;
my $errs = $args{errors};
push @errors, @$errs if is_array($errs);
#say(explain(\%args));
# Header
my @header;
push @header, (
["Checkit", $name], # Checkit name
["Type", $args{type} || 'http'], # Checkit type
["Result", $args{result} ? 'PASSED' : 'FAILED'], # Checkit result
["Source", $args{source} || "UNKNOWN"], # Source string
["Status", $args{status} ? 'OK' : 'ERROR'], # Checkit status (NO RESULT!!);
["Code", $args{code} // "UNKNOWN"], # Checkit code value
["Note", $args{note} // "No comments"], # Checkit note string
["Message", $args{message} // ""], # Checkit message string
);
# Report
my $report = App::MonM::Report->new(name => $name, configfile => $self->configfile);
$report->title($args{result} ? "checking report" : "error report");
$report->common(@header); # Common information
$report->summary($args{result} ? "All checks successful" : "Errors occurred while checking"); # Summary
$report->errors(@errors) if @errors; # List of occurred errors
$report->footer($self->tms);
# Send
my $notify_status = $self->notifier->notify(
to => $sendto,
subject => $subject,
message => $report->as_string,
before => sub {
my $this = shift; # App::MonM::QNotifier object (this)
my $message = shift; # App::MonM::Message object
# Check internal errors
if ($this->error) {
nope($this->error);
$self->log_error($this->error);
}
return 1;
},
after => sub {
my $this = shift; # App::MonM::QNotifier object (this)
my $message = shift; # App::MonM::Message object
my $sent = shift; # Status of sending
# Check internal errors
if ($this->error) {
nope($this->error);
$self->log_error($this->error);
}
# Check sending status
if ($sent) {
my $msg = $this->channel->error
? sprintf("Message was not sent to %s: %s", $message->recipient, $this->channel->error)
: sprintf("Message has been sent to %s", $message->recipient);
if ($this->channel->error) { print red MARKER_FAIL }
else { print cyan MARKER_INFO }
say " ", $msg;
$self->log_debug($msg);
} else {
my $err = sprintf("Message was not sent to %s: %s", $message->recipient, $this->channel->error || "unknown error");
print red MARKER_FAIL;
print " ";
nope($err);
$self->log_warning($err);
}
return 1;
},
);
unless ($notify_status) {
print red MARKER_FAIL;
print " ";
nope($self->notifier->error);
$self->log_error($self->notifier->error);
}
return 1;
}
# Private methods
sub _fbytes {
my $n = int(shift);
if ($n >= 1024 ** 3) {
return sprintf "%.3g GB", $n / (1024 ** 3);
} elsif ($n >= 1024 ** 2) {
return sprintf "%.3g MB", $n / (1024.0 * 1024);
} elsif ($n >= 1024) {
return sprintf "%.3g KB", $n / 1024.0;
} else {
return "$n B";
}
}
sub _fdate {
my $d = shift || time;
my $g = shift || 0;
return "unknown" unless $d;
return dtf(DATETIME_GMT_FORMAT, $d, 1) if $g;
return dtf(DATETIME_FORMAT . " " . tz_diff(), $d);
}
1;
__END__
( run in 1.249 second using v1.01-cache-2.11-cpan-d7a12ab2c7f )