.gitignore 0000666 00000000030 13533422203 0006526 0 ustar 00 /composer.lock
/vendor/
LICENSE.md 0000666 00000002071 13533422203 0006151 0 ustar 00 The MIT License (MIT)
Copyright (c) 2016 Javier Eguiluz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
README.md 0000666 00000020364 13533422203 0006031 0 ustar 00 EasyLogHandler (human-friendly log files)
=========================================
Symfony log files are formatted in the same way for all environments. This means that `dev.log` is optimized for machines instead of humans. The result is a log file bloated with useless information that makes you less productive.
**EasyLogHandler** is a new Monolog handler that creates human-friendly log files. It's optimized to display the log information in a clear and concise way. Use it in the development environment to become a much more productive developer.
1. [Features](#features)
2. [Installation](#installation)
3. [Configuration and Usage](#configuration-and-usage)
----
Features
--------
These are some of the best features of **EasyLogHandler** and how it compares itself with the default Symfony logs.
### Better Log Structure
Symfony log files are a huge stream of text. When you open them, you can't easily tell when a request started or finished and which log messages belong together:
| Symfony | EasyLogHandler
| ------- | --------------
| ![structure-overview-symfony-mini](https://cloud.githubusercontent.com/assets/73419/17691467/e838b0b2-6394-11e6-9028-bfa04adc36c1.png) | ![structure-overview-easylog-mini](https://cloud.githubusercontent.com/assets/73419/17691466/e8336c88-6394-11e6-9a13-32146e2bdb6f.png)
EasyLogHandler structures the log files in a different way:
![structure-easylog](https://cloud.githubusercontent.com/assets/73419/17691552/788c4138-6395-11e6-8436-36051a4eb0da.png)
* It adds a large header and some new lines to separate each request logs;
* If the request is less significant (e.g. Assetic requests) the header is more compact and displays less information;
* Log messages are divided internally so you can better understand their different parts (request, doctrine, security, etc.)
### Less Verbose Logs
First of all, EasyLogHandler doesn't display the timestamp in every log message. In the `dev` environment you shouldn't care about that, so the timestamp is only displayed once for each group of log messages.
| Symfony | EasyLogHandler
| ------- | --------------
| ![timestamps-symfony](https://cloud.githubusercontent.com/assets/73419/17691577/9f0bce78-6395-11e6-8e6b-f2ae3354342b.png) | ![timestamps-easylog](https://cloud.githubusercontent.com/assets/73419/17691578/9f4ceed0-6395-11e6-96ea-aada7577e1b2.png)
The `extra` information, which some log messages include to add more details about the log, is displayed only when it's different from the previous log. In contrast, Symfony always displays the `extra` for all logs, generating a lot of duplicated information:
| Symfony
| -------
| ![extra-symfony](https://cloud.githubusercontent.com/assets/73419/17691601/c17f2996-6395-11e6-876b-fbd87422c04d.png)
| EasyLogHandler
| -------
| ![extra-easylog](https://cloud.githubusercontent.com/assets/73419/17691600/c13fe75e-6395-11e6-92db-bb8457967642.png)
It's becoming increasingly popular to use placeholders in log messages instead of the actual values (e.g. `Matched route "{route}".` instead of `Matched route "home".`) This is great for machines, because they can group similar messages that only vary in the placeholder values.
However, for humans this "feature" is disturbing. That's why EasyLogHandler automatically replaces any placeholder included in the log message:
| Symfony
| -------
| ![placeholders-symfony](https://cloud.githubusercontent.com/assets/73419/17691694/541e4a20-6396-11e6-8400-546383a69755.png)
| EasyLogHandler
| --------------
| ![placeholder-easylog](https://cloud.githubusercontent.com/assets/73419/17691695/545b2f9e-6396-11e6-9b46-814c6dcde9e0.png)
### Better Visual Hierarchy
Important elements, such as deprecations and security-related messages, must stand out in log files to help you spot them instantly. However, in Symfony all logs look exactly the same. How can you know which are the important ones?
| Symfony
| -------
| ![visual-hierarchy-symfony](https://cloud.githubusercontent.com/assets/73419/17691756/a0164edc-6396-11e6-949a-73e973219d13.png)
(all messages look exactly the same)
| EasyLogHandler
| --------------
| ![visual-hierarchy-easylog](https://cloud.githubusercontent.com/assets/73419/17691755/9fe3b86e-6396-11e6-9308-abaeb8c5b823.png)
(deprecations, warnings, errors and security messages stand out)
### Dynamic Variable Inlining
Log messages usually contain related variables in their `context` and `extra` properties. Displaying the content of these variables in the log files is always a tough balance between readability and conciseness.
EasyLogHandler decides how to inline these variables dynamically depending on each log message. For example, Doctrine query parameters are always inlined but request parameters are inlined for unimportant requests and nested for important requests:
![dynamic-inline-easylog](https://cloud.githubusercontent.com/assets/73419/17691843/2fde6324-6397-11e6-8627-e7c04528c6b3.png)
### Stack Traces
When log messages include error stack traces, you definitely want to take a look at them. However, Symfony displays stack traces inlined, making them impossible to inspect. EasyLogHandler displays them as proper stack traces:
| Symfony
| -------
| ![stack-trace-symfony](https://cloud.githubusercontent.com/assets/73419/17691905/716839d2-6397-11e6-8d45-b8be84ad9596.png)
| EasyLogHandler
| --------------
| ![stack-trace-easylog](https://cloud.githubusercontent.com/assets/73419/17691908/72190302-6397-11e6-95c0-8c9c0b570d97.png)
### Log Message Grouping
One of the most frustrating experiences when inspecting log files is having lots of repeated or similar consecutive messages. They provide little information and they just distract you. EasyLogHandler process all log messages at once instead of one by one, so it's aware when there are similar consecutive logs.
For example, this is a Symfony log file displaying three consecutive missing translation messages:
![translation-group-symfony](https://cloud.githubusercontent.com/assets/73419/17691954/b76ef04c-6397-11e6-9127-675ae831fd31.png)
And this is how the same messages are displayed by EasyLogHandler:
![translation-group-easylog](https://cloud.githubusercontent.com/assets/73419/17691955/b895ca86-6397-11e6-83c8-e5d6da4dbdf3.png)
The difference is even more evident for "event notified" messages, which usually generate tens of consecutive messages:
| Symfony
| -------
| ![event-group-symfony](https://cloud.githubusercontent.com/assets/73419/17691951/b447634a-6397-11e6-8482-265d7b02ead6.png)
| EasyLogHandler
| --------------
| ![event-group-easylog](https://cloud.githubusercontent.com/assets/73419/17691952/b5f5fd96-6397-11e6-8fc6-8835e6be7824.png)
Installation
------------
This project is distributed as a PHP package instead of a Symfony bundle, so you just need to require the project with [Composer](https://getcomposer.org):
```bash
$ composer require easycorp/easy-log-handler
```
Configuration and Usage
-----------------------
### Step 1
Define a new service in your application for this log handler:
```yaml
# app/config/services.yml
services:
# ...
easycorp.easylog.handler:
class: EasyCorp\EasyLog\EasyLogHandler
public: false
arguments:
- '%kernel.logs_dir%/%kernel.environment%.log'
```
### Step 2
Update your Monolog configuration in the `dev` environment to define a buffered handler that wraps this new handler service (keep reading to understand why). You can safely copy+paste this configuration:
```yaml
# app/config/config_dev.yml
monolog:
handlers:
buffered:
type: buffer
handler: easylog
channels: ["!event"]
level: debug
easylog:
type: service
id: easycorp.easylog.handler
```
Most log handlers treat each log message separately. In contrast, EasyLogHandler advanced log processing requires each log message to be aware of the other logs (for example to merge similar consecutive messages). This means that all the logs associated with the request must be captured and processed in batch.
In the above configuration, the `buffered` handler saves all log messages and then passes them to the EasyLog handler, which processes all messages at once and writes the result in the log file.
Use the `buffered` handler to configure the channels logged/excluded and the level of the messages being logged.
composer.json 0000666 00000001545 13533422203 0007274 0 ustar 00 {
"name": "easycorp/easy-log-handler",
"description": "A handler for Monolog that optimizes log messages to be processed by humans instead of software. Improve your productivity with logs that are easy to understand.",
"keywords": ["log", "logging", "monolog", "easy", "productivity"],
"homepage": "https://github.com/EasyCorp/easy-log-handler",
"license": "MIT",
"authors": [
{
"name": "Javier Eguiluz",
"email": "javiereguiluz@gmail.com"
},
{
"name": "Project Contributors",
"homepage": "https://github.com/EasyCorp/easy-log-handler"
}
],
"require": {
"php" : ">=5.3.0",
"monolog/monolog" : "~1.6",
"symfony/yaml" : "~2.3|~3.0|~4.0"
},
"autoload": {
"psr-4": { "EasyCorp\\EasyLog\\": "src" }
}
}
src/EasyLogFormatter.php 0000666 00000054705 13533422203 0011307 0 ustar 00 '');
// ignore the logs related to the web debug toolbar
if ($this->isWebDebugToolbarLog($records)) {
return $logBatch;
}
$logBatch['formatted'] .= $this->formatLogBatchHeader($records);
foreach ($records as $key => $record) {
if ($this->isDeprecationLog($record)) {
$records[$key] = $this->processDeprecationLogRecord($record);
}
if ($this->isEventStopLog($record)) {
$records[$key] = $this->processEventStopLogRecord($record);
}
if ($this->isEventNotificationLog($record)) {
$records[$key] = $this->processEventNotificationLogRecord($records, $key);
}
if ($this->isTranslationLog($record)) {
$records[$key] = $this->processTranslationLogRecord($records, $key);
}
if ($this->isRouteMatchLog($record)) {
$records[$key] = $this->processRouteMatchLogRecord($record);
}
if ($this->isDoctrineLog($record)) {
$records[$key] = $this->processDoctrineLogRecord($record);
}
$logBatch['formatted'] .= rtrim($this->formatRecord($records, $key), PHP_EOL).PHP_EOL;
}
$logBatch['formatted'] .= PHP_EOL.PHP_EOL;
return $logBatch;
}
/**
* @param array $record
*
* @return bool
*/
private function isAsseticLog(array $record)
{
return isset($record['context']['route']) && 0 === strpos($record['context']['route'], '_assetic_');
}
/**
* @param array $record
*
* @return bool
*/
private function isDeprecationLog(array $record)
{
$isPhpChannel = 'php' === $record['channel'];
$isDeprecationError = isset($record['context']['type']) && E_USER_DEPRECATED === $record['context']['type'];
$looksLikeDeprecationMessage = false !== strpos($record['message'], 'deprecated since');
return $isPhpChannel && ($isDeprecationError || $looksLikeDeprecationMessage);
}
/**
* @param array $record
*
* @return bool
*/
private function isDoctrineLog(array $record)
{
return isset($record['channel']) && 'doctrine' === $record['channel'];
}
/**
* @param array $record
*
* @return bool
*/
private function isEventStopLog(array $record)
{
return 'Listener "{listener}" stopped propagation of the event "{event}".' === $record['message'];
}
/**
* @param array $record
*
* @return bool
*/
private function isEventNotificationLog(array $record)
{
$isEventNotifyChannel = isset($record['channel']) && '_event_notify' === $record['channel'];
$isEventChannel = isset($record['channel']) && 'event' === $record['channel'];
$contextIncludesEventNotification = isset($record['context']) && isset($record['context']['event']) && isset($record['context']['listener']);
return $isEventNotifyChannel || ($isEventChannel && $contextIncludesEventNotification);
}
/**
* @param array $record
*
* @return bool
*/
private function isRouteMatchLog(array $record)
{
return 'Matched route "{route}".' === $record['message'];
}
/**
* @param array $record
*
* @return bool
*/
private function isTranslationLog(array $record)
{
return isset($record['channel']) && 'translation' === $record['channel'];
}
/**
* @param array $records
*
* @return bool
*/
private function isWebDebugToolbarLog(array $records)
{
foreach ($records as $record) {
if (isset($record['context']['route']) && '_wdt' === $record['context']['route']) {
return true;
}
}
return false;
}
/**
* @param array $record
*
* @return array
*/
private function processDeprecationLogRecord(array $record)
{
$context = $record['context'];
unset($context['type']);
unset($context['level']);
$record['context'] = $context;
return $record;
}
/**
* @param array $record
*
* @return array
*/
private function processDoctrineLogRecord(array $record)
{
if (!isset($record['context']) || empty($record['context'])) {
return $record;
}
$isDatabaseQueryContext = $this->arrayContainsOnlyNumericKeys($record['context']);
if ($isDatabaseQueryContext) {
$record['context'] = array('query params' => $record['context']);
}
return $record;
}
/**
* In Symfony applications is common to have lots of consecutive "event notify"
* log messages. This method combines them all to generate a more compact output.
*
* @param array $records
* @param int $currentRecordIndex
*
* @return string
*/
private function processEventNotificationLogRecord(array $records, $currentRecordIndex)
{
$record = $records[$currentRecordIndex];
$record['message'] = null;
$record['channel'] = '_event_notify';
$record['context'] = array($record['context']['event'] => $record['context']['listener']);
// if the previous record is also an event notification, combine them
if (isset($records[$currentRecordIndex - 1]) && $this->isEventNotificationLog($records[$currentRecordIndex - 1])) {
$record['_properties']['display_log_info'] = false;
}
return $record;
}
/**
* @param array $record
*
* @return array
*/
private function processEventStopLogRecord(array $record)
{
$record['channel'] = '_event_stop';
$record['message'] = 'Event "{event}" stopped by:';
return $record;
}
/**
* @param array $record
*
* @return array
*/
private function processRouteMatchLogRecord(array $record)
{
if ($this->isAsseticLog($record)) {
$record['message'] = '{method}: {request_uri}';
return $record;
}
$context = $record['context'];
unset($context['method']);
unset($context['request_uri']);
$record['context'] = array_merge(
array($record['context']['method'] => $record['context']['request_uri']),
$context
);
return $record;
}
/**
* It interpolates the given string replacing its placeholders with the
* values defined in the given variables array.
*
* @param string $string
* @param array $variables
*
* @return string
*/
private function processStringPlaceholders($string, array $variables)
{
foreach ($variables as $key => $value) {
if (!is_string($value) && !is_numeric($value) && !is_bool($value)) {
continue;
}
$string = str_replace('{'.$key.'}', $value, (string) $string);
}
return $string;
}
/**
* In Symfony applications is common to have lots of consecutive "translation not found"
* log messages. This method combines them all to generate a more compact output.
*
* @param array $records
* @param int $currentRecordIndex
*
* @return string
*/
private function processTranslationLogRecord(array $records, $currentRecordIndex)
{
$record = $records[$currentRecordIndex];
if (isset($records[$currentRecordIndex - 1]) && $this->isTranslationLog($records[$currentRecordIndex - 1])) {
$record['_properties']['display_log_info'] = false;
$record['message'] = null;
}
return $record;
}
/**
* @param array $record
*
* @return string
*/
private function formatLogChannel(array $record)
{
if (!isset($record['channel'])) {
return '';
}
if ($this->isDeprecationLog($record)) {
return '** DEPRECATION **';
}
if ($this->isEventNotificationLog($record)) {
return 'NOTIFIED EVENTS';
}
$channel = $record['channel'];
$channelIcons = array(
'_event_stop' => '[!] ',
'security' => '(!) ',
);
$channelIcon = array_key_exists($channel, $channelIcons) ? $channelIcons[$channel] : '';
return sprintf('%s%s', $channelIcon, strtoupper($channel));
}
/**
* @param array $record
*
* @return string
*/
private function formatContext(array $record)
{
$context = $this->filterVariablesUsedAsPlaceholders($record['message'], $record['context']);
$context = $this->formatDateTimeObjects($context);
$context = $this->formatThrowableObjects($context);
$contextAsString = Yaml::dump($context, $this->getInlineLevel($record), $this->prefixLength, Yaml::DUMP_OBJECT);
if (substr($contextAsString, strpos($contextAsString, self::PHP_SERIALIZED_OBJECT_PREFIX), strlen(self::PHP_SERIALIZED_OBJECT_PREFIX)) === self::PHP_SERIALIZED_OBJECT_PREFIX) {
$contextAsString = $this->formatSerializedObject($contextAsString);
}
$contextAsString = $this->formatTextBlock($contextAsString, '--> ');
$contextAsString = rtrim($contextAsString, PHP_EOL);
return $contextAsString;
}
/**
* Turns any Throwable object present in the given array into a string
* representation. If the object cannot be serialized, an approximative
* representation of the object is given instead.
*
* @param array $array
*
* @return array
*/
private function formatThrowableObjects(array $array): array
{
array_walk_recursive($array, function (&$value) {
if ($value instanceof \Throwable) {
try {
$value = serialize($value);
} catch (\Throwable $throwable) {
$value = $this->formatThrowable($value);
}
}
});
return $array;
}
private function formatThrowable(\Throwable $throwable): array
{
return [
'class' => get_class($throwable),
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTraceAsString(),
'previous' => $throwable->getPrevious() ? $this->formatThrowable($throwable->getPrevious()) : null,
];
}
/**
* @param $objectString
*
* @return string
*/
private function formatSerializedObject($objectString)
{
$objectPrefixLength = strlen(self::PHP_SERIALIZED_OBJECT_PREFIX);
$objectStart = strpos($objectString, self::PHP_SERIALIZED_OBJECT_PREFIX) + $objectPrefixLength;
$beforePrefix = substr($objectString, 0, $objectStart - $objectPrefixLength);
$objectAsString = print_r(unserialize(substr($objectString, $objectStart)), true);
return $beforePrefix.$objectAsString;
}
/**
* @param array $record
*
* @return string
*/
private function formatExtra(array $record)
{
$extra = $this->formatDateTimeObjects($record['extra']);
$extraAsString = Yaml::dump(array('extra' => $extra), 1, $this->prefixLength);
$extraAsString = $this->formatTextBlock($extraAsString, '--> ');
return $extraAsString;
}
/**
* @param array $record
*
* @return string
*/
private function formatLogInfo(array $record)
{
return sprintf('%s%s', $this->formatLogLevel($record), $this->formatLogChannel($record));
}
/**
* @param array $record
*
* @return mixed|string
*/
private function formatLogLevel(array $record)
{
if (!isset($record['level_name'])) {
return '';
}
$level = $record['level_name'];
$levelLabels = array(
'DEBUG' => '',
'INFO' => '',
'WARNING' => '** WARNING ** ==> ',
'ERROR' => '*** ERROR *** ==> ',
'CRITICAL' => '*** CRITICAL ERROR *** ==> ',
);
return array_key_exists($level, $levelLabels) ? $levelLabels[$level] : $level.' ';
}
/**
* @param array $record
*
* @return string
*/
private function formatMessage(array $record)
{
$message = $this->processStringPlaceholders($record['message'], $record['context']);
$message = $this->formatStringAsTextBlock($message);
return $message;
}
/**
* @param array $records
* @param int $currentRecordIndex
*
* @return string
*/
private function formatRecord(array $records, $currentRecordIndex)
{
$record = $records[$currentRecordIndex];
$recordAsString = '';
if ($this->isLogInfoDisplayed($record)) {
$logInfo = $this->formatLogInfo($record);
$recordAsString .= $this->formatAsSection($logInfo);
}
if (isset($record['message']) && !empty($record['message'])) {
$recordAsString .= $this->formatMessage($record).PHP_EOL;
}
if (isset($record['context']) && !empty($record['context'])) {
// if the context contains an error stack trace, remove it to display it separately
$stack = null;
if (isset($record['context']['stack'])) {
$stack = $record['context']['stack'];
unset($record['context']['stack']);
}
$recordAsString .= $this->formatContext($record).PHP_EOL;
if (null !== $stack) {
$recordAsString .= '--> Stack Trace:'.PHP_EOL;
$recordAsString .= $this->formatStackTrace($stack, ' | ');
}
}
if (isset($record['extra']) && !empty($record['extra'])) {
// don't display the extra information when it's identical to the previous log record
$previousRecordExtra = isset($records[$currentRecordIndex - 1]) ? $records[$currentRecordIndex - 1]['extra'] : null;
if ($record['extra'] !== $previousRecordExtra) {
$recordAsString .= $this->formatExtra($record).PHP_EOL;
}
}
return $recordAsString;
}
/**
* @param array $trace
* @param string $prefix
*
* @return string
*/
private function formatStackTrace(array $trace, $prefix = '')
{
$traceAsString = '';
foreach ($trace as $line) {
if (isset($line['class']) && isset($line['type']) && isset($line['function'])) {
$traceAsString .= sprintf('%s%s%s()', $line['class'], $line['type'], $line['function']).PHP_EOL;
} elseif (isset($line['class'])) {
$traceAsString .= sprintf('%s', $line['class']).PHP_EOL;
} elseif (isset($line['function'])) {
$traceAsString .= sprintf('%s()', $line['function']).PHP_EOL;
}
if (isset($line['file']) && isset($line['line'])) {
$traceAsString .= sprintf(' > %s:%d', $this->makePathRelative($line['file']), $line['line']).PHP_EOL;
}
}
$traceAsString = $this->formatTextBlock($traceAsString, $prefix, true);
return $traceAsString;
}
/**
* @param array $records
*
* @return string
*/
private function formatLogBatchHeader(array $records)
{
$firstRecord = $records[0];
if ($this->isAsseticLog($firstRecord)) {
return $this->formatAsSubtitle('Assetic request');
}
$logDate = $firstRecord['datetime'];
$logDateAsString = $logDate->format('d/M/Y H:i:s');
return $this->formatAsTitle($logDateAsString);
}
/**
* @param string $text
*
* @return string
*/
private function formatAsTitle($text)
{
$titleLines = array();
$titleLines[] = str_repeat('#', $this->maxLineLength);
$titleLines[] = rtrim($this->formatAsSubtitle($text), PHP_EOL);
$titleLines[] = str_repeat('#', $this->maxLineLength);
return implode(PHP_EOL, $titleLines).PHP_EOL;
}
/**
* @param string $text
*
* @return string
*/
private function formatAsSubtitle($text)
{
$subtitle = str_pad('### '.$text.' ', $this->maxLineLength, '#', STR_PAD_BOTH);
return $subtitle.PHP_EOL;
}
/**
* @param string $text
*
* @return string
*/
private function formatAsSection($text)
{
$section = str_pad(str_repeat('_', 3).' '.$text.' ', $this->maxLineLength, '_', STR_PAD_RIGHT);
return $section.PHP_EOL;
}
/**
* @param string $string
*
* @return string
*/
private function formatStringAsTextBlock($string)
{
if (!is_string($string)) {
return '';
}
$string = wordwrap($string, $this->maxLineLength - $this->prefixLength);
$stringLines = explode(PHP_EOL, $string);
foreach ($stringLines as &$line) {
$line = str_repeat(' ', $this->prefixLength).$line;
}
$string = implode(PHP_EOL, $stringLines);
// needed to remove the unnecessary prefix added to the first line
$string = trim($string);
return $string;
}
/**
* Prepends the prefix to every line of the given string.
*
* @param string $text
* @param string $prefix
* @param bool $prefixAllLines If false, prefix is only added to lines that don't start with white spaces
*
* @return string
*/
private function formatTextBlock($text, $prefix = '', $prefixAllLines = false)
{
if (empty($text)) {
return $text;
}
$textLines = explode(PHP_EOL, $text);
// remove the trailing PHP_EOL (and add it back afterwards) to avoid formatting issues
$addTrailingNewline = false;
if ('' === $textLines[count($textLines) - 1]) {
array_pop($textLines);
$addTrailingNewline = true;
}
$newTextLines = array();
foreach ($textLines as $line) {
if ($prefixAllLines) {
$newTextLines[] = $prefix.$line;
} else {
if (isset($line[0]) && ' ' !== $line[0]) {
$newTextLines[] = $prefix.$line;
} else {
$newTextLines[] = str_repeat(' ', strlen($prefix)).$line;
}
}
}
return implode(PHP_EOL, $newTextLines).($addTrailingNewline ? PHP_EOL : '');
}
/**
* Turns any DateTime object present in the given array into a string
* representation of that date and time.
*
* @param array $array
*
* @return array
*/
private function formatDateTimeObjects(array $array)
{
array_walk_recursive($array, function (&$value) {
if ($value instanceof \DateTimeInterface) {
$value = date_format($value, 'c');
}
});
return $array;
}
/**
* It scans the given string for placeholders and removes from $variables
* any element whose key matches the name of a placeholder.
*
* @param string $string
* @param array $variables
*
* @return array
*/
private function filterVariablesUsedAsPlaceholders($string, array $variables)
{
if (empty($string)) {
return $variables;
}
// array_filter() is not used because ARRAY_FILTER_USE_KEY requires PHP 5.6
$filteredVariables = array();
foreach ($variables as $key => $value) {
if (false === strpos($string, '{'.$key.'}')) {
$filteredVariables[$key] = $value;
}
}
return $filteredVariables;
}
/**
* It returns the level at which YAML component inlines the values, which
* determines how compact or readable the information is displayed.
*
* @param array $record
*
* @return int
*/
private function getInlineLevel(array $record)
{
if ($this->isTranslationLog($record)) {
return 0;
}
if ($this->isDoctrineLog($record) || $this->isAsseticLog($record)) {
return 1;
}
return 2;
}
/**
* It returns true when the general information related to the record log
* should be displayed. It returns false when a log is displayed in a compact
* way to combine it with a similar previous record.
*
* @param array $record
*
* @return bool
*/
private function isLogInfoDisplayed(array $record)
{
if (!isset($record['_properties']) || !isset($record['_properties']['display_log_info'])) {
return true;
}
return $record['_properties']['display_log_info'];
}
/**
* @param array $array
*
* @return bool
*/
private function arrayContainsOnlyNumericKeys(array $array)
{
return 0 === count(array_filter(array_keys($array), 'is_string'));
}
/**
* @param string $filePath
*
* @return mixed
*/
private function makePathRelative($filePath)
{
$thisFilePath = __FILE__;
$thisFilePathParts = explode('/src/', $thisFilePath);
$projectRootDir = $thisFilePathParts[0].DIRECTORY_SEPARATOR;
return str_replace($projectRootDir, '', $filePath);
}
}
src/EasyLogHandler.php 0000666 00000002234 13533422203 0010707 0 ustar 00 formatter = new EasyLogFormatter();
}
/**
* @param array $record
*/
public function handle(array $record)
{
throw new \RuntimeException('The method "handle()" should never be called (call "handleBatch()" instead). This is probably caused by a wrong "monolog" configuration. Please read EasyLogHandler README instructions to learn how to configure and use it.');
}
/**
* @param array $records
*/
public function handleBatch(array $records)
{
// if the log records were filtered (by channel, level, etc.) the array
// no longer contains consecutive numeric keys. Make them consecutive again
// before the log processing (this eases getting the next/previous record)
$records = array_values($records);
if ($records) {
$this->write($this->getFormatter()->formatBatch($records));
}
}
}