Real Time Logging

You are viewing revision #5 of this wiki article.
This is the latest version of this article.
You may want to see the changes made in this revision.

« previous (#4)

  1. Better Formatting
  2. Excluding Categories
  3. CPSLiveLogRoute.php

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 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:
'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',
                                        ),
				),
			),
		),
  1. 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
/**
 * 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;
	}
}