common/BaseLogger.php
<?php
/*******************************************************************************
Copyright 2015 Whole Foods Co-op
This file is part of CORE-POS.
IT CORE is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
IT CORE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
in the file license.txt along with IT CORE; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*********************************************************************************/
namespace COREPOS\common;
/**
@class BaseLogger
Base class defining common logging functionality
Handles syslog RFC log levels and output format
Also supports remote syslog.
Child classes should at minimum override
the getLogLocation method to specify a proper
output file. They may also override the
verboseDebugging method and programName property
if needed.
*/
class BaseLogger
{
/**
Map log level integers to
text level names
*/
protected $logLevelMap = array(
0 => 'emergency',
1 => 'alert',
2 => 'critical',
3 => 'error',
4 => 'warning',
5 => 'notice',
6 => 'info',
7 => 'debug',
);
/**
Remote syslog settings
*/
protected $syslogHost = false;
protected $syslogPort = 514;
protected $syslogProtocol = 'udp';
/**
Name of program in log lines
*/
protected $programName = 'corepos';
/**
Log level constants
*/
const EMERGENCY = 0;
const ALERT = 1;
const CRITICAL = 2;
const ERROR = 3;
const WARNING = 4;
const NOTICE = 5;
const INFO = 6;
const DEBUG = 7;
/**
Normalize log level argument
@param $level [int or string] log level
@return [string] log level description
as lowercase
*/
protected function normalizeLevel($level)
{
if (isset($this->logLevelMap[$level])) {
return $this->logLevelMap[$level];
}
return strtolower($level);
}
/**
Set values for remote syslog
@param $host [string] host name or IP
@param $port [int, optional] default 514
@param $protocol [string, optional] default 'udp'.
Alternative is 'tcp'.
*/
public function setRemoteSyslog($host, $port=514, $protocol='udp')
{
$this->syslogHost = $host;
$this->syslogPort = $port;
$this->syslogProtocol = $protocol;
}
protected function getSyslogSocket()
{
if (!function_exists('socket_create')) {
throw new \Exception('Sockets extension required for remote logging');
}
$socket = false;
if (strtolower($this->syslogProtocol) === 'udp') {
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
} elseif (strtolower($this->syslogProtocol) === 'tcp') {
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
}
if ($socket === false) {
throw new \Exception('Remote logging socket error: ' . socket_last_error());
}
return $socket;
}
/**
Send message to remote syslog
@param $message [string] log line
@return [boolean] success
*/
protected function syslogRemote($message)
{
$context = array('skip_remote' => true);
try {
$socket = $this->getSyslogSocket();
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 5, 'usec' => 0));
if (!socket_connect($socket, $this->syslogHost, $this->syslogPort)) {
$this->debug('Unable to connect to ' . $this->syslogHost . ':' . $this->syslogPort
. ' for remote logging. Error: ' . socket_last_error(), $context);
}
socket_write($socket, $message);
socket_close($socket);
} catch (\Exception $ex) {
$this->debug($ex->getMessage(), $context);
}
return true;
}
/**
PSR-3 Interface method.
*/
public function emergency($message, array $context = array())
{
$this->writeLog($message, $context, self::EMERGENCY);
}
/**
PSR-3 Interface method.
*/
public function alert($message, array $context = array())
{
$this->writeLog($message, $context, self::ALERT);
}
/**
PSR-3 Interface method.
*/
public function critical($message, array $context = array())
{
$this->writeLog($message, $context, self::CRITICAL);
}
/**
PSR-3 Interface method.
*/
public function error($message, array $context = array())
{
$this->writeLog($message, $context, self::ERROR);
}
/**
PSR-3 Interface method.
*/
public function warning($message, array $context = array())
{
$this->writeLog($message, $context, self::WARNING);
}
/**
PSR-3 Interface method.
*/
public function notice($message, array $context = array())
{
$this->writeLog($message, $context, self::NOTICE);
}
/**
PSR-3 Interface method.
*/
public function info($message, array $context = array())
{
$this->writeLog($message, $context, self::INFO);
}
/**
PSR-3 Interface method.
*/
public function debug($message, array $context = array())
{
$this->writeLog($message, $context, self::DEBUG);
}
/**
PSR-3 Interface method.
*/
public function log($level, $message, array $context = array())
{
}
/**
Write a message to log(s)
@param $message [string] log message
@param $context [array] optional contextual info
@param $intLevel [int] log level
*/
private function writeLog($message, array $context, $intLevel)
{
$trace = debug_backtrace();
$file = $this->getLogLocation($intLevel);
$verboseDebug = $this->verboseDebugging();
/**
The 'logfile' context value just exists for testing
purposes. Calling code should not rely on this
behavior
*/
if (isset($context['logfile'])) {
$file = $context['logfile'];
}
if (isset($context['verbose'])) {
$verboseDebug = true;
}
if ($file) {
$fptr = fopen($file, 'a');
fwrite($fptr, $this->rfcLogLine($message, $intLevel) . "\n");
if ($this->syslogHost && !isset($context['skip_remote'])) {
$this->syslogRemote($message);
}
if ($intLevel === self::DEBUG && $verboseDebug) {
$stack = array();
if (isset($context['exception']) && $context['exception'] instanceof \Exception) {
$stack = $this->stackTrace($context['exception']->getTrace());
} else {
$stack = $this->stackTrace($trace);
}
foreach ($stack as $frame) {
fwrite($fptr, $this->rfcLogLine($frame, $intLevel) . "\n");
}
}
fclose($fptr);
}
}
private function rfcLogLine($line, $intLevel)
{
$date = date('M j H:i:s');
$host = gethostname() ? gethostname() : 'localhost';
$pid = getmypid() ? getmypid() : 0;
return sprintf('%s %s %s[%d]: (%s) %s',
$date, $host, $this->programName, $pid,
$this->logLevelMap[$intLevel],
$line);
}
/**
Convert a stack trace array into readable text
@param $stack [array] stack trace
@return [array] of log lines
*/
private function stackTrace($stack)
{
$counter = count($stack);
$lines = array();
foreach ($stack as $frame) {
$ret = 'Frame #' . $counter . ' - ';
$line = isset($frame['line']) ? $frame['line'] : 0;
$file = isset($frame['file']) ? $frame['file'] : 'Unknown file';
$args = isset($frame['args']) ? $frame['args'] : array();
$ret .= 'File ' . $file . ', Line ' . $line
. ', function ' . $this->frameToFunction($frame);
$lines[] = $ret;
$counter--;
}
return $lines;
}
private function frameToFunction($frame)
{
$function = isset($frame['function']) ? $frame['function'] : 'Unknown function';
if (isset($frame['class'])) {
$function = $frame['class'] . '::' . $function;
}
return $function;
}
/**
Get filename for log
@param [integer] log level constant
@return [string] filename or [boolean] false
*/
public function getLogLocation($intLevel)
{
return stristr(PHP_OS, 'WIN') ? 'nul' : '/dev/null';
}
/**
Include stack traces on error logging
@return [boolean]
*/
public function verboseDebugging()
{
return false;
}
}