-1

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.

Mirko Pagliai
  • 1,220
  • 1
  • 19
  • 36
  • What is a "_parser log_"? And what's the context here, where exactly is what exactly being logged that you want to have in a specific format? – ndm Mar 03 '16 at 15:54
  • @ndm, see my edit. It's simple: the logs are written as a string, which contains within it a variety of information (what type of error, date and time, referer and url of the request, message, etc.). But I want to be a serialized array with that information – Mirko Pagliai Mar 03 '16 at 17:07
  • I already got that, but where exactly is that data? Are you talking about the default debug/error logs? ie, should _every_ log be serialized? Or maybe just the errors? Are you maybe using a custom logger which should the only one being affected? You really need to be more specific. – ndm Mar 04 '16 at 09:36
  • @nmn it does not matter where is this code. It could be a controller, but it could also be an external script to CakePHP. It is important however as CakePHP writes its logs. The code that you see reads CakePHP log files, those generated by the default logger. I try to explain better this way: now the logs are sorted by date. How can I do, if I want to sort them by request URL or by filename that generated them? So the code I posted reads the log file and the regex extracts information. All this would be easier if CakePHP write the log as a serialized array. – Mirko Pagliai Mar 04 '16 at 09:46
  • You're not getting my point, I just can't give you a proper answer if I don't know _which_ logs exactly should be written in a serialized manner! – ndm Mar 04 '16 at 10:00

2 Answers2

1

I think what you want to do is write your own LogAdapter. You simply create a class ArrayLog (extends BaseLog) as mentioned in the docs and configure cakePHP to use it. Within the log function you append the information like $level, $message and $context to a file as an array. This will result in a log file with several arrays that then can be split.

That being said, I would suggest to log to the database and read it out instead of parsing.

DIDoS
  • 812
  • 9
  • 23
  • Thanks @DIDoS, I had already thought of this. The problem is that, in this case, the `$message` argument already contains the final string, ie the various concatenated information. Sure, it's always better to divide here (again using a regex), but I would like to know if it was possible to operate upstream. Is this possible, in your opinion? – Mirko Pagliai Mar 07 '16 at 19:48
  • I believe that the affected code is `_getMessage()`, `_logError()` and `_logException()` methods from `BaseErrorHandler`. It's here that, for errors and exceptions, the information are concatenated into a string. Once they are string, they are sent to log. So, maybe, we need to write a LogAdapter AND an ErrorHandler. Do you think is correct? – Mirko Pagliai Mar 08 '16 at 21:19
0

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;
    }
}
Mirko Pagliai
  • 1,220
  • 1
  • 19
  • 36