Difference between #5 and #4 of Real Time Logging

unchanged
Title
Real Time Logging
unchanged
Category
Tips
unchanged
Tags
Logging, extending, extensions, debug
changed
Content
I've seen a lot of people asking about the logging facilities in Yii and thought
I'd share a nice little class I wrote that provides near real-time logging.

This is really handy while you're developing a site as sometimes, as you
probably know, when certain fatal errors occur, Yii pukes and doesn't record the
buffered log entries.

My design of this class utilizes the
[error_log](http://php.net/manual/en/function.error-log.php) function in PHP to
write to the log file. I figured that it must be pretty optimized since it's
written in C and is a core PHP function.

So, the class is called CPSLiveLogRoute and is part of my Yii Extension library
but has no dependencies on my extensions. You can use it in any project. Change
the name if you'd like, I don't care.

To use this class:

1. Copy the class to your protected/extensions or protected/components directory
2. Import the class if you're not already including the destination in your
imports section.
3. Add a log route for it in your configuration file:
~~~
[php]
		'log' => array(
			'class' => 'CLogRouter',
			'routes' => array(
				array(
					'class' => 'CPSLiveLogRoute',
					'levels' => 'error, warning, info, trace',
					'maxFileSize' => '10240',
					'logFile' => 'my_app_log_file_name',
                                        //  Optional excluded category
                                        'excludeCategories' => array(
                                                'system.db.CDbCommand',
                                        ),
				),
			),
		),
~~~
4. Enjoy!

Better Formatting
-----------------
I change the output format of the logging through this component as well. While
the Yii stock logging is adequate, this format is easier on the eyes and let's
you pull out what you're looking for more easily.

The new format is:

`Mmm dd hh:mm:ss [category fixed 30 chars] : <L> LOG_ENTRY`

Where: 

Mmm dd hh:mm:ss looks like this: `Feb 11 12:34:56` and `<L>` is the first
letter of the log level (E,W,I, or T).

Here's an example:

    Feb 07 15:00:02 [system.CModule                ] : <T> Loading
"log" application component
    Feb 07 15:00:02 [system.CModule                ] : <T> Loading
"coreMessages" application component 
    Feb 07 15:00:02 [system.CModule                ] : <T> Loading
"request" application component
    Feb 07 15:00:02 [system.CModule                ] : <T> Loading
"db" application component
    Feb 07 15:00:02 [system.db.CDbConnection       ] : <T> Opening DB
connection

Excluding Categories
--------------------
One feature I added was category exclusion. I like to use [Yii::trace()] for
informational debug logging. However, when turning on trace level output, you
get a lot of crap that, frankly, doesn't really do anything but prolong the time
it takes for me to find relevant entries. 

Excluding these categories (like system.db.[CDbCommand]) is simple with this log
router. You can configure the excluded categories in your configuration file or
at runtime.

The class exposes a property called `excludeCategories` and is defined as an
array. It will take literal strings or regular expressions (regex patterns must
be enclosed in slashes (i.e. /^pattern$/) to match. It checks the literal first,
then the regex for performance.

CPSLiveLogRoute.php
-------------------
And, without further ado, here is the class.

~~~
[php]
<?php
/**
 * This file is part of the psYiiExtensions package.
 * 
 * @copyright Copyright &copy; 2009-2011 Pogostick, LLC
 * @link http://www.pogostick.com Pogostick, LLC.
 * @license http://www.pogostick.com/licensing
 * @package psYiiExtensions
 * @subpackage logging
 * @filesource
 * @version $Id$
 */

/**
 * CPSLiveLogRoute utilizes PHP's {@link error_log} function to write logs in
real time
 * 
 * @author Jerry Ablan <jablan@pogostick.com>
 * @since v1.1.0
 */
class CPSLiveLogRoute extends CFileLogRoute
{
	//********************************************************************************
	//* Private Members
	//********************************************************************************
	
	/**
	 * @property array $excludeCategories An array of categories to exclude from
logging. Regex pattern matching is supported via {@link preg_match}
	 */
	protected $_excludeCategories = array();
	public function getExcludeCategories() { return $this->_excludeCategories; }
	public function setExcludeCategories( $value ) { $this->_excludeCategories =
$value; }
		
	//********************************************************************************
	//* Public Methods
	//********************************************************************************
	
	/**
	 * Initialize component
	 */
	public function init()
	{
		parent::init();
		
		//	Write each line out to disk
		Yii::getLogger()->autoFlush = 1;
	}

	/**
	 * Retrieves filtered log messages from logger for further processing.
	 * @param CLogger $logger logger instance
	 * @param boolean $processLogs whether to process the logs after they are
collected from the logger. ALWAYS TRUE NOW!
	 */
	public function collectLogs( $logger, $processLogs = false /* ignored */ )
	{
		parent::collectLogs( $logger, true );
	}
	
	//********************************************************************************
	//* Private Methods
	//********************************************************************************

	/**
	 * Writes log messages in files.
	 * @param array $logs list of log messages
	 */
	protected function processLogs( $logs = array() )
	{
		try
		{
			$_logFile = $this->getLogPath() . DIRECTORY_SEPARATOR .
$this->getLogFile();

			if ( @filesize( $_logFile ) > $this->getMaxFileSize() * 1024 )
				$this->rotateFiles();

			//	Write out the log entries
			foreach ( $logs as $_log )
			{
				$_exclude = false;
				
				//	Check out the exclusions
				if ( ! empty( $this->_excludeCategories ) )
				{
					foreach ( $this->_excludeCategories as $_category )
					{
						//	If found, we skip
						if ( trim( strtolower( $_category ) ) == trim( strtolower( $_log[2] ) ) )
						{
							$_exclude = true;
							break;
						}

						//	Check for regex
						if ( '/' == $_category[0] && 0 != @preg_match( $_category,
$_log[2] ) )
						{
							$_exclude = true;
							break;
						}
					}
				}
					
				/**
				 * 	Use {@link error_log} facility to write out log entry
				 */
				if ( ! $_exclude )
					error_log( $this->formatLogMessage( $_log[0], $_log[1], $_log[2],
$_log[3] ), 3, $_logFile );
			}

			//	Processed, clear!
			$this->logs = null;
		}
		catch ( Exception $_ex )
		{
			error_log( __METHOD__ . ': Exception processing application logs: ' .
$_ex->getMessage() );
		}
	}

	/**
	 * Formats a log message given different fields.
	 * @param string $message message content
	 * @param integer $level message level
	 * @param string $category message category
	 * @param integer $time timestamp
	 * @return string formatted message
	 */
	protected function formatLogMessage( $message, $level = 'I', $category = null,
$time = null )
	{
		if ( null === $time )
			$time = time();
		
		$level = strtoupper( $level[0] );

		return @date( 'M d H:i:s', $time ) . ' [' . sprintf( '%-30s', $category ) . ']
' . ': <' . $level . '> ' . $message . PHP_EOL;
	}
}
~~~