Welcome Guest, Not a member yet? Register   Sign In
Emailing errors generated by the log_message() function
#1

[eluser]alive[/eluser]
I sprinkled the log_message() function willy-nilly throughout my application with the intent that when I moved to production I could extend a core library so that log_message() emails me every "error" it's fed.

Code:
class MY_Log extends CI_Log
{
    private $CI;

    public function MY_Log()
    {
        parent::CI_Log();
        $this->CI =& get_instance();
        $this->CI->load->library('email');
    }
...
}

I tried extending the Log class (which log_message() uses), but found that the base class is not loaded until after the Log class (as described in this thread). This is trouble because I cannot use the CI object (i.e. to load the email class) because it doesn't exist yet. I get the fatal error
Quote:Fatal error: Call to undefined function get_instance() in /[site info]/system/application/libraries/MY_Log.php on line 16

I'm sure I'd be able to concoct some hack wherein a hook passes the CI object to the Log class and things more or less work (or just use native php email functions), but I'm wondering if anyone more knowledgeable can offer a better solution.

Your help is greatly appreciated.
#2

[eluser]bientek[/eluser]
The problem with sending an email on every error is that a warning message generated by code deep inside a nested loop is going to flood your inbox. Email providers might ban you, thinking you are misusing their service.

Here is the solution I ended up using to periodically email errors and save them to the database.

In my MY_Log, code temporarily saves the error info in a flat file for a cron consumer script to process for DB storage and email. Each minute, the cron task reads and deletes the file by calling
Code:
consume_errors()
. File locking is used since otherwise, there could be one or more readers in progress while a writer is also working.

Update 5/15: attempted fix of problem with serialization of certain error messages by calling trim()

Code:
<?php
/**
* MY_Log Class
*
* This library extends the native Log library. It produces temporary error log
* files for a consumer to email and store in a DB.
*
* @package     CodeIgniter
* @subpackage      Libraries
* @category        Logging
*/
class MY_Log extends CI_Log {
    
/**
  * Name of file to lock on when calling _get_lock()
  */
private static $lock_filename = 'serialized_errors_lock.txt';
/**
  * Name of the file to store errors before being saved in the database
  */
private static $error_filename = 'serialized_errors.txt';

/**
     * Constructor
     *
     * @access  public
     */
    function MY_Log()
    {
        parent::CI_Log();
    }

    /**
     * Write Log File
     *
     * Calls the native write_log() method and then sends an email if a log message was generated.
     *
     * @access  public
     * @param   string  the error level
     * @param   string  the error message
     * @param   bool    whether the error is a native PHP error
     * @return  bool
     */
function write_log($level = 'error', $msg, $php_error = FALSE)
{
  if(!isset($_SERVER['HTTP_HOST']))
   return;
  if(isset($_SERVER['SCRIPT_NAME']))
   $msg .= ' --> URL --> http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'].$_SERVER['REQUEST_URI'].' --> IP '.$_SERVER['REMOTE_ADDR'];
  $result = parent::write_log($level, $msg, $php_error);

  // save error to local HDD for consumption by a cron task; it will
  // be moved to DB and emailed to admin
  if ($result == TRUE && strtoupper($level) == 'ERROR') {
   $message = "An error occurred: ".PHP_EOL;
   $message .= $level.' - '.date($this->_date_fmt). ' --> '.trim($msg).PHP_EOL;
  
   $errors  = array();
  
   try {
    $lock_handle = self::_get_lock();

    $existing_errors = self::_get_errors();
    if ($existing_errors != null)
     $errors = $existing_errors;
    $errors[] = array('message' => $message, 'http_host' => $_SERVER['HTTP_HOST'],
      'script_name' => $_SERVER['SCRIPT_NAME'], 'request_uri' => $_SERVER['REQUEST_URI'],
      'remote_addr' => $_SERVER['REMOTE_ADDR'], 'timestamp' => date('Y-m-d H:i:s'));
    $serialized_errors = serialize($errors);
    $fh = fopen(self::$error_filename, 'w');
    if ($fh === FALSE)
     throw new Exception('Could not open error file to log an error message in prep for addition to DB');
    fwrite($fh, $serialized_errors);
    fclose($fh);

    self::_release_lock($lock_handle);
   } catch (Exception $e) {
    parent::write_log('error', $e->getMessage().PHP_EOL.$e->getTraceAsString());
   }
  }
  return $result;
}

private static function _get_lock() {
  $lock_file_handle = fopen(self::$lock_filename, 'w');
  if ($lock_file_handle === FALSE) {
   throw new Exception('Could not get handle on the lockfile "'.self::$lock_filename.'"');
  }
  
  if (flock($lock_file_handle, LOCK_EX) === FALSE) {
   fclose($lock_file_handle);
   throw new Exception('Could not obtain lock on "'.self::$lock_filename.'" in MY_Log::get_errors');
  }
  
  return $lock_file_handle;
}

private static function _release_lock($lock_file_handle) {
  flock($lock_file_handle, LOCK_UN);
  fclose($lock_file_handle);
}

private static function _get_errors() {
  if (file_exists(self::$error_filename)) {
   $fh = fopen(self::$error_filename, 'r');
   $serialized_errors = fread($fh, filesize(self::$error_filename));
   fclose($fh);
   $errors = unserialize($serialized_errors);
   return $errors;
  }
  return null; // file DNE
}

/**
  * Read, delete and return an array of errors; the consumer can process the errors
  * e.g. store them in a database
  * @return array an array of errors
  */
public static function consume_errors() {
  $handle = self::_get_lock();
  $errors = self::_get_errors();
  if ($errors !== NULL) {
   unlink(self::$error_filename);
  }
  self::_release_lock($handle);
  return $errors;
}
}
#3

[eluser]bientek[/eluser]
This code actually produces an error during the unserialize process when something is echoed before the view is displayed. The offset is always near the end of the bytes that are being unserialized. Not sure what's wrong yet.

Quote:A PHP Error was encountered
Severity: Warning
Message: Cannot modify header information - headers already sent by (output started at /system/application/controllers/test.php:20)
Filename: views/header.php
Line Number: 2

A PHP Error was encountered
Severity: Warning
Message: Cannot modify header information - headers already sent by (output started at /system/application/controllers/test.php:20)
Filename: views/header.php
Line Number: 3

Notice: unserialize() [function.unserialize]: Error at offset 607 of 608 bytes in /system/application/libraries/MY_Log.php on line 108
A PHP Error was encountered


Severity: Warning
Message: Cannot modify header information - headers already sent by (output started at /system/application/controllers/test.php:20)
Filename: views/header.php
Line Number: 4
#4

[eluser]bientek[/eluser]
The serialization failure was solved with this change:
Code:
if (file_exists(self::$error_filename))
unlink(self::$error_filename);
Previously, if the file existed, fwrite would only write up to the length of the last file and consequently the serialized array was only partially written. This only happened in a special case - the "headers already sent" type of warning.

Also, I changed the file locking to non-blocking since this caused a server crash when many of the "headers already sent" warnings were generated. The locking still prevents corruption from multiple writers, but it won't actually allow additional errors to be logged at all while one is currently being logged. This is a tradeoff:

Code:
if (flock($lock_file_handle, LOCK_EX | LOCK_NB) === FALSE) {
fclose($lock_file_handle);
throw new Exception('Could not obtain lock on "'.self::$lock_filename.'" in MY_Log::get_errors');
}

Here is the updated code:

Code:
<?php
/**
* MY_Log Class
*
* This library extends the native Log library. It produces temporary error log
* files for a consumer to email and store in a DB.
*
* @package     CodeIgniter
* @subpackage      Libraries
* @category        Logging
*/
class MY_Log extends CI_Log {
    
/**
  * Name of file to lock on when calling _get_lock()
  */
private static $lock_filename = 'serialized_errors_lock.txt';
/**
  * Name of the file to store errors before being saved in the database
  */
private static $error_filename = 'serialized_errors.txt';

/**
     * Constructor
     *
     * @access  public
     */
    function MY_Log()
    {
        parent::CI_Log();
    }

    /**
     * Write Log File
     *
     * Calls the native write_log() method. If a log message was generated, it
   * gets saved to a flat file for consumption and batch processing (e.g.
   * to email and/or store in a database).
     *
     * @access  public
     * @param   string  the error level
     * @param   string  the error message
     * @param   bool    whether the error is a native PHP error
     * @return  bool
     */
function write_log($level = 'error', $msg, $php_error = FALSE)
{
  if(!isset($_SERVER['HTTP_HOST']))
   return;
  if(isset($_SERVER['SCRIPT_NAME']))
   $msg .= PHP_EOL.'URL: http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'].$_SERVER['REQUEST_URI'].PHP_EOL.'IP: '.$_SERVER['REMOTE_ADDR'];  
  $result = parent::write_log($level, $msg, $php_error);

  // save error to local HDD for consumption by a cron task; it will
  // be moved to DB and emailed to admin
  if ($result == TRUE && strtoupper($level) == 'ERROR') {
   $message = "An error occurred: ".$level.' - '.date($this->_date_fmt).PHP_EOL.trim($msg);
   $errors  = array();
  
   try {
    $lock_handle = self::_get_lock();

    $existing_errors = self::_get_errors();
    if ($existing_errors != null)
     $errors = $existing_errors;
    $errors[] = array('message' => $message, 'http_host' => $_SERVER['HTTP_HOST'],
      'script_name' => $_SERVER['SCRIPT_NAME'], 'request_uri' => $_SERVER['REQUEST_URI'],
      'remote_addr' => $_SERVER['REMOTE_ADDR'], 'timestamp' => date('Y-m-d H:i:s'));
    $serialized_errors = serialize($errors);
    if (file_exists(self::$error_filename))
     unlink(self::$error_filename);
    $fh = fopen(self::$error_filename, 'w');
    if ($fh === FALSE)
     throw new Exception('Could not open error file to log an error message');
    fwrite($fh, $serialized_errors);
    fclose($fh);

    self::_release_lock($lock_handle);
   } catch (Exception $e) {
    parent::write_log('error', $e->getMessage().PHP_EOL.$e->getTraceAsString());
   }
  }
  return $result;
}

private static function _get_lock() {
  $lock_file_handle = fopen(self::$lock_filename, 'w');
  if ($lock_file_handle === FALSE) {
   throw new Exception('Could not get handle on the lockfile "'.self::$lock_filename.'"');
  }
  
  if (flock($lock_file_handle, LOCK_EX | LOCK_NB) === FALSE) {
   fclose($lock_file_handle);
   throw new Exception('Could not obtain lock on "'.self::$lock_filename.'" in MY_Log::get_errors');
  }
  
  return $lock_file_handle;
}

private static function _release_lock($lock_file_handle) {
  flock($lock_file_handle, LOCK_UN);
  fclose($lock_file_handle);
}

private static function _get_errors() {
  if (file_exists(self::$error_filename)) {
   $fh = fopen(self::$error_filename, 'r');
   $serialized_errors = fread($fh, filesize(self::$error_filename));
   fclose($fh);
   $errors = unserialize($serialized_errors);
   return $errors;
  }
  return null; // file DNE
}

/**
  * Read, delete and return an array of errors; the consumer can process the errors
  * e.g. store them in a database
  * @return array an array of errors
  */
public static function consume_errors() {
  $handle = self::_get_lock();
  $errors = self::_get_errors();
  if ($errors !== NULL) {
   unlink(self::$error_filename);
  }
  self::_release_lock($handle);
  return $errors;
}
}





Theme © iAndrew 2016 - Forum software by © MyBB