<?php
/**
 * DooLog class file.
 *
 * @author Leng Sheng Hong <darkredz@gmail.com>
 * @link http://www.doophp.com/
 * @copyright Copyright &copy; 2009 Leng Sheng Hong
 * @license http://www.doophp.com/license
 */

/**
 * Records log messages in memory.
 *
 * <p>Log various messages type with filters including log levels and log categories.
 * Use DooLogWriter to write logs into files. You can output the log message to the debugger tool for development purpose.</p>
 *
 * @author Leng Sheng Hong <darkredz@gmail.com>
 * @version $Id: DooLog.php 1000 2009-07-7 18:27:22
 * @package doo.logging
 * @since 1.0
 */
class DooLog{
	const EMERG = 0;	//Emergency - system is unusable 
	const ALERT = 1;	//Alert - action must be taken immediately 
	const CRIT = 2;		//Critical - critical conditions 
	const ERR = 3;		//Error - error conditions 
	const WARN = 4;		//Warning - warning conditions 
	const NOTICE = 5;	//Notice - normal but significant condition 
	const INFO = 6;		//Informational - informational messages 
	const TRACE = 7;	//Debug trace - debug messages 
	const PROFILE = 8;	//Profile

    /**
     * Log/Profile rotate file size in KB
     * @var int
     */
    protected $rotate_size = 0;

	private $_levels;
	private $_logs = array();
	private $_profiles = array();
	private $_debug;
	
	function __construct($debug=true){
		$this->_debug = $debug;
		$this->_levels = array('EMERGENCY', 'ALERT', 'CRITICAL', 'ERROR', 'WARNING', 'NOTICE', 'INFO', 'TRACE', 'PROFILE');
	}

    /**
     * Set the Log/Profile rotate file size in KB
     * @param int $size File size in KB
     */
    public function rotateFile($size){
        $this->rotate_size = $size*1024;
    }

    /**
     * Turn on/off debug mode for DooLog
     * Traces will only be logged in debug mode.
     * @param bool $enable
     */
    public function debug($enable){
        $this->_debug = $enable;
    }

    /**
	 * Logs a message.
	 * Messages logged by this method may be retrieved back via {@link getLogs}.
     * @param string $msg
     * @param int $level default DooLog::TRACE
     * @param string $category
     */
	public function log($msg, $level=6, $category='application'){
		$this->_logs[] = array($msg, $level, $category, microtime(true));
	}

	/**
	 * Get log messages.
	 *
	 * <p>Log messages can be filtered by levels and/or categories.
	 * A level filter is specified by a list of levels or a single level
     * A category filter is specified in the same way.</p>
	 *
	 * <p>If you do not specify level filter, it will bring back logs at all levels.
	 * The same applies to category filter.</p>
     * 
	 * @param int level filter
	 * @param string category filter
	 * @return array list of messages. Each array element represents one message
	 * with the structure:
	 * <code>
     * array(
	 *   [0] => message (string)
	 *   [1] => level (int)
	 *   [2] => category (string)
	 *   [3] => timestamp (float, microtime(true)
     * );
     * </code>
	 */
    public function getLogs($levels=NULL,$categories=NULL){
        if(empty($levels) && empty($categories)){
            return $this->_logs;
        }
    }

    //---------------------- logging shorthands ----------------------
    /**
     * Shorthand for logging messages with level Emergency
     * @param string $msg
     * @param string $category
     */
	public function emerg($msg, $category='application'){
		$this->log($msg, 0, $category);
	}

    /**
     * Shorthand for logging messages with level Alert
     * @param string $msg
     * @param string $category 
     */
    public function alert($msg, $category='application'){
		$this->log($msg, 1, $category);
	}

    /**
     * Shorthand for logging messages with level Crtical
     * @param string $msg
     * @param string $category 
     */
    public function crit($msg, $category='application'){
		$this->log($msg, 2, $category);
	}

    /**
     * Shorthand for logging messages with level Error
     * @param string $msg
     * @param string $category
     */
	public function err($msg, $category='application'){
		$this->log($msg, 3, $category);
	}

    /**
     * Shorthand for logging messages with level Warning
     * @param string $msg
     * @param string $category
     */
	public function warn($msg, $category='application'){
		$this->log($msg, 4, $category);
	}

    /**
     * Shorthand for logging messages with level Notice
     * @param string $msg
     * @param string $category
     */
	public function notice($msg, $category='application'){
		$this->log($msg, 5, $category);
	}

    /**
     * Shorthand for logging messages with level Info
     * @param string $msg
     * @param string $category
     */
	public function info($msg, $category='application'){
		$this->log($msg, 6, $category);
	}

    /**
     * Shorthand for logging messages with level Trace
     * Trace messages will only be logged if debug mode is On
     * @param string $msg
     * @param string $category
     */
	public function trace($msg, $category='application'){
		if($this->_debug)
			$this->log($msg, 7, $category);
	}

    //---------------------- General Profiling ----------------------
    /**
     * Marks the start of a profile identified by a unique token and category.
     * @param string $token This must be unique
     * @param string $category Category filter
     */
	public function beginProfile($token, $category='application'){
		$log = array("Begin Profiling $token", 8, $category, microtime(true));
		$this->_logs[] = $log;
		$this->_profiles[$token] = $log;
		$this->_profiles[$token]['startmem'] = $this->memory_used();
	}

    /**
     * Ending of a profile and adds the profile result to memory
     * @param string $token This must be unique
     */
	public function endProfile($token){
		$log = $this->_profiles[$token];
        $log[0] = "End Profiling $token";
		$this->_logs[] = $log;
		$this->_profiles[$token]['result'] = microtime(true) - $this->_profiles[$token][3];
		$this->_profiles[$token]['memory'] = $this->memory_used() - $this->_profiles[$token]['startmem'];
	}

    /**
     * Returns a profile result to get the time and memory used
     * @param string $token This must be unique
     * @return array Profile result in an array
     */
	public function getProfileResult($token){
		return $this->_profiles[$token];
	}

    //---------------------- Database Profiling ----------------------
    /**
     * Marks the start of a database profile identified by a unique token and category.
     * @param string $token This must be unique
     * @param string $category Category filter
     */
	public function beginDbProfile($token, $category='database'){
		$log = array("Begin SQL Profiling $token", 8, $category, microtime(true));
		$this->_logs[] = $log;
		$this->_profiles[$token] = $log;
        $query_index = Doo::db()->getQueryCount();
        $this->_profiles[$token]['sqlstart'] = ($query_index<0)?0:$query_index;
		$this->_profiles[$token]['startmem'] = $this->memory_used();
	}

    /**
     * Ending of a database profile and adds the profile result to memory
     * @param string $token This must be unique
     */
	public function endDbProfile($token){
		$log = $this->_profiles[$token];
        $log[0] = "End SQL Profiling $token";
		$this->_logs[] = $log;
        if($sqls = Doo::db()->showSQL())
            $this->_profiles[$token]['sql'] = implode("\n\r", array_slice($sqls, $this->_profiles[$token]['sqlstart']));
        else
            $this->_profiles[$token]['sql'] = '';
		$this->_profiles[$token]['result'] = microtime(true) - $this->_profiles[$token][3];
		$this->_profiles[$token]['memory'] = $this->memory_used() - $this->_profiles[$token]['startmem'];
	}

    /**
     * Returns a profile result to get the time and memory used
     * @param string $token This must be unique
     * @return array Database profile result in an array
     */
	public function getDbProfileResult($token){
		return $this->_profiles[$token];
	}

    //---------------------- log & profile displaying HTML ----------------------
    /**
     * Return a neatly formatted XML log view, filtered by level or category.
     * @param int $level
     * @param string $category 
     * @return string formatted XML log view
     */
    public function showLogs($xml=true, $level=NULL, $category=NULL){
            $msg = "\n<!-- Generate on ".date('Y-m-d H:i:s', time() )." -->\n";
            $keep = $msg;
            if($level==NULL && $category==NULL){
                foreach($this->_logs as $k=>$p){
                    if($p[0]!=''){
                        if(!$xml){
                            $msg .= $this->formatLog($p[0], $p[1], $p[2], $p[3]);
                        }else{
                            $msg .= $this->formatLogXML($p[0], $p[1], $p[2], $p[3]);
                        }
                    }
                }
            }
            else if($category==NULL){
                foreach($this->_logs as $k=>$p){
                    if($p[0]!='' && $p[1]==$level){
                        if(!$xml){
                            $msg .= $this->formatLog($p[0], $p[1], $p[2], $p[3]);
                        }else{
                            $msg .= $this->formatLogXML($p[0], $p[1], $p[2], $p[3]);
                        }
                    }
                }
            }
            else if($level==NULL){
                foreach($this->_logs as $k=>$p){
                    if($p[0]!='' && $p[2]==$category){
                       if(!$xml){
                            $msg .= $this->formatLog($p[0], $p[1], $p[2], $p[3]);
                        }else{
                            $msg .= $this->formatLogXML($p[0], $p[1], $p[2], $p[3]);
                        }
                    }
                 }
            }else{
                foreach($this->_logs as $k=>$p){
                    if($p[0]!='' && $p[1]==$level && $p[2]==$category){
                       if(!$xml){
                            $msg .= $this->formatLog($p[0], $p[1], $p[2], $p[3]);
                        }else{
                            $msg .= $this->formatLogXML($p[0], $p[1], $p[2], $p[3]);
                        }
                    }
                 }
            }

        if($keep!=$msg)
            return $msg;
        return;
    }

    /**
     * Return a neatly formatted XML debug traces, filtered by level or category.
     * @param string $category
     * @return string formatted XML debug traces
     */
    public function showTraces($xml=true, $category=NULL){
        if($this->_debug)
            return $this->showLogs($xml, self::TRACE , $category);
    }

    /**
     * Return a neatly formatted XML profile view, filtered by level or category.
     * @param string $category
     * @return string formatted XML Profile result
     */
    public function showProfiles($xml=true, $category=NULL){
        $msg = "\n<!-- Generate on ".date('Y-m-d H:i:s', time() )." -->\n";
        $keep = $msg;
        if($category==NULL){
            foreach($this->_profiles as $k=>$p){
                if(!$xml){
                    $msg .= $this->formatProfile($k,$p[2], $p[3], $p['result'], $p['memory']);
                }else{
                    $msg .= $this->formatProfileXML($k,$p[2],$p[3], $p['result'], $p['memory']);
                }
            }
        }else{
            foreach($this->_profiles as $k=>$p){
                if($p[2]!=$category)continue;
                if(!$xml){
                    $msg .= $this->formatProfile($k,$p[2], $p[3], $p['result'], $p['memory']);
                }else{
                    $msg .= $this->formatProfileXML($k,$p[2], $p[3], $p['result'], $p['memory']);
                }
            }
        }
        if($keep!=$msg)
            return $msg;
        return;
    }

    /**
     * Return a neatly formatted String/XML profile view, filtered category 'database'.
     * @param bool $xml Return as XML string
     * @return string formatted String/XML Database profile result
     */
    public function showDbProfiles($xml=true){
        $msg = "\n<!-- Generate on ".date('Y-m-d H:i:s', time() )." -->\n";
        $keep = $msg;
        foreach($this->_profiles as $k=>$p){
            if(isset($p['sql']) && $p['sql']!=''){
                if(!$xml){
                    $msg .= $this->formatDbLog($k, $p['sql'], $p[3], $p['result'], $p['memory']);
                }else{
                    $msg .= $this->formatDbLogXML($k, $p['sql'], $p[3], $p['result'], $p['memory']);                        
                }
            }
        }
        if($keep!=$msg)
            return $msg;
        return;
    }

    /**
     * Writes the Database profiles into a file.
     * @param string $filename File name for the profiles data log.
     * @param bool $xml Whether to write as plain text or XML file.
     */
    public function writeDbProfiles($filename='db.profile.xml', $xml=true){
        $this->writeToFile($this->showDbProfiles($xml), $filename);
    }

    /**
     * Writes the profiles into a file.
     * @param string $filename File name for the profiles data log.
     * @param string $category Filter by a category name
     * @param bool $xml Whether to write as plain text or XML file.
     */
    public function writeProfiles($filename='profile.xml', $xml=true, $category=NULL){
        $this->writeToFile($this->showProfiles($xml,$category), $filename);
    }

    /**
     * Writes the log messages into a file.
     * @param string $filename File name for log.
     * @param int $level Filter by a log level, eg. DooLog::INFO
     * @param string $category Filter by a category name
     * @param bool $xml Whether to write as plain text or XML file.
     */
    public function writeLogs($filename='log.xml', $xml=true, $level=NULL, $category=NULL){
        $this->writeToFile($this->showLogs($xml, $level, $category), $filename);
    }

    /**
     * Writes the Trace messages into a file.
     * @param string $filename File name for trace log.
     * @param bool $xml Whether to write as plain text or XML file.
     */
    public function writeTraces($filename='trace.log.xml', $xml=true){
        $this->writeToFile($this->showTraces($xml), $filename);
    }

    /**
     * Write to file.
     * If rotate file size is set, logs and profiles are automatically rotate when the file size is reached.
     * @param string $data Data string to be logged
     * @param string $filename File name for the log/profile
     */
    protected function writeToFile($data, $filename){
        //only write to file if there's a record
        if($data!=NULL){
            $mode = 'a+';
            if(isset(Doo::conf()->LOG_PATH))
                $filename = Doo::conf()->LOG_PATH . $filename;

            if($this->rotate_size!=0){
                if(file_exists($filename) && filesize($filename) > $this->rotate_size ){
                    $mode = 'w+';
                }
            }
			Doo::loadHelper('DooFile');
			$file = new DooFile(0777);
			$file->create($filename, $data, $mode);
        }
    }

	/**
	 * Returns the memory usage of the current application.
	 * This method relies on the PHP function memory_get_usage().
	 * If it is not available, the method will attempt to use OS programs
	 * to determine the memory usage. A value 0 will be returned if the
	 * memory usage can still not be determined.
	 * @return integer memory usage of the application (in bytes).
	 */
	public function memory_used(){
        //might be disabled
		if(function_exists('memory_get_usage')){
			return memory_get_usage();
        }
        else{
			$output=array();
			if(strncmp(PHP_OS,'WIN',3)===0){
				exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST',$output);
				return isset($output[5])?preg_replace('/[\D]/','',$output[5])*1024 : 0;
			}
			else{
				$pid=getmypid();
				exec("ps -eo%mem,rss,pid | grep $pid", $output);
				$output=explode("  ",$output[0]);
				return isset($output[1]) ? $output[1]*1024 : 0;
			}
		}
	}

    //---------------------- log & profile formatting ----------------------

    /**
     * Format a single log message
     * Example formatted message:
     * <code>2009-6-22 15:21:30 [INFO (6)] [application] User johnny has logined from 60.30.142.85</code>
     * @param string $msg Log message
     * @param int $level Log level
     * @param string $category
     * @param float $time Time used in second
     * @return string A formatted log message
     */
	protected function formatLog($msg,$level,$category,$time){
		return date('Y-m-d H:i:s',$time)." Access from {$_SERVER['REQUEST_URI']} [{$this->_levels[$level]} ($level)] [$category] $msg\n";
	}

    /**
     * Format a single log message
     * Example formatted message:
     * <code>2009/6/22 15:21:30 [INFO (6)] [application] User johnny has logined from 60.30.142.85</code>
     * @param string $msg Log message
     * @param int $level Log level
     * @param string $category
     * @param float $time Time used in second
     * @return string A formatted log message
     */
	protected function formatLogXML($msg,$level,$category,$time){
		return "<log><date>".date('Y-m-d H:i:s',$time)."</date><access_uri><![CDATA[{$_SERVER['REQUEST_URI']}]]></access_uri><level id=\"$level\">{$this->_levels[$level]}</level><category>$category</category><msg><![CDATA[$msg]]></msg></log>\n";
	}


    /**
     * Format a single database log message
     * Example formatted message:
     * <code>[application] # 2009-6-22 15:21:30 Access from /admin/product/edit/18 [time used: 0.002354] [memory used: 1902]
     * SELECT * FROM products</code>
     * @param string $msg_id Profile id
     * @param string $sql SQL of the executed query profiled
     * @param int $time Timestamp where the log was recorded
     * @param float $timeuse Time used in second
     * @param int $memory Memory used in bytes
     * @return string A formatted log message
     */
	protected function formatDbLog($msg_id, $sql,$time, $timeuse, $memory){
		return "[$msg_id] # " . date('Y-m-d H:i:s',$time)." Access from {$_SERVER['REQUEST_URI']}  [time used: {$timeuse}] [memory used: {$memory}]\n$sql\n";
	}


    /**
     * Format a single database log message in XML
     * @param string $sql SQL of the executed query profiled
     * @param int $time Timestamp where the log was recorded
     * @param float $timeuse Time used in second
     * @param int $memory Memory used in bytes
     * @return string A formatted log message
     */
	protected function formatDbLogXML($msg_id, $sql,$time, $timeuse, $memory){
		return "<dbprofile><id>$msg_id</id><date>" . date('Y-m-d H:i:s',$time) .'</date>' ."<access_uri><![CDATA[{$_SERVER['REQUEST_URI']}]]></access_uri><duration>{$timeuse}</duration><memory>{$memory}</memory><sql><![CDATA[$sql]]></sql></dbprofile>\n";
	}

    /**
     * Format a single profile message in XML
     * @param string $msg_id Profile id
     * @param string $category Category
     * @param int $time Timestamp where the log was recorded
     * @param float $timeuse Time used in second
     * @param int $memory Memory used in bytes
     * @return string A formatted log message
     */
	protected function formatProfileXML($msg_id, $category, $time, $timeuse, $memory){
		return "<profile><id>$msg_id</id><category>$category</category><date>" . date('Y-m-d H:i:s',$time) .'</date>' ."<access_uri><![CDATA[{$_SERVER['REQUEST_URI']}]]></access_uri><duration>{$timeuse}</duration><memory>{$memory}</memory></profile>\n";
	}

    /**
     * Format a single profile message
     * Example formatted message:
     * <code>[application] # 2009-6-22 15:21:30 Access from /admin/product/edit/18 [time used: 0.002354] [memory used: 1902]</code>
     * @param string $msg_id Profile id
     * @param string $category Category
     * @param int $time Timestamp where the log was recorded
     * @param float $timeuse Time used in second
     * @param int $memory Memory used in bytes
     * @return string A formatted log message
     */
	protected function formatProfile($msg_id, $category, $time, $timeuse, $memory){
		return "[$category][$msg_id] # " . date('Y-m-d H:i:s',$time)." Access from {$_SERVER['REQUEST_URI']}  [time used: {$timeuse}] [memory used: {$memory}]\n$sql\n";
	}

}
