Search code examples
phpcakephpcakephp-3.x

CakePHP 3.x: log as serialized array


I'm writing my own parser log for CakePHP.

I only need one thing: that is not written a log "message" (as a string), but a serialized array with various log information (date, type, line, stack traces, etc.).

But I don't understand what method/class I should rewrite, although I have consulted the APIs. Can you help me?

EDIT:
For now I do the opposite: I read the logs (already written) and I transform them into an array with a regex.

My code:

$logs = array_map(function($log) {
    preg_match('/^'.
        '([\d\-]+\s[\d:]+)\s(Error: Fatal Error|Error|Notice: Notice|Warning: Warning)(\s\(\d+\))?:\s([^\n]+)\n'.
        '(Exception Attributes:\s((.(?!Request|Referer|Stack|Trace))+)\n)?'.
        '(Request URL:\s([^\n]+)\n)?'.
        '(Referer URL:\s([^\n]+)\n)?'.
        '(Stack Trace:\n(.+))?'.
        '(Trace:\n(.+))?(.+)?'.
    '/si', $log, $matches);

    switch($matches[2]) {
        case 'Error: Fatal Error':
            $type = 'fatal';
            break;
        case 'Error':
            $type = 'error';
            break;
        case 'Notice: Notice':
            $type = 'notice';
            break;
        case 'Warning: Warning':
            $type = 'warning';
            break;
        default:
            $type = 'unknown';
            break;
    }

    return (object) af([
        'datetime'      => \Cake\I18n\FrozenTime::parse($matches[1]),
        'type'          => $type,
        'error'         => $matches[4],
        'attributes'    => empty($matches[6]) ? NULL : $matches[6],
        'url'           => empty($matches[9]) ? NULL : $matches[9],
        'referer'       => empty($matches[11]) ? NULL : $matches[11],
        'stack_trace'   => empty($matches[13]) ? (empty($matches[16]) ? NULL : $matches[16]) : $matches[13],
        'trace'         => empty($matches[15]) ? NULL : $matches[15]
    ]);
}, af(preg_split('/[\r\n]{2,}/', $logs)));

For now I do the opposite: I read the logs (already written) and with a regex I transform them into an array.

The problem is this is terribly expensive. and that it would be better to do the opposite: to write directly to the logs as a serialized array.


Solution

  • Ok, that's it!

    (note that this code is absolutely experimental, I have yet to test it properly)

    One interesting thing that I want to do: for each log, write to the serialized file and also simultaneously in a plan file. This allows me either to read logs as a plain text file, or they can be manipulated using the serialized file.

    use Cake\Log\Engine\FileLog;
    
    class SerializedLog extends FileLog {
        protected function _getLogAsArray($level, $message) {       
            $serialized['level'] = $level;
            $serialized['datetime'] = date('Y-m-d H:i:s');
    
            //Sets exception type and message
            if(preg_match('/^(\[([^\]]+)\]\s)?(.+)/', $message, $matches)) {                
                if(!empty($matches[2]))
                    $serialized['exception'] = $matches[2];
    
                $serialized['message'] = $matches[3];
            }
    
            //Sets the exception attributes
            if(preg_match('/Exception Attributes:\s((.(?!Request URL|Referer URL|Stack Trace|Trace))+)/is', $message, $matches)) {
                $serialized['attributes'] = $matches[1];
            }
    
            //Sets the request URL
            if(preg_match('/^Request URL:\s(.+)$/mi', $message, $matches)) {
                $serialized['request'] = $matches[1];
            }
    
            //Sets the referer URL
            if(preg_match('/^Referer URL:\s(.+)$/mi', $message, $matches)) {
                $serialized['referer'] = $matches[1];
            }
    
            //Sets the trace
            if(preg_match('/(Stack )?Trace:\n(.+)$/is', $message, $matches)) {
                $serialized['trace'] = $matches[2];
            }
    
            $serialized['full'] = date('Y-m-d H:i:s').' '.ucfirst($level).': '.$message;
    
            return (object) $serialized;
        }
    
    
        public function log($level, $message, array $context = []) {
            $message = $this->_format(trim($message), $context);
    
            $filename = $this->_getFilename($level);
            if (!empty($this->_size)) {
                $this->_rotateFile($filename);
            }
    
            $pathname = $this->_path . $filename;
            $mask = $this->_config['mask'];
    
            //Gets the content of the existing logs and unserializes
            $logs = @unserialize(@file_get_contents($pathname));
    
            if(empty($logs) || !is_array($logs))
                $logs = [];
    
            //Adds the current log
            $logs[] = $this->_getLogAsArray($level, $message);
    
            //Serializes logs
            $output = serialize($logs);
    
            if (empty($mask)) {
                return file_put_contents($pathname, $output);
            }
    
            $exists = file_exists($pathname);
            $result = file_put_contents($pathname, $output);
            static $selfError = false;
    
            if (!$selfError && !$exists && !chmod($pathname, (int)$mask)) {
                $selfError = true;
                trigger_error(vsprintf(
                    'Could not apply permission mask "%s" on log file "%s"',
                    [$mask, $pathname]
                ), E_USER_WARNING);
                $selfError = false;
            }
    
            return $result;
        }
    }