Tracer class
Tracer class
When you are developing a page and don't have the possibility to run a debugger in your IDE, or more likely if you uploaded your project but sometimes get strange errors and have no idea why, then a tracer can help. I wrote this class at a time (once upon a long time) when the debugger capabilities where so useless that it made no sense to use them. Since that time this class was enhanced a little bit, so that I have my trace points where I want, but can get an email from the server if an exception occurs, see which files are loaded, see the defined variables at the beginning and the end of the script execution and so on. Take a look at the sample source code and the corresponding output to see what's inside:
Sample source code
Wenn man eine Seite entwickelt und nicht die Möglichkeit hat einen Debugger mitlaufen zu lassen (oder wahrscheinlicher, dass das Projekt hochgeladen ist und manchmal eigenartige Fehler auftreten), dann kann ein Tracer eine wirkliche Hilfe sein. Ich habe diese Klasse zu einer Zeit geschrieben, als die IDE Debugger einfach noch zu viele Unzulänglichkeiten hatten, und bin dann auch bei dieser Lösung geblieben. Der Tracer wurde noch ein wenig verbessert, so dass er nun nicht nur die normalen Tracepunkte hat, sondern auch definierte Variablen im Kontext (zu Begin oder am Ende der Skriptausführung) ausgeben kann, oder mir eine eMail schickt, wenn auf meinem Server was schief gelaufen ist. Die Details des Trace-Protokolls sieht der Leser nicht (bzw. nur, wenn nach einer Nutzerprüfung festgestellt wurde, dass er Admin ist). Der Beispielquelltext zeigt, wie es funktioniert und wie die Ausgabe aussieht:
Anwendungsbeispiel
<?php
include_once('swlib/swlib.class.php');
// So you can change the trace level anytime, anywhere in the script
// LOG_NOTICE is equivalent to 5
Tracer::setLevel(LOG_NOTICE);
//
// Let's create a small context
//
class A {
// Traces something
public function traceHere() {
Tracer::trace("I am " . get_class($this));
}
// Calls sub functions, one of them fails ...
public function fail() {
$this->fail1(0, 'i called you');
}
private function fail1($arg0, $arg1) {
$this->fail2($arg0 + 1, crc32($arg1));
}
private function fail2($arg0, $arg1) {
$this->fail3(2 * $arg0, crypt($arg1));
}
private function fail3($arg0, $arg1) {
$this->fail4($arg0 + 10, md5($arg1));
}
private function fail4($arg0, $arg1) {
throw new Exception('Well, that failed');
}
}
// Just to have an inherited class
class B extends A {
}
// Just to have a function in no class scope
function fail(A $a) {
$a->fail();
}
// Implicit traced with high priority (level = 1 == LOG_ALERT)
Tracer::trace('Over Here');
// Traced with trace level LOG_WARNING (== 4)
Tracer::trace('Over there', LOG_WARNING);
// Traced with trace level LOG_DEBUG (== 7)
Tracer::trace('This is not traced, because the trace level is LOG_NOTICE', LOG_DEBUG);
// The class scope is traced as well ...
$a = new A();
$b = new B();
$a->traceHere(); // Shows "A::traceHere() I am B"
$b->traceHere(); // Shows "A::traceHere() I am B", not "B::traceHere() I am B"
// That's the way you trace exceptions if you caught them. The backtrace
// is with function arguments and class scope:
try {
fail($a);
} catch (Exception $e) {
Tracer::traceException($e);
}
// Trace a variable (like print_r) with caption "My array" and level 2
$array = array(1 => 1000, 'key' => 'value');
Tracer::trace_r($array, 'My array', LOG_NOTICE);
// Print out some information, notice that this will be displayed above the
// trace output, because the tracer buffers its own output.
print "Tracer timer = " . Tracer::getTimer() . '<br />';
print "Trace level = " . Tracer::getLevel() . '<br />';
Class source code
Klassen-Quelltext
<?
/**
* Main tracing class, can be extended.
* Traces to output window when Tracer::stop() is called
* if the trace level > 0 or an uncaught Exception occured.
* - Name of recipient: $GLOBALS['config']['admin.name']
* - EMail of recipient: $GLOBALS['config']['admin.email']
*
* The trace level is automatically set if $_GET['trace'] > 0
* The trace level is saved in $_SESSION.
* There is the possibility to set a default trace level using
* - define('TRACER_DEFAULT_LEVEL', <INT VALUE>). This should be
* for debugging only.
*
* If you want to force the Tracer being off (not seen by any
* unauthenticated user) simply call Tracer::disable();
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2010
* @license GPL
* @version 1.0
*/
namespace sw;
class Tracer {
/**
* Tracer class configuration
* @var array
*/
public static $config = array(
'auto_source' => true, // Automatically add file, line, method ...
'level' => -1, // Default level
'append_trace_protocol' => false, // Defines if the user can see the output
'trace_to_file' => '', // File path to trace into (''=off)
'class_levels' => array(), // Keys are the names, values (bool) if traced
'add_context' => false, // Defines if $_SERVER, $_POST, $_GET, etc shall be appended
'add_included_files' => false, // Defines if included flie list shall be appended
'html_tracer_block_prefix' => '<br><pre class="tracer">',
'html_tracer_block_sufffix' => '</pre>',
);
/**
* Contains the context (globals) list if $traceContext == true
* @staticvar array
*/
private static $context = array();
/**
* Global trace level
* @staticvar int
*/
private static $level = -1;
/**
* Global tracer object
* @staticvar tracer
*/
private static $instance = null;
/**
* Global tracer start timer
* @staticvar long
*/
private static $timer;
/**
* Tracer output
* @staticvar array
*/
private static $output = array();
/**
* Contains the information about the last traced exception
* @staticvar Exception[]
*/
private static $exceptions = array();
/**
* Indicator that an uncaught exception occured
* @staticvar bool
*/
private static $hasUncaughgtException = false;
/**
* Tracer configuration. Sets the specified config settings (merges
* with the existing). Returns the actual configuration after the
* new array has been merged to the defaults/previous settings.
* @param array $config
* @return array
*/
public static final function config($config = array()) {
if (!is_array($config)) {
throw new LException(':swlib config is no array', array(':swlib' => 'swlib'));
} else {
self::$config = array_merge(self::$config, $config);
if (!isset(self::$config['class_levels'])) {
self::$config['class_levels'] = array();
}
self::$config['class_levels'] = array_change_key_case(self::$config['class_levels'], CASE_LOWER);
}
return self::$config;
}
/**
* Tracer factory, subclass must be, if specified, a tracer derivate
* @param array $config
*/
public static final function start($config=array()) {
if (!is_null(self::$instance)) {
return self::$instance;
}
if (!empty($config)) {
self::config($config);
}
if (!empty(self::$config['trace_to_file'])) {
$o = "\n----------------------------------------------------------------------------- \n" .
"-- " . $_SERVER['REQUEST_URI'] . "\n" .
"----------------------------------------------------------------------------- \n";
file_put_contents(self::$config['trace_to_file'], $o, FILE_APPEND);
chmod(self::$config['trace_to_file'], 0666);
}
self::$timer = microtime(true);
self::$instance = new self;
self::$level = intval(self::$config['level']);
if (self::$level === 0) {
if (isset($_SESSION['swlib.tracelevel']))
unset($_SESSION['swlib.tracelevel']);
} else if (self::$level > -1) {
$_SESSION["swlib.tracelevel"] = self::$level;
} else if (isset($_SESSION["swlib.tracelevel"]) && is_numeric($_SESSION['swlib.tracelevel'])) {
self::$level = $_SESSION['swlib.tracelevel'];
} else {
self::$level = 0;
if (isset($_SESSION['swlib.tracelevel']))
unset($_SESSION['swlib.tracelevel']);
}
if (self::$config['add_context']) {
self::$context = array(
'GET' => $_GET,
'POST' => $_POST,
'FILES' => $_FILES,
'COOKIE' => $_COOKIE,
'SESSION' => $_SESSION,
'REQUEST' => $_REQUEST,
'HTTP_RESPONSE_HEADERS' => function_exists('headers_list') ? headers_list() : array()
);
}
return self::$instance;
}
/**
* Disables tracer
*/
public static final function stop() {
self::$instance = null;
}
/**
* Construction
*/
private final function __construct() {
}
/**
* Destruction
*/
public final function __destruct() {
if (self::$hasUncaughgtException || (self::$level > 0 && self::$config['append_trace_protocol'])) {
$headers = function_exists('headers_list') ? headers_list() : array();
$headers = str_replace(' ', '', strtolower(implode("\n", $headers)));
if (self::$config['append_trace_protocol']) {
if (preg_match('/content-type[\s]*:[\s]*text\/plain/', $headers) || swlib::isCLIMode()) {
print "\n\n--------------------------------------------------------------------------------\n";
print "-- TRACE\n";
print "--------------------------------------------------------------------------------\n";
print $this->generateProtocol();
print "\n\n";
} else if (preg_match('/content-type[\s]*:[\s]*text\/html/i', $headers)) {
print self::$config['html_tracer_block_prefix'] . htmlspecialchars($this->generateProtocol()) . self::$config['html_tracer_block_sufffix'];
} else {
// No trace otherwise
}
} else {
// No trace yet
}
}
}
/**
* Generates the trace protocol and returns the output as string
* @return string&
*/
protected function & generateProtocol() {
$trace = '';
// Build trace log
foreach (self::$output as $line) {
$trace .= sprintf("T%02d-%s %s %s\n", $line[1], $line[0], $line[2], $line[3]);
}
// Included file list
if (self::$hasUncaughgtException || self::$config['add_included_files']) {
$trace .= "\n";
$trace .= "--------------------------------------------------------------------------------\n";
$trace .= "-- Included files\n";
$trace .= "--------------------------------------------------------------------------------\n";
foreach (get_included_files() as $value) {
$trace .= str_replace($_SERVER['DOCUMENT_ROOT'], '', $value) . "\n";
}
}
// Variable context
if ((self::$hasUncaughgtException || self::$config['add_context']) && !swlib::isCLIMode()) {
$trace .= "\n";
$trace .= "--------------------------------------------------------------------------------\n";
$trace .= "-- Context of global variables\n";
$trace .= "--------------------------------------------------------------------------------\n";
$context = array('SERVER' => $_SERVER);
if (!empty($_GET))
$context['GET'] = $_GET;
if (!empty($_POST))
$context['POST'] = $_POST;
if (!empty($_FILES))
$context['FILES'] = $_FILES;
if (!empty($_COOKIE))
$context['COOKIE'] = $_COOKIE;
if (!empty($_SESSION))
$context['SESSION'] = $_SESSION;
if (!empty($_REQUEST))
$context['REQUEST'] = $_REQUEST;
if (function_exists('apache_request_headers'))
$context['HTTP_REQUEST_HEADERS'] = apache_request_headers();
if (function_exists('headers_list'))
$context['HTTP_RESPONSE_HEADERS'] = headers_list();
if (isset($context['SERVER']['PHP_AUTH_PW'])) {
$context['SERVER']['PHP_AUTH_PW'] = "(Tracer replaced: password)";
}
foreach ($context as $k => $v) {
$trace .= '[' . $k . '] = ' . print_r($v, true) . "\n";
}
}
$trace = trim($trace, " \n\r\t");
if (swlib::isCLIMode()) {
$trace .= "\n";
}
return $trace;
}
/**
* Trace write method.
* @param string $text
* @param int $level
* @param string $source
*/
public final function write($text, $level=0, $source='') {
self::$output[] = array(
self::getTimer(),
$level,
$source,
$text
);
$o = sprintf("T%02d-%s %s %s\n", $level, self::getTimer(), $source, $text);
if (!empty(self::$config['trace_to_file'])) {
file_put_contents(self::$config['trace_to_file'], $o . "\n", FILE_APPEND);
}
}
/**
* Get seconds since tracer factory was called (page was requested)
* @return double
*/
public static final function getTimer() {
return trim(sprintf("%08.3f", 1000 * (microtime(true) - self::$timer)));
}
/**
* Main class trace function
* @param string $text
* @param int $level
* @return void
*/
public static final function trace($text, $level=1) {
if ($level > self::$level || self::$instance == null) {
return;
}
$source = '';
if (self::$config['auto_source']) {
$bt = debug_backtrace();
$d = null;
foreach ($bt as $a) {
if (!(strtolower($a['function']) == 'trace' || !isset($a['class']) || $a['class'] == __CLASS__ || $a['class'] == 'EException')) {
$d = $a;
break;
}
}
unset($bt);
unset($a);
if (is_array($d) && isset($d['function'])) {
$c = isset($d['class']) ? $d['class'] : '';
$source = $c . '::' . $d['function'] . '()';
if (!empty($c)) {
$c = strtolower($c);
// Filter explicitly specified classes
if (isset(self::$config['class_levels'][$c]) && (self::$config['class_levels'][$c] === false || $level > self::$config['class_levels'][$c])) {
return;
}
}
}
}
self::$instance->write(trim($text), $level, $source);
}
/**
* Exception tracing
* @param Exception &$e
* @param bool $returnOnly=false
* @return string
*/
public static final function traceException(&$e, $returnOnly=false) {
self::$exceptions[] = $e;
if (self::$level < 1 || self::$instance == null)
return;
$out = 'Exception("' . $e->getMessage() . '"' . ($e->getCode() != 0 ? (' code=' . $e->getCode()) : '') . ') in ' . str_replace($_SERVER['DOCUMENT_ROOT'], '', $e->getFile()) . "@" . $e->getLine() . "\n";
$out .= "\n" . self::backtrace($e->getTrace());
if(!$returnOnly) self::$instance->write($out, 0, "");
return $out;
}
/**
* Traces uncaught exceptions (called in class EException)
* Uncaught exceptions cause sending an email to the admin.
* @param Exception $e
*/
public static final function traceUncaughtException(&$e) {
self::$hasUncaughgtException = true;
self::traceException($e);
}
/**
* Recursive tracing
* @param mixed &$variable
* @param string& $varname=''
* @param int $level=1
* @return void
*/
public static final function trace_r(&$variable, $varname='', $level=1) {
if ($level <= self::$level && self::$instance != null) {
self::trace(
"\n" . (
(empty($varname) ? '' : $varname . '=') .
print_r($variable, true)
) . '', $level
);
}
}
/**
* Returns a backtrace string, if no argument
* the function uses normal backtrace
* @param array $bt
* @return string
*/
public static function backtrace($bt=null) {
if ($bt == null)
$bt = debug_backtrace();
$out = '';
foreach ($bt as $a) {
if (isset($a['class']) && $a['class'] == __CLASS__) {
continue;
}
$location = '';
if (isset($a['file']))
$location .= trim(str_replace(': runtime-created function', '', str_replace($_SERVER['DOCUMENT_ROOT'], '', $a['file'])));
if (isset($a['line']))
$location .= '@' . $a['line'];
$call = '';
if (isset($a['class']))
$call .= $a['class'];
if (isset($a['function']))
$call .= '::' . $a['function'];
$call .= '(';
if (isset($a['args'])) {
$args = array();
foreach ($a['args'] as $arg) {
switch (gettype($arg)) {
case "boolean":
$args[] = $arg ? 'true' : 'false';
break;
case "integer":
$args[] = $arg;
break;
case "double":
$args[] = $arg;
break;
case "string":
if (strlen($arg) > 250) {
$args[] = '"' . substr($arg, 0, 250) . '" [...]';
} else {
$args[] = '"' . $arg . '"';
}
break;
case "array":
$args[] = "array(" . count($arg) . ")";
break;
case "object":
$args[] = get_class($arg);
break;
case "resource":
$args[] = 'resource(' . get_resource_type($arg) . ')';
break;
case "NULL":
$args[] = 'null';
break;
default:
$args[] = 'UNKNOWN';
}
}
$call .= implode(',', $args);
}
$call .= ')';
$out .= sprintf("%-30s %s\n", $location, $call);
}
return $out;
}
/**
* Traces user defined variables fetched with function get_defined_vars
* (no globals, session, getm post ...)
*/
public static function traceDefinedVariables(array $get_defined_vars) {
$ignore = array('GLOBALS' => '', '_ENV' => '', 'HTTP_ENV_VARS' => '',
'_POST' => '', 'HTTP_POST_VARS' => '', '_GET' => '', 'HTTP_GET_VARS' => '',
'_COOKIE' => '', 'HTTP_COOKIE_VARS' => '', '_SERVER' => '', 'HTTP_SERVER_VARS' => '',
'_FILES' => '', 'HTTP_POST_FILES' => '', '_REQUEST' => '', 'HTTP_SESSION_VARS' => '',
'_SESSION' => ''
);
$get_defined_vars = array_diff_key($get_defined_vars, $ignore);
$out = '';
foreach ($get_defined_vars as $key => $value) {
$out .= $key . '=' . gettype($value) . "<br>";
}
self::trace($out);
}
/**
* Sets the new trace level
* @param int $level
* @param string $class=null
* @return void
*/
public static final function setLevel($level, $class=null) {
if(!is_numeric($level) && !is_bool($level)) {
return;
} else if(empty($class)) {
self::$level = intval($level);
} else if(is_string($class) && !empty($class)) {
$class = strtolower(trim($class));
if(is_numeric($level)) {
$level = $level >= 0 ? $level : false;
} else {
$level = $level ? true : false;
}
self::$config['class_levels'][$class] = $level;
}
}
/**
* Returns the actual trace level.
* @param string $class=null
* @return int
*/
public static final function getLevel($class=null) {
if(empty($class)) {
return self::$level;
} else if (isset(self::$config['class_levels'][$class])) {
return self::$config['class_levels'][$class];
} else {
return null;
}
}
/**
* Sets if the automatic trace source is automatically fetched by
* using backtrace.
* @param bool $enable
* @return bool
*/
public static final function autoSourceEnable($enable=null) {
if (!is_null($enable)) {
self::$config['auto_source'] = !$enable ? false : true;
}
return self::$config['auto_source'];
}
/**
* Purges the trace buffer and returns the actual contents as string
* @return string
*/
public static final function purge() {
$return = self::$instance ? self::$instance->generateProtocol() : '';
self::$output = array();
return $return;
}
/**
* Disables the tracer, optionally returns the protocol logged up to now
* @param bool $getProtocol=false
* @return string
*/
public static final function disable($getProtocol=false) {
$return = ($getProtocol && self::$instance) ? self::$instance->generateProtocol() : '';
self::$level = -1;
self::$output = array();
self::$context = array();
return $return;
}
/**
* Defines/returns if the user can see the trace output on the page (appended
* in a <pre class="tracer"> ... </pre>
* @param bool $userCanSeeTracingsOnPage
* @return bool
*/
public static function appendProtocol($userCanSeeTracingsOnPage=null) {
if (!is_null($userCanSeeTracingsOnPage)) {
self::$config['append_trace_protocol'] = $userCanSeeTracingsOnPage ? true : false;
}
return self::$config['append_trace_protocol'];
}
/**
* Sets/returns if a class is registered as traced or not. Returns true if
* explicitly switched on, false if explicitly switched off, null otherwise.
* @param string $class
* @param bool $level
* @return bool
*/
public static function tracedClass($class, $level=null) {
if(!is_null($level)) {
self::setLevel($level, $class);
} else {
return self::getLevel($class);
}
}
}