How to create a flood-proof e-mail log in PHP

From Web Services Wiki

Jump to: navigation, search

Contents

Problem

You want to log fatal errors via e-mail so that you are alerted to any immediate problems in a timely manner, but you do not want your inbox flooded with duplicate error messages.

Solution

The following function uses a predefined delay and creates a temporary log file to use as a buffer between messages.

// Location of log file to use as buffer between e-mails
define("EMAIL_ADMIN_LOG_FILE", "/afs/ir.stanford.edu/users/your/home/directory/email.log");
 
// Recipient
define("EMAIL_ADMIN_ADDRESS", "nobody@stanford.edu");
 
// Delay between messages in seconds
define("EMAIL_ADMIN_DELAY", "600");
 
// E-mail subject
define("EMAIL_ADMIN_SUBJECT", "[SU site] Admin error log");
 
 
/**
  * function email_admin($message[, $email[, $logfile[, $delay]]])
  *
  * Author(s): ddonahue
  * Date: May 30, 2008
  * 
  * Creates a buffered log of error messages to send via e-mail
  *
  * Parameters:
  *  $message:    The error message
  *  $email:      The recipient.  Optional.  Default is constant EMAIL_ADMIN_ADDRESS.
  *  $logfile:    Absolute path to log file to use as buffer.  Optional.  Default is EMAIL_ADMIN_LOG_FILE.
  *  $delay:      Delay between sending messages in seconds.  Optional.  Default is EMAIL_ADMIN_DELAY.
  *
  * Returns array:
  *  $result[status]:   True on success, false on failure
  *  $result[message]:  The error message (if applicable)
  */
 
function email_admin($message, $email='', $logfile='', $delay=0) {
 
  // Check message
  if($message == '') {
    return array(status => false, message => "Message is blank");
  }
  else {
    // Prepend timestamp
    $message = "[" . date("M d - h:ia", time()) . "] " . $message;
  }
 
  // Check e-mail
  if($email == '') {
    if(defined("EMAIL_ADMIN_ADDRESS")) {
      $email = EMAIL_ADMIN_ADDRESS;
    }
    else {
      return array(status => false, message => "E-mail is not defined");
    }
  }
 
  if(filter_var($email, FILTER_VALIDATE_EMAIL) == false) {
    return array(status => false, message => "E-mail address is invalid");
  }
 
  // Check log file
  if($logfile == '') {
    if(defined("EMAIL_ADMIN_LOG_FILE")) {
      $logfile = EMAIL_ADMIN_LOG_FILE;
    }
    else {
      return array(status => false, message => "Log file is not defined");
    }
  }
 
  // Check delay
  if($delay == 0) {
    if(defined("EMAIL_ADMIN_DELAY")) {
      $delay = EMAIL_ADMIN_DELAY;
    }
    else {
      return array(status => false, message => "Delay is not defined");
    }
  }
 
  // Check subject
  if(defined("EMAIL_ADMIN_SUBJECT")) {
    $subject = EMAIL_ADMIN_SUBJECT;
  }
  else {
    $subject = "PHP admin_email() error log";
  }
 
  // Open file for reading (suppress warning on nonexistent file)
  if( ($fr = @fopen($logfile, 'r')) == false) {
    // Log file does not exist
 
    // Set the body of the e-mail to the current message
    $body = $message;
 
    // Send the error message via e-mail
    $sendEmail = true;
  }
  else {
    // Log file exists
 
    // Read time of last sent log
    $time_last_sent = intval(fgets($fr, 4096));
 
    if($time_last_sent == 0) {
      return array(status => false, message => "First line of log file is not a timestamp");
    }
 
    else {
 
      // Calculate time to send
      $time_to_send = $time_last_sent + $delay;
 
      if($time_to_send <= time()) {
        // It is time to send the message
 
        // Construct the body
        while (!feof($fr)) {
          $body .= fgets($fr, 4096);
        }
 
        // Append the new error message to the message
        $body .= $message;
 
        // Close the file
        fclose($fr);
 
        // Send the message
        $sendEmail = true;
 
      }
      else {
        // Not time to send, append to log file
 
        // Close the file's read pointer
        fclose($fr);
 
        // Open file for append
        if( ($fa = @fopen($logfile, "a")) == false) {
          return array(status => false, message => "Unable to open log file for append");
        }
 
        // Write the error
        fwrite($fa, "$message\n");
 
        // Close the file
        fclose($fa);        
      }
    }
  }
 
  if($sendEmail == true) {
 
    // Prepend to body
    $body = "The following errors occurred: \n\n" . $body;
 
    // Send message
    if(mail($email, $subject, $body) == false) {
      return array(status => false, message => "mail() returned false, unable to send message");
    }
 
    // Reset the log file
 
    // Open file for write/truncate
    if( ($fw = @fopen($logfile, 'w')) == false) {
      return array(status => false, message => "Unable to open log file for write access");
    }
 
    // Write the time
    $time = time();
 
    fwrite($fw, "$time\n");
 
    // Close the file    
    fclose($fw);
  }
 
  return array(status => true);
}

Sample usage

The examples below explain how to call the function, set and override default arguments, and handle errors.

// Define the constants to use as defaults
 
define("EMAIL_ADMIN_LOG_FILE", "/afs/ir.stanford.edu/users/your/home/directory/email.log");
define("EMAIL_ADMIN_ADDRESS", "nobody@stanford.edu");
define("EMAIL_ADMIN_DELAY", "600");
define("EMAIL_ADMIN_SUBJECT", "[SU site] Admin error log");
 
// Standard usage (with values above)
if($error_occurred) {
  $result = email_admin("An error occurred!");
}
 
// Alternatively, override the default values
if($different_error_occurred) {
  $result = email_admin("A different error occurred!", "nobody@stanford.edu", $path_to_another_log_file, 120);
}
 
// How to perform error handling
$result = email_admin("Error message");
 
if($result[status] == false) {
  echo '<p><strong>Error: </strong> ' . $result[message] . '</p>';
}

Discussion

Why should I use this function?

PHP provides a convenient means of sending e-mail through code, which makes it easy to set up a script to e-mail the administrator when a major function stops working. Unfortunately, it offers no flood control. Often when a major error occurs, such as when the database server is inaccessible, it happens many times in a short period. If you have a script set up to send the admin a message every time one of these errors are raised, the admin would be flooded with messages. The function provided in this recipe resolves this problem by buffering messages and adding a delay between the actual sending of each e-mail. We suggest you set the delay to 10 minutes (600 seconds) or something else more manageable to suit your needs, depending on how often the function is called and the severity of the errors it handles in your application.

How does the function work?

Before the function is called for the first time, the log file is nonexistent. On the first call, the error message is immediately sent via e-mail, and a new, empty log file is created with the current timestamp on the first line. On subsequent calls to the function, the previously written timestamp is added to the delay and compared with the current time. If sufficient time has not passed since the last e-mail, the error is added to the buffer. When enough time has passed since the timestamp was created, the entire buffer including the current error message is sent and the log file is reset with a new timestamp.

What AFS permissions must I give my CGI principal in the log file directory?

This script requires read, write, and insert access on the directory containing the log file used as a buffer. We suggest making a directory specifically for e-mail logs so as to not interfere with the functionality of the rest of your site and also to limit the write and insert abilities necessary for this script to function. Set the permissions as follows.

# Make directory
mkdir email_logs
 
# Enter the directory
cd email_logs
 
# Set permissions
fs sa . [cgi_principal_name] rwi

Read more about setting AFS permissions.

References

Personal tools