package Anvil::Tools::Alert; # # This module contains methods used to handle alerts and errors. # use strict; use warnings; use Scalar::Util qw(weaken isweak); our $VERSION = "3.0.0"; my $THIS_FILE = "Alert.pm"; ### Methods; # check_alert_sent # check_condition_age # error # register =pod =encoding utf8 =head1 NAME Anvil::Tools::Alert Provides all methods related to warnings and alerts. =head1 SYNOPSIS use Anvil::Tools; # Get a common object handle on all Anvil::Tools modules. my $anvil = Anvil::Tools->new(); # Access to methods using '$anvil->Alert->X'. Example using 'find'; my $foo_path = $anvil->Storage->find({file => "foo"}); =head1 METHODS Methods in the core module; =cut sub new { my $class = shift; my $self = {}; bless $self, $class; return ($self); } # Get a handle on the Anvil::Tools object. I know that technically that is a sibling module, but it makes more # sense in this case to think of it as a parent. sub parent { my $self = shift; my $parent = shift; $self->{HANDLE}{TOOLS} = $parent if $parent; # Defend against memory leads. See Scalar::Util'. if (not isweak($self->{HANDLE}{TOOLS})) { weaken($self->{HANDLE}{TOOLS}); } return ($self->{HANDLE}{TOOLS}); } ############################################################################################################# # Public methods # ############################################################################################################# =head2 check_alert_sent This method is used to see if an event that might last some time has had an alert send already to recipients. This is used by programs, usually scancore scan agents, that need to track whether an alert was sent when a sensor dropped below/rose above a set alert threshold. For example, if a sensor alerts at 20°C and clears at 25°C, this will be called when either value is passed. When passing the warning threshold, the alert is registered and sent to the user. Once set, no further warning alerts are sent. When the value passes over the clear threshold, this is checked and if an alert was previously registered, it is removed and an "all clear" message is sent. In this way, multiple alerts will not go out if a sensor floats around the warning threshold and a "cleared" message won't be sent unless a "warning" message was previously sent. If there is a problem, C<< !!error!! >> is returned. Parameters; =head3 record_locator This is a record locator, which generally allows a given alert to be tied to a given source. For example, an alert related to a temperature might use C<< an-a01n01.alteeve.com:cpu1_temperature >>. =head3 set_by (required) This is a string, usually the name of the program, that set the alert. Usuall this is simple C<< $THIS_FILE >> or C<< $0 >>. =head3 clear (optional, default '0') If set to C<< 0 >> (set the alert), C<< 1 >> will be returned if this is the first time we've tried to set this alert. If the alert was set before, C<< 0 >> is returned. If set to C<< 1 >> (clear the alert), C<< 1 >> will be returned if this is the alert existed and was cleared. If the alert didn't exist (and thus didn't need to be cleared), C<< 0 >> is returned. =cut sub check_alert_sent { my $self = shift; my $parameter = shift; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $anvil = $self->parent; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Alert->check_alert_sent()" }}); my $record_locator = defined $parameter->{record_locator} ? $parameter->{record_locator} : ""; my $set_by = defined $parameter->{set_by} ? $parameter->{set_by} : ""; my $clear = defined $parameter->{clear} ? $parameter->{clear} : 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { record_locator => $record_locator, set_by => $set_by, clear => $clear, }}); # Do we have an record locator? if (not $record_locator) { # Nope $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Alert->check_alert_sent()", parameter => "record_locator" }}); return("!!error!!"); } # Do we know who is setting this?? if (not $set_by) { # Nope $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Alert->check_alert_sent()", parameter => "set_by" }}); return("!!error!!"); } # This will get set to '1' if an alert is added or removed. my $changed = 0; my $query = " SELECT alert_sent_uuid FROM alert_sent WHERE alert_sent_host_uuid = ".$anvil->Database->quote($anvil->Get->host_uuid)." AND alert_set_by = ".$anvil->Database->quote($set_by)." AND alert_record_locator = ".$anvil->Database->quote($record_locator)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); # Now, if this is clear = 0, register the alert if it doesn't exist. If it is clear = 1, remove the # alert if it exists. my $alert_sent_uuid = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0]; $alert_sent_uuid = "" if not defined $alert_sent_uuid; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { clear => $clear, alert_sent_uuid => $alert_sent_uuid, }}); if ((not $clear) && (not $alert_sent_uuid)) { ### New alert # Make sure this host is in the database... It might not be on the very first run of ScanCore # before the peer exists (tried to connect to the peer, fails, tries to send an alert, but # this host hasn't been added because it is the very first attempt to connect...) if (not $anvil->data->{sys}{host_is_in_db}) { my $query = " SELECT COUNT(*) FROM hosts WHERE host_uuid = ".$anvil->Database->quote($anvil->Get->host_uuid)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); my $count = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { count => $count }}); if (not $count) { # Too early, we can't set an alert. $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "alert", key => "log_0098", variables => { clear => $clear, set_by => $set_by, record_locator => $record_locator, }}); return("!!error!!"); } else { $anvil->data->{sys}{host_is_in_db} = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'sys::host_is_in_db' => $anvil->data->{sys}{host_is_in_db} }}); } } $changed = 1; my $query = " INSERT INTO alert_sent ( alert_sent_uuid, alert_sent_host_uuid, alert_set_by, alert_record_locator, modified_date ) VALUES ( ".$anvil->Database->quote($anvil->Get->uuid).", ".$anvil->Database->quote($anvil->Get->host_uuid).", ".$anvil->Database->quote($set_by).", ".$anvil->Database->quote($record_locator).", ".$anvil->Database->quote($anvil->Database->refresh_timestamp)." ); "; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query, changed => $changed, }}); $anvil->Database->write({query => $query, source => $THIS_FILE, line => __LINE__}); } elsif (($clear) && ($alert_sent_uuid)) { # Alert previously existed, clear it. $changed = 1; my $query = " DELETE FROM alert_sent WHERE alert_sent_uuid = ".$anvil->Database->quote($alert_sent_uuid)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query, changed => $changed, }}); $anvil->Database->write({query => $query, source => $THIS_FILE, line => __LINE__}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { changed => $changed }}); return($changed); } =head2 check_condition_age This checks to see how long ago a given condition (variable, really) has been set. This is generally used when a program, often a scan agent, wants to wait to see if a given state persists before sending an alert and/or taking an action. A common example is seeing how long power has been lost, if a lost sensor is going to return, etc. The age of the condition is returned, in seconds. If there is a problem, C<< !!error!! >> is returned. Parameters; =head3 clear (optional) When set to C<< 1 >>, if the condition exists, it is cleared. If the condition does not exist, nothing happens. =head3 name (required) This is the name of the condition being set. It's a free-form string, but generally in a format like C<< :: >>. =head3 host_uuid (optional) If a condition is host-specific, this can be set to the caller's C<< host_uuid >>. Generally this is needed, save for conditions related to hosted servers that are not host-bound. =cut sub check_condition_age { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Database->check_condition_age()" }}); my $clear = defined $parameter->{clear} ? $parameter->{clear} : 0; my $name = defined $parameter->{name} ? $parameter->{name} : ""; my $host_uuid = defined $parameter->{host_uuid} ? $parameter->{host_uuid} : "NULL"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { clear => $clear, name => $name, host_uuid => $host_uuid, }}); if (not $name) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Database->check_condition_age()", parameter => "name" }}); return("!!error!!"); } my $age = 0; my $source_table = $host_uuid ? "hosts" : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { source_table => $source_table }}); # See if this variable has been set yet. my ($variable_value, $variable_uuid, $epoch_modified_date, $modified_date) = $anvil->Database->read_variable({ debug => $debug, variable_name => $name, variable_source_table => $source_table, variable_source_uuid => $host_uuid, }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { variable_value => $variable_value, variable_uuid => $variable_uuid, epoch_modified_date => $epoch_modified_date, modified_date => $modified_date, }}); if ($variable_uuid) { # Are we clearing? if ($clear) { # Yup $variable_uuid = $anvil->Database->insert_or_update_variables({ debug => $debug, variable_uuid => $variable_uuid, variable_value => "clear", update_value_only => 1, }); } # if the 'clear' parameter isn't set, and the value is 'clear', change it to 'set'. if (($variable_value eq "clear") && (not $clear)) { # Set it. $variable_uuid = $anvil->Database->insert_or_update_variables({ debug => $debug, variable_uuid => $variable_uuid, variable_value => "set", update_value_only => 1, }); } else { # How old is it? $age = time - $epoch_modified_date; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { age => $age }}); return($age); } } elsif (not $clear) { # New, set it. my $variable_uuid = $anvil->Database->insert_or_update_variables({ debug => $debug, variable_name => $name, variable_value => "set", variable_default => "set", variable_description => "striker_0278", variable_section => "conditions", variable_source_uuid => $host_uuid, variable_source_table => $source_table, }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { variable_uuid => $variable_uuid }}); } return($age); } =head2 register This registers an alert to be sent later by C<< Email->send_alerts >>. The C<< alert_uuid >> is returned on success. If anything goes wrong, C<< !!error!! >> is returned. If there are no recipients who would receive the alert, it will not be recorded and an empty string is returned. Parameters; =head3 alert_level (required) This assigns an severity level to the alert. Any recipient listening to this level or higher will receive this alert. This value can be set as a numeric value or as a string. =head4 1 / critical Alerts at this level will go to all recipients, except for those ignoring the source system entirely. This is reserved for alerts that could lead to imminent service interruption or unexpected loss of redundancy. Alerts at this level should trigger alarm systems for all administrators as well as management who may be impacted by service interruptions. =head4 2 / warning This is used for alerts that require attention from administrators. Examples include intentional loss of redundancy caused by load shedding, hardware in pre-failure, loss of input power, temperature anomalies, etc. Alerts at this level should trigger alarm systems for administrative staff. =head4 3 / notice This is used for alerts that are generally safe to ignore, but might provide early warnings of developing issues or insight into system behaviour. Alerts at this level should not trigger alarm systems. Periodic review is sufficient. =head4 4 / info This is used for alerts that are almost always safe to ignore, but may be useful in testing and debugging. =head3 clear_alert (optional, default '0') If set, this indicate that the alert has returned to an OK state. Alert level is still honoured for notification target delivery decisions, but some internal values are adjusted. =head3 message (required) This is the message body of the alert. It is expected to be in the format C<< >>. If variables are to be injected into the C<< string_key >>, a comma-separated list in the format C<< !!variable_name1!value1!![,!!variable_nameN!valueN!!] >> is used. Example with a message alone; C<< foo_0001 >>. Example with two variables; C<< foo_0002,!!bar!abc!!,!!baz!123!! >>. B<< Note >>: See C<< variables >> for an alternate method of passing variables =head3 set_by (required) This is the name of the program that registered this alert. Usually this is simply the caller's C<< $THIS_FILE >> or C<< $0 >> variable. =head3 show_header (optional, default '1') When set to C<< 0 >>, only the alert message body is shown, and the title is omitted. This can be useful when a set of alerts are sorted under a common title. =head3 sort_position (optional, default '9999') This is used to keep a set of alerts in a certain order when converted to an message body. By default, all alerts have a default value of '9999', so they will be sorted using their severity level, and then the time they were entered into the system. If this is set to a number lower than this, then the value here will sort/prioritize messages over the severity/time values. If two or more alerts have the same sort position, then severity and then time stamps will be used. In brief; alert messages are sorted in this order; 1. C<< sort_position >> 2. c<< alert_level >> 3. C<< timestamp >> NOTE: All C<< sort_position >> values are automatically zero-padded (ie: C<< 12 >> -> C<< 0012 >>) to ensure accurate sorting. If you plan to use values greater than C<< 9999 >>, be sure to manually zero-pad your numbers. (Or, better, find a way to make shorter alerts... ). NOTE: The timestamp is generally set for a given program or agent run (set when connecting to the database), NOT by the real time of the database insert. For this reason, relying on the timestamp alone will not generally give the desired results, and why C<< sort_position >> exists. =head3 title (optional) NOTE: If not set and C<< show_header >> is set to C<< 1 >>, a generic title will be added based on the C<< alert_level >> and if C<< clear_alert >> is set or not. This is the title of the alert. It is expected to be in the format C<< >>. If variables are to be injected into the C<< string_key >>, a comma-separated list in the format C<< !!variable_name1!value1!![,!!variable_nameN!valueN!!] >> is used. Example with a message alone; C<< foo_0001 >>. Example with two variables; C<< foo_0002,!!bar!abc!!,!!baz!123!! >>. =head3 variables (optional) This can be set as a hash reference containing key / variable pairs to inject into the message key. the C<< variable => value >> pairs will be appended to the C<< message >> key automatically. This is meant to simplify when an alert is also being longed, or when a large number of variables are being injected into the string. =cut sub register { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Alert->register()" }}); my $alert_level = defined $parameter->{alert_level} ? $parameter->{alert_level} : 0; my $clear_alert = defined $parameter->{clear_alert} ? $parameter->{clear_alert} : 0; my $message = defined $parameter->{message} ? $parameter->{message} : ""; my $set_by = defined $parameter->{set_by} ? $parameter->{set_by} : ""; my $show_header = defined $parameter->{show_header} ? $parameter->{show_header} : 1; my $sort_position = defined $parameter->{sort_position} ? $parameter->{sort_position} : 9999; my $title = defined $parameter->{title} ? $parameter->{title} : ""; my $variables = defined $parameter->{variables} ? $parameter->{variables} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { show_header => $show_header, clear_alert => $clear_alert, alert_level => $alert_level, message => $message, set_by => $set_by, sort_position => $sort_position, title => $title, variables => ref($variables), }}); # Missing parameters? if (not $alert_level) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Alert->register()", parameter => "alert_level" }}); return("!!error!!"); } if (not $set_by) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Alert->register()", parameter => "set_by" }}); return("!!error!!"); } if (not $message) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Alert->register()", parameter => "message" }}); return("!!error!!"); } if (ref($variables) eq "HASH") { foreach my $variable (sort {$a cmp $b} keys %{$variables}) { my $value = defined $variables->{$variable} ? $variables->{$variable} : "undefined:".$variable; $message .= ",!!".$variable."!".$value."!!"; } } # Sort the alerts as they come in, if the sort_position was not set. if ($sort_position == 9999) { if (not defined $anvil->data->{sys}{sort_position}{$alert_level}) { if (($alert_level eq "critical") or ($alert_level eq "1")) { $anvil->data->{sys}{sort_position}{$alert_level} = 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::sort_position::${alert_level}" => $anvil->data->{sys}{sort_position}{$alert_level}, }}); } elsif (($alert_level eq "warning") or ($alert_level eq "2")) { $anvil->data->{sys}{sort_position}{$alert_level} = 20; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::sort_position::${alert_level}" => $anvil->data->{sys}{sort_position}{$alert_level}, }}); } elsif (($alert_level eq "notice") or ($alert_level eq "3")) { $anvil->data->{sys}{sort_position}{$alert_level} = 100; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::sort_position::${alert_level}" => $anvil->data->{sys}{sort_position}{$alert_level}, }}); } else { $anvil->data->{sys}{sort_position}{$alert_level} = 1000; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::sort_position::${alert_level}" => $anvil->data->{sys}{sort_position}{$alert_level}, }}); } } else { $anvil->data->{sys}{sort_position}{$alert_level}++; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::sort_position::${alert_level}" => $anvil->data->{sys}{sort_position}{$alert_level}, }}); } $sort_position = $anvil->data->{sys}{sort_position}{$alert_level}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { sort_position => $sort_position }}); } # If the alert level was a string, convert it to the numerical version. Also check that we've got a # sane alert level at all. if (lc($alert_level) eq "critical") { $alert_level = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { alert_level => $alert_level }}); } elsif (lc($alert_level) eq "warning") { $alert_level = 2; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { alert_level => $alert_level }}); } elsif (lc($alert_level) eq "notice") { $alert_level = 3; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { alert_level => $alert_level }}); } elsif (lc($alert_level) eq "info") { $alert_level = 4; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { alert_level => $alert_level }}); } elsif (($alert_level =~ /\D/) or ($alert_level < 1) or ($alert_level > 4)) { # Invalid $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "error_0142", variables => { alert_level => $alert_level }}); return("!!error!!"); } # Do we need to generate a header? if (($show_header) && (not $title)) { # Set it based on the alert_level. if ($alert_level == 1) { $title = $clear_alert ? "title_0005" : "title_0001"; } # Critical (or Critical Cleared) elsif ($alert_level == 2) { $title = $clear_alert ? "title_0006" : "title_0002"; } # Warning (or Warning Cleared) elsif ($alert_level == 3) { $title = $clear_alert ? "title_0007" : "title_0003"; } # Notice (or Notice Cleared) elsif ($alert_level == 4) { $title = $clear_alert ? "title_0008" : "title_0004"; } # Info (or Info Cleared) $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { title => $title }}); } # zero-pad sort numbers so that they sort properly. $sort_position = sprintf("%04d", $sort_position); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { sort_position => $sort_position }}); # Before we actually record the alert, see if there are any recipients listening. For example, very # rarely is anyone listening to alert level 4 (info), so skipping recording it saves unnecessary # growth of the history.alerts table. my $proceed = 0; $anvil->Database->get_recipients({debug => $debug}); foreach my $recipient_uuid (keys %{$anvil->data->{recipients}{recipient_uuid}}) { my $recipient_email = $anvil->data->{recipients}{recipient_uuid}{$recipient_uuid}{recipient_email}; my $recipient_level = $anvil->data->{recipients}{recipient_uuid}{$recipient_uuid}{level_on_host}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 's1:recipient_uuid' => $recipient_uuid, 's2:recipient_level' => $recipient_level, 's3:recipient_email' => $recipient_email, }}); if ($recipient_level >= $alert_level) { # Someone wants to hear about this. $proceed = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { proceed => $proceed }}); last; } } if (not $proceed) { # No one is listening, ignore. return(""); } # Always INSERT. ScanCore removes them as they're acted on (copy is left in history.alerts). my $alert_uuid = $anvil->Get->uuid(); my $query = " INSERT INTO alerts ( alert_uuid, alert_host_uuid, alert_set_by, alert_level, alert_title, alert_message, alert_sort_position, alert_show_header, modified_date ) VALUES ( ".$anvil->Database->quote($alert_uuid).", ".$anvil->Database->quote($anvil->Get->host_uuid()).", ".$anvil->Database->quote($set_by).", ".$anvil->Database->quote($alert_level).", ".$anvil->Database->quote($title).", ".$anvil->Database->quote($message).", ".$anvil->Database->quote($sort_position).", ".$anvil->Database->quote($show_header).", ".$anvil->Database->quote($anvil->Database->refresh_timestamp)." ); "; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); $anvil->Database->write({query => $query, source => $THIS_FILE, line => __LINE__}); ### TODO: Add an optional 'send_now' parameter to causes us to call 'Email->send_alerts' $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { alert_uuid => $alert_uuid }}); return($alert_uuid); } ### TODO: Write this, maybe? Or remove it and ->warning()? =head2 error =cut # Later, this will support all the translation and logging methods. For now, just print the error and exit. sub error { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Alert->error()" }}); } 1;