104 changed files with 12698 additions and 0 deletions
@ -0,0 +1,59 @@ |
|||
<?php |
|||
|
|||
$header = <<<EOF |
|||
This file is part of the Monolog package. |
|||
|
|||
(c) Jordi Boggiano <j.boggiano@seld.be> |
|||
|
|||
For the full copyright and license information, please view the LICENSE |
|||
file that was distributed with this source code. |
|||
EOF; |
|||
|
|||
$finder = Symfony\CS\Finder::create() |
|||
->files() |
|||
->name('*.php') |
|||
->exclude('Fixtures') |
|||
->in(__DIR__.'/src') |
|||
->in(__DIR__.'/tests') |
|||
; |
|||
|
|||
return Symfony\CS\Config::create() |
|||
->setUsingCache(true) |
|||
//->setUsingLinter(false) |
|||
->setRiskyAllowed(true) |
|||
->setRules(array( |
|||
'@PSR2' => true, |
|||
'binary_operator_spaces' => true, |
|||
'blank_line_before_return' => true, |
|||
'header_comment' => array('header' => $header), |
|||
'include' => true, |
|||
'long_array_syntax' => true, |
|||
'method_separation' => true, |
|||
'no_blank_lines_after_class_opening' => true, |
|||
'no_blank_lines_after_phpdoc' => true, |
|||
'no_blank_lines_between_uses' => true, |
|||
'no_duplicate_semicolons' => true, |
|||
'no_extra_consecutive_blank_lines' => true, |
|||
'no_leading_import_slash' => true, |
|||
'no_leading_namespace_whitespace' => true, |
|||
'no_trailing_comma_in_singleline_array' => true, |
|||
'no_unused_imports' => true, |
|||
'object_operator_without_whitespace' => true, |
|||
'phpdoc_align' => true, |
|||
'phpdoc_indent' => true, |
|||
'phpdoc_no_access' => true, |
|||
'phpdoc_no_package' => true, |
|||
'phpdoc_order' => true, |
|||
'phpdoc_scalar' => true, |
|||
'phpdoc_trim' => true, |
|||
'phpdoc_type_to_var' => true, |
|||
'psr0' => true, |
|||
'single_blank_line_before_namespace' => true, |
|||
'spaces_cast' => true, |
|||
'standardize_not_equals' => true, |
|||
'ternary_operator_spaces' => true, |
|||
'trailing_comma_in_multiline_array' => true, |
|||
'whitespacy_lines' => true, |
|||
)) |
|||
->finder($finder) |
|||
; |
|||
@ -0,0 +1,370 @@ |
|||
### 1.24.0 (2018-11-05) |
|||
|
|||
* Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors |
|||
* Added a `ProcessorInterface` as an optional way to label a class as being a processor (mostly useful for autowiring dependency containers) |
|||
* Added a way to log signals being received using Monolog\SignalHandler |
|||
* Added ability to customize error handling at the Logger level using Logger::setExceptionHandler |
|||
* Added InsightOpsHandler to migrate users of the LogEntriesHandler |
|||
* Added protection to NormalizerHandler against circular and very deep structures, it now stops normalizing at a depth of 9 |
|||
* Added capture of stack traces to ErrorHandler when logging PHP errors |
|||
* Added RavenHandler support for a `contexts` context or extra key to forward that to Sentry's contexts |
|||
* Added forwarding of context info to FluentdFormatter |
|||
* Added SocketHandler::setChunkSize to override the default chunk size in case you must send large log lines to rsyslog for example |
|||
* Added ability to extend/override BrowserConsoleHandler |
|||
* Added SlackWebhookHandler::getWebhookUrl and SlackHandler::getToken to enable class extensibility |
|||
* Added SwiftMailerHandler::getSubjectFormatter to enable class extensibility |
|||
* Dropped official support for HHVM in test builds |
|||
* Fixed normalization of exception traces when call_user_func is used to avoid serializing objects and the data they contain |
|||
* Fixed naming of fields in Slack handler, all field names are now capitalized in all cases |
|||
* Fixed HipChatHandler bug where slack dropped messages randomly |
|||
* Fixed normalization of objects in Slack handlers |
|||
* Fixed support for PHP7's Throwable in NewRelicHandler |
|||
* Fixed race bug when StreamHandler sometimes incorrectly reported it failed to create a directory |
|||
* Fixed table row styling issues in HtmlFormatter |
|||
* Fixed RavenHandler dropping the message when logging exception |
|||
* Fixed WhatFailureGroupHandler skipping processors when using handleBatch |
|||
and implement it where possible |
|||
* Fixed display of anonymous class names |
|||
|
|||
### 1.23.0 (2017-06-19) |
|||
|
|||
* Improved SyslogUdpHandler's support for RFC5424 and added optional `$ident` argument |
|||
* Fixed GelfHandler truncation to be per field and not per message |
|||
* Fixed compatibility issue with PHP <5.3.6 |
|||
* Fixed support for headless Chrome in ChromePHPHandler |
|||
* Fixed support for latest Aws SDK in DynamoDbHandler |
|||
* Fixed support for SwiftMailer 6.0+ in SwiftMailerHandler |
|||
|
|||
### 1.22.1 (2017-03-13) |
|||
|
|||
* Fixed lots of minor issues in the new Slack integrations |
|||
* Fixed support for allowInlineLineBreaks in LineFormatter when formatting exception backtraces |
|||
|
|||
### 1.22.0 (2016-11-26) |
|||
|
|||
* Added SlackbotHandler and SlackWebhookHandler to set up Slack integration more easily |
|||
* Added MercurialProcessor to add mercurial revision and branch names to log records |
|||
* Added support for AWS SDK v3 in DynamoDbHandler |
|||
* Fixed fatal errors occuring when normalizing generators that have been fully consumed |
|||
* Fixed RollbarHandler to include a level (rollbar level), monolog_level (original name), channel and datetime (unix) |
|||
* Fixed RollbarHandler not flushing records automatically, calling close() explicitly is not necessary anymore |
|||
* Fixed SyslogUdpHandler to avoid sending empty frames |
|||
* Fixed a few PHP 7.0 and 7.1 compatibility issues |
|||
|
|||
### 1.21.0 (2016-07-29) |
|||
|
|||
* Break: Reverted the addition of $context when the ErrorHandler handles regular php errors from 1.20.0 as it was causing issues |
|||
* Added support for more formats in RotatingFileHandler::setFilenameFormat as long as they have Y, m and d in order |
|||
* Added ability to format the main line of text the SlackHandler sends by explictly setting a formatter on the handler |
|||
* Added information about SoapFault instances in NormalizerFormatter |
|||
* Added $handleOnlyReportedErrors option on ErrorHandler::registerErrorHandler (default true) to allow logging of all errors no matter the error_reporting level |
|||
|
|||
### 1.20.0 (2016-07-02) |
|||
|
|||
* Added FingersCrossedHandler::activate() to manually trigger the handler regardless of the activation policy |
|||
* Added StreamHandler::getUrl to retrieve the stream's URL |
|||
* Added ability to override addRow/addTitle in HtmlFormatter |
|||
* Added the $context to context information when the ErrorHandler handles a regular php error |
|||
* Deprecated RotatingFileHandler::setFilenameFormat to only support 3 formats: Y, Y-m and Y-m-d |
|||
* Fixed WhatFailureGroupHandler to work with PHP7 throwables |
|||
* Fixed a few minor bugs |
|||
|
|||
### 1.19.0 (2016-04-12) |
|||
|
|||
* Break: StreamHandler will not close streams automatically that it does not own. If you pass in a stream (not a path/url), then it will not close it for you. You can retrieve those using getStream() if needed |
|||
* Added DeduplicationHandler to remove duplicate records from notifications across multiple requests, useful for email or other notifications on errors |
|||
* Added ability to use `%message%` and other LineFormatter replacements in the subject line of emails sent with NativeMailHandler and SwiftMailerHandler |
|||
* Fixed HipChatHandler handling of long messages |
|||
|
|||
### 1.18.2 (2016-04-02) |
|||
|
|||
* Fixed ElasticaFormatter to use more precise dates |
|||
* Fixed GelfMessageFormatter sending too long messages |
|||
|
|||
### 1.18.1 (2016-03-13) |
|||
|
|||
* Fixed SlackHandler bug where slack dropped messages randomly |
|||
* Fixed RedisHandler issue when using with the PHPRedis extension |
|||
* Fixed AmqpHandler content-type being incorrectly set when using with the AMQP extension |
|||
* Fixed BrowserConsoleHandler regression |
|||
|
|||
### 1.18.0 (2016-03-01) |
|||
|
|||
* Added optional reduction of timestamp precision via `Logger->useMicrosecondTimestamps(false)`, disabling it gets you a bit of performance boost but reduces the precision to the second instead of microsecond |
|||
* Added possibility to skip some extra stack frames in IntrospectionProcessor if you have some library wrapping Monolog that is always adding frames |
|||
* Added `Logger->withName` to clone a logger (keeping all handlers) with a new name |
|||
* Added FluentdFormatter for the Fluentd unix socket protocol |
|||
* Added HandlerWrapper base class to ease the creation of handler wrappers, just extend it and override as needed |
|||
* Added support for replacing context sub-keys using `%context.*%` in LineFormatter |
|||
* Added support for `payload` context value in RollbarHandler |
|||
* Added setRelease to RavenHandler to describe the application version, sent with every log |
|||
* Added support for `fingerprint` context value in RavenHandler |
|||
* Fixed JSON encoding errors that would gobble up the whole log record, we now handle those more gracefully by dropping chars as needed |
|||
* Fixed write timeouts in SocketHandler and derivatives, set to 10sec by default, lower it with `setWritingTimeout()` |
|||
* Fixed PHP7 compatibility with regard to Exception/Throwable handling in a few places |
|||
|
|||
### 1.17.2 (2015-10-14) |
|||
|
|||
* Fixed ErrorHandler compatibility with non-Monolog PSR-3 loggers |
|||
* Fixed SlackHandler handling to use slack functionalities better |
|||
* Fixed SwiftMailerHandler bug when sending multiple emails they all had the same id |
|||
* Fixed 5.3 compatibility regression |
|||
|
|||
### 1.17.1 (2015-08-31) |
|||
|
|||
* Fixed RollbarHandler triggering PHP notices |
|||
|
|||
### 1.17.0 (2015-08-30) |
|||
|
|||
* Added support for `checksum` and `release` context/extra values in RavenHandler |
|||
* Added better support for exceptions in RollbarHandler |
|||
* Added UidProcessor::getUid |
|||
* Added support for showing the resource type in NormalizedFormatter |
|||
* Fixed IntrospectionProcessor triggering PHP notices |
|||
|
|||
### 1.16.0 (2015-08-09) |
|||
|
|||
* Added IFTTTHandler to notify ifttt.com triggers |
|||
* Added Logger::setHandlers() to allow setting/replacing all handlers |
|||
* Added $capSize in RedisHandler to cap the log size |
|||
* Fixed StreamHandler creation of directory to only trigger when the first log write happens |
|||
* Fixed bug in the handling of curl failures |
|||
* Fixed duplicate logging of fatal errors when both error and fatal error handlers are registered in monolog's ErrorHandler |
|||
* Fixed missing fatal errors records with handlers that need to be closed to flush log records |
|||
* Fixed TagProcessor::addTags support for associative arrays |
|||
|
|||
### 1.15.0 (2015-07-12) |
|||
|
|||
* Added addTags and setTags methods to change a TagProcessor |
|||
* Added automatic creation of directories if they are missing for a StreamHandler to open a log file |
|||
* Added retry functionality to Loggly, Cube and Mandrill handlers so they retry up to 5 times in case of network failure |
|||
* Fixed process exit code being incorrectly reset to 0 if ErrorHandler::registerExceptionHandler was used |
|||
* Fixed HTML/JS escaping in BrowserConsoleHandler |
|||
* Fixed JSON encoding errors being silently suppressed (PHP 5.5+ only) |
|||
|
|||
### 1.14.0 (2015-06-19) |
|||
|
|||
* Added PHPConsoleHandler to send record to Chrome's PHP Console extension and library |
|||
* Added support for objects implementing __toString in the NormalizerFormatter |
|||
* Added support for HipChat's v2 API in HipChatHandler |
|||
* Added Logger::setTimezone() to initialize the timezone monolog should use in case date.timezone isn't correct for your app |
|||
* Added an option to send formatted message instead of the raw record on PushoverHandler via ->useFormattedMessage(true) |
|||
* Fixed curl errors being silently suppressed |
|||
|
|||
### 1.13.1 (2015-03-09) |
|||
|
|||
* Fixed regression in HipChat requiring a new token to be created |
|||
|
|||
### 1.13.0 (2015-03-05) |
|||
|
|||
* Added Registry::hasLogger to check for the presence of a logger instance |
|||
* Added context.user support to RavenHandler |
|||
* Added HipChat API v2 support in the HipChatHandler |
|||
* Added NativeMailerHandler::addParameter to pass params to the mail() process |
|||
* Added context data to SlackHandler when $includeContextAndExtra is true |
|||
* Added ability to customize the Swift_Message per-email in SwiftMailerHandler |
|||
* Fixed SwiftMailerHandler to lazily create message instances if a callback is provided |
|||
* Fixed serialization of INF and NaN values in Normalizer and LineFormatter |
|||
|
|||
### 1.12.0 (2014-12-29) |
|||
|
|||
* Break: HandlerInterface::isHandling now receives a partial record containing only a level key. This was always the intent and does not break any Monolog handler but is strictly speaking a BC break and you should check if you relied on any other field in your own handlers. |
|||
* Added PsrHandler to forward records to another PSR-3 logger |
|||
* Added SamplingHandler to wrap around a handler and include only every Nth record |
|||
* Added MongoDBFormatter to support better storage with MongoDBHandler (it must be enabled manually for now) |
|||
* Added exception codes in the output of most formatters |
|||
* Added LineFormatter::includeStacktraces to enable exception stack traces in logs (uses more than one line) |
|||
* Added $useShortAttachment to SlackHandler to minify attachment size and $includeExtra to append extra data |
|||
* Added $host to HipChatHandler for users of private instances |
|||
* Added $transactionName to NewRelicHandler and support for a transaction_name context value |
|||
* Fixed MandrillHandler to avoid outputing API call responses |
|||
* Fixed some non-standard behaviors in SyslogUdpHandler |
|||
|
|||
### 1.11.0 (2014-09-30) |
|||
|
|||
* Break: The NewRelicHandler extra and context data are now prefixed with extra_ and context_ to avoid clashes. Watch out if you have scripts reading those from the API and rely on names |
|||
* Added WhatFailureGroupHandler to suppress any exception coming from the wrapped handlers and avoid chain failures if a logging service fails |
|||
* Added MandrillHandler to send emails via the Mandrillapp.com API |
|||
* Added SlackHandler to log records to a Slack.com account |
|||
* Added FleepHookHandler to log records to a Fleep.io account |
|||
* Added LogglyHandler::addTag to allow adding tags to an existing handler |
|||
* Added $ignoreEmptyContextAndExtra to LineFormatter to avoid empty [] at the end |
|||
* Added $useLocking to StreamHandler and RotatingFileHandler to enable flock() while writing |
|||
* Added support for PhpAmqpLib in the AmqpHandler |
|||
* Added FingersCrossedHandler::clear and BufferHandler::clear to reset them between batches in long running jobs |
|||
* Added support for adding extra fields from $_SERVER in the WebProcessor |
|||
* Fixed support for non-string values in PrsLogMessageProcessor |
|||
* Fixed SwiftMailer messages being sent with the wrong date in long running scripts |
|||
* Fixed minor PHP 5.6 compatibility issues |
|||
* Fixed BufferHandler::close being called twice |
|||
|
|||
### 1.10.0 (2014-06-04) |
|||
|
|||
* Added Logger::getHandlers() and Logger::getProcessors() methods |
|||
* Added $passthruLevel argument to FingersCrossedHandler to let it always pass some records through even if the trigger level is not reached |
|||
* Added support for extra data in NewRelicHandler |
|||
* Added $expandNewlines flag to the ErrorLogHandler to create multiple log entries when a message has multiple lines |
|||
|
|||
### 1.9.1 (2014-04-24) |
|||
|
|||
* Fixed regression in RotatingFileHandler file permissions |
|||
* Fixed initialization of the BufferHandler to make sure it gets flushed after receiving records |
|||
* Fixed ChromePHPHandler and FirePHPHandler's activation strategies to be more conservative |
|||
|
|||
### 1.9.0 (2014-04-20) |
|||
|
|||
* Added LogEntriesHandler to send logs to a LogEntries account |
|||
* Added $filePermissions to tweak file mode on StreamHandler and RotatingFileHandler |
|||
* Added $useFormatting flag to MemoryProcessor to make it send raw data in bytes |
|||
* Added support for table formatting in FirePHPHandler via the table context key |
|||
* Added a TagProcessor to add tags to records, and support for tags in RavenHandler |
|||
* Added $appendNewline flag to the JsonFormatter to enable using it when logging to files |
|||
* Added sound support to the PushoverHandler |
|||
* Fixed multi-threading support in StreamHandler |
|||
* Fixed empty headers issue when ChromePHPHandler received no records |
|||
* Fixed default format of the ErrorLogHandler |
|||
|
|||
### 1.8.0 (2014-03-23) |
|||
|
|||
* Break: the LineFormatter now strips newlines by default because this was a bug, set $allowInlineLineBreaks to true if you need them |
|||
* Added BrowserConsoleHandler to send logs to any browser's console via console.log() injection in the output |
|||
* Added FilterHandler to filter records and only allow those of a given list of levels through to the wrapped handler |
|||
* Added FlowdockHandler to send logs to a Flowdock account |
|||
* Added RollbarHandler to send logs to a Rollbar account |
|||
* Added HtmlFormatter to send prettier log emails with colors for each log level |
|||
* Added GitProcessor to add the current branch/commit to extra record data |
|||
* Added a Monolog\Registry class to allow easier global access to pre-configured loggers |
|||
* Added support for the new official graylog2/gelf-php lib for GelfHandler, upgrade if you can by replacing the mlehner/gelf-php requirement |
|||
* Added support for HHVM |
|||
* Added support for Loggly batch uploads |
|||
* Added support for tweaking the content type and encoding in NativeMailerHandler |
|||
* Added $skipClassesPartials to tweak the ignored classes in the IntrospectionProcessor |
|||
* Fixed batch request support in GelfHandler |
|||
|
|||
### 1.7.0 (2013-11-14) |
|||
|
|||
* Added ElasticSearchHandler to send logs to an Elastic Search server |
|||
* Added DynamoDbHandler and ScalarFormatter to send logs to Amazon's Dynamo DB |
|||
* Added SyslogUdpHandler to send logs to a remote syslogd server |
|||
* Added LogglyHandler to send logs to a Loggly account |
|||
* Added $level to IntrospectionProcessor so it only adds backtraces when needed |
|||
* Added $version to LogstashFormatter to allow using the new v1 Logstash format |
|||
* Added $appName to NewRelicHandler |
|||
* Added configuration of Pushover notification retries/expiry |
|||
* Added $maxColumnWidth to NativeMailerHandler to change the 70 chars default |
|||
* Added chainability to most setters for all handlers |
|||
* Fixed RavenHandler batch processing so it takes the message from the record with highest priority |
|||
* Fixed HipChatHandler batch processing so it sends all messages at once |
|||
* Fixed issues with eAccelerator |
|||
* Fixed and improved many small things |
|||
|
|||
### 1.6.0 (2013-07-29) |
|||
|
|||
* Added HipChatHandler to send logs to a HipChat chat room |
|||
* Added ErrorLogHandler to send logs to PHP's error_log function |
|||
* Added NewRelicHandler to send logs to NewRelic's service |
|||
* Added Monolog\ErrorHandler helper class to register a Logger as exception/error/fatal handler |
|||
* Added ChannelLevelActivationStrategy for the FingersCrossedHandler to customize levels by channel |
|||
* Added stack traces output when normalizing exceptions (json output & co) |
|||
* Added Monolog\Logger::API constant (currently 1) |
|||
* Added support for ChromePHP's v4.0 extension |
|||
* Added support for message priorities in PushoverHandler, see $highPriorityLevel and $emergencyLevel |
|||
* Added support for sending messages to multiple users at once with the PushoverHandler |
|||
* Fixed RavenHandler's support for batch sending of messages (when behind a Buffer or FingersCrossedHandler) |
|||
* Fixed normalization of Traversables with very large data sets, only the first 1000 items are shown now |
|||
* Fixed issue in RotatingFileHandler when an open_basedir restriction is active |
|||
* Fixed minor issues in RavenHandler and bumped the API to Raven 0.5.0 |
|||
* Fixed SyslogHandler issue when many were used concurrently with different facilities |
|||
|
|||
### 1.5.0 (2013-04-23) |
|||
|
|||
* Added ProcessIdProcessor to inject the PID in log records |
|||
* Added UidProcessor to inject a unique identifier to all log records of one request/run |
|||
* Added support for previous exceptions in the LineFormatter exception serialization |
|||
* Added Monolog\Logger::getLevels() to get all available levels |
|||
* Fixed ChromePHPHandler so it avoids sending headers larger than Chrome can handle |
|||
|
|||
### 1.4.1 (2013-04-01) |
|||
|
|||
* Fixed exception formatting in the LineFormatter to be more minimalistic |
|||
* Fixed RavenHandler's handling of context/extra data, requires Raven client >0.1.0 |
|||
* Fixed log rotation in RotatingFileHandler to work with long running scripts spanning multiple days |
|||
* Fixed WebProcessor array access so it checks for data presence |
|||
* Fixed Buffer, Group and FingersCrossed handlers to make use of their processors |
|||
|
|||
### 1.4.0 (2013-02-13) |
|||
|
|||
* Added RedisHandler to log to Redis via the Predis library or the phpredis extension |
|||
* Added ZendMonitorHandler to log to the Zend Server monitor |
|||
* Added the possibility to pass arrays of handlers and processors directly in the Logger constructor |
|||
* Added `$useSSL` option to the PushoverHandler which is enabled by default |
|||
* Fixed ChromePHPHandler and FirePHPHandler issue when multiple instances are used simultaneously |
|||
* Fixed header injection capability in the NativeMailHandler |
|||
|
|||
### 1.3.1 (2013-01-11) |
|||
|
|||
* Fixed LogstashFormatter to be usable with stream handlers |
|||
* Fixed GelfMessageFormatter levels on Windows |
|||
|
|||
### 1.3.0 (2013-01-08) |
|||
|
|||
* Added PSR-3 compliance, the `Monolog\Logger` class is now an instance of `Psr\Log\LoggerInterface` |
|||
* Added PsrLogMessageProcessor that you can selectively enable for full PSR-3 compliance |
|||
* Added LogstashFormatter (combine with SocketHandler or StreamHandler to send logs to Logstash) |
|||
* Added PushoverHandler to send mobile notifications |
|||
* Added CouchDBHandler and DoctrineCouchDBHandler |
|||
* Added RavenHandler to send data to Sentry servers |
|||
* Added support for the new MongoClient class in MongoDBHandler |
|||
* Added microsecond precision to log records' timestamps |
|||
* Added `$flushOnOverflow` param to BufferHandler to flush by batches instead of losing |
|||
the oldest entries |
|||
* Fixed normalization of objects with cyclic references |
|||
|
|||
### 1.2.1 (2012-08-29) |
|||
|
|||
* Added new $logopts arg to SyslogHandler to provide custom openlog options |
|||
* Fixed fatal error in SyslogHandler |
|||
|
|||
### 1.2.0 (2012-08-18) |
|||
|
|||
* Added AmqpHandler (for use with AMQP servers) |
|||
* Added CubeHandler |
|||
* Added NativeMailerHandler::addHeader() to send custom headers in mails |
|||
* Added the possibility to specify more than one recipient in NativeMailerHandler |
|||
* Added the possibility to specify float timeouts in SocketHandler |
|||
* Added NOTICE and EMERGENCY levels to conform with RFC 5424 |
|||
* Fixed the log records to use the php default timezone instead of UTC |
|||
* Fixed BufferHandler not being flushed properly on PHP fatal errors |
|||
* Fixed normalization of exotic resource types |
|||
* Fixed the default format of the SyslogHandler to avoid duplicating datetimes in syslog |
|||
|
|||
### 1.1.0 (2012-04-23) |
|||
|
|||
* Added Monolog\Logger::isHandling() to check if a handler will |
|||
handle the given log level |
|||
* Added ChromePHPHandler |
|||
* Added MongoDBHandler |
|||
* Added GelfHandler (for use with Graylog2 servers) |
|||
* Added SocketHandler (for use with syslog-ng for example) |
|||
* Added NormalizerFormatter |
|||
* Added the possibility to change the activation strategy of the FingersCrossedHandler |
|||
* Added possibility to show microseconds in logs |
|||
* Added `server` and `referer` to WebProcessor output |
|||
|
|||
### 1.0.2 (2011-10-24) |
|||
|
|||
* Fixed bug in IE with large response headers and FirePHPHandler |
|||
|
|||
### 1.0.1 (2011-08-25) |
|||
|
|||
* Added MemoryPeakUsageProcessor and MemoryUsageProcessor |
|||
* Added Monolog\Logger::getName() to get a logger's channel name |
|||
|
|||
### 1.0.0 (2011-07-06) |
|||
|
|||
* Added IntrospectionProcessor to get info from where the logger was called |
|||
* Fixed WebProcessor in CLI |
|||
|
|||
### 1.0.0-RC1 (2011-07-01) |
|||
|
|||
* Initial release |
|||
@ -0,0 +1,19 @@ |
|||
Copyright (c) 2011-2016 Jordi Boggiano |
|||
|
|||
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. |
|||
@ -0,0 +1,94 @@ |
|||
# Monolog - Logging for PHP [](https://travis-ci.org/Seldaek/monolog) |
|||
|
|||
[](https://packagist.org/packages/monolog/monolog) |
|||
[](https://packagist.org/packages/monolog/monolog) |
|||
|
|||
|
|||
Monolog sends your logs to files, sockets, inboxes, databases and various |
|||
web services. See the complete list of handlers below. Special handlers |
|||
allow you to build advanced logging strategies. |
|||
|
|||
This library implements the [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) |
|||
interface that you can type-hint against in your own libraries to keep |
|||
a maximum of interoperability. You can also use it in your applications to |
|||
make sure you can always use another compatible logger at a later time. |
|||
As of 1.11.0 Monolog public APIs will also accept PSR-3 log levels. |
|||
Internally Monolog still uses its own level scheme since it predates PSR-3. |
|||
|
|||
## Installation |
|||
|
|||
Install the latest version with |
|||
|
|||
```bash |
|||
$ composer require monolog/monolog |
|||
``` |
|||
|
|||
## Basic Usage |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\StreamHandler; |
|||
|
|||
// create a log channel |
|||
$log = new Logger('name'); |
|||
$log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING)); |
|||
|
|||
// add records to the log |
|||
$log->addWarning('Foo'); |
|||
$log->addError('Bar'); |
|||
``` |
|||
|
|||
## Documentation |
|||
|
|||
- [Usage Instructions](doc/01-usage.md) |
|||
- [Handlers, Formatters and Processors](doc/02-handlers-formatters-processors.md) |
|||
- [Utility classes](doc/03-utilities.md) |
|||
- [Extending Monolog](doc/04-extending.md) |
|||
|
|||
## Third Party Packages |
|||
|
|||
Third party handlers, formatters and processors are |
|||
[listed in the wiki](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages). You |
|||
can also add your own there if you publish one. |
|||
|
|||
## About |
|||
|
|||
### Requirements |
|||
|
|||
- Monolog works with PHP 5.3 or above, and is also tested to work with HHVM. |
|||
|
|||
### Submitting bugs and feature requests |
|||
|
|||
Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/monolog/issues) |
|||
|
|||
### Framework Integrations |
|||
|
|||
- Frameworks and libraries using [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) |
|||
can be used very easily with Monolog since it implements the interface. |
|||
- [Symfony2](http://symfony.com) comes out of the box with Monolog. |
|||
- [Silex](http://silex.sensiolabs.org/) comes out of the box with Monolog. |
|||
- [Laravel 4 & 5](http://laravel.com/) come out of the box with Monolog. |
|||
- [Lumen](http://lumen.laravel.com/) comes out of the box with Monolog. |
|||
- [PPI](http://www.ppi.io/) comes out of the box with Monolog. |
|||
- [CakePHP](http://cakephp.org/) is usable with Monolog via the [cakephp-monolog](https://github.com/jadb/cakephp-monolog) plugin. |
|||
- [Slim](http://www.slimframework.com/) is usable with Monolog via the [Slim-Monolog](https://github.com/Flynsarmy/Slim-Monolog) log writer. |
|||
- [XOOPS 2.6](http://xoops.org/) comes out of the box with Monolog. |
|||
- [Aura.Web_Project](https://github.com/auraphp/Aura.Web_Project) comes out of the box with Monolog. |
|||
- [Nette Framework](http://nette.org/en/) can be used with Monolog via [Kdyby/Monolog](https://github.com/Kdyby/Monolog) extension. |
|||
- [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog. |
|||
|
|||
### Author |
|||
|
|||
Jordi Boggiano - <j.boggiano@seld.be> - <http://twitter.com/seldaek><br /> |
|||
See also the list of [contributors](https://github.com/Seldaek/monolog/contributors) which participated in this project. |
|||
|
|||
### License |
|||
|
|||
Monolog is licensed under the MIT License - see the `LICENSE` file for details |
|||
|
|||
### Acknowledgements |
|||
|
|||
This library is heavily inspired by Python's [Logbook](http://packages.python.org/Logbook/) |
|||
library, although most concepts have been adjusted to fit to the PHP world. |
|||
@ -0,0 +1,66 @@ |
|||
{ |
|||
"name": "monolog/monolog", |
|||
"description": "Sends your logs to files, sockets, inboxes, databases and various web services", |
|||
"keywords": ["log", "logging", "psr-3"], |
|||
"homepage": "http://github.com/Seldaek/monolog", |
|||
"type": "library", |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "Jordi Boggiano", |
|||
"email": "j.boggiano@seld.be", |
|||
"homepage": "http://seld.be" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": ">=5.3.0", |
|||
"psr/log": "~1.0" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "~4.5", |
|||
"graylog2/gelf-php": "~1.0", |
|||
"sentry/sentry": "^0.13", |
|||
"ruflin/elastica": ">=0.90 <3.0", |
|||
"doctrine/couchdb": "~1.0@dev", |
|||
"aws/aws-sdk-php": "^2.4.9 || ^3.0", |
|||
"php-amqplib/php-amqplib": "~2.4", |
|||
"swiftmailer/swiftmailer": "^5.3|^6.0", |
|||
"php-console/php-console": "^3.1.3", |
|||
"phpunit/phpunit-mock-objects": "2.3.0", |
|||
"jakub-onderka/php-parallel-lint": "0.9" |
|||
}, |
|||
"_": "phpunit/phpunit-mock-objects required in 2.3.0 due to https://github.com/sebastianbergmann/phpunit-mock-objects/issues/223 - needs hhvm 3.8+ on travis", |
|||
"suggest": { |
|||
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", |
|||
"sentry/sentry": "Allow sending log messages to a Sentry server", |
|||
"doctrine/couchdb": "Allow sending log messages to a CouchDB server", |
|||
"ruflin/elastica": "Allow sending log messages to an Elastic Search server", |
|||
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", |
|||
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", |
|||
"ext-mongo": "Allow sending log messages to a MongoDB server", |
|||
"mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", |
|||
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", |
|||
"rollbar/rollbar": "Allow sending log messages to Rollbar", |
|||
"php-console/php-console": "Allow sending log messages to Google Chrome" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": {"Monolog\\": "src/Monolog"} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": {"Monolog\\": "tests/Monolog"} |
|||
}, |
|||
"provide": { |
|||
"psr/log-implementation": "1.0.0" |
|||
}, |
|||
"extra": { |
|||
"branch-alias": { |
|||
"dev-master": "2.0.x-dev" |
|||
} |
|||
}, |
|||
"scripts": { |
|||
"test": [ |
|||
"parallel-lint . --exclude vendor", |
|||
"phpunit" |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,231 @@ |
|||
# Using Monolog |
|||
|
|||
- [Installation](#installation) |
|||
- [Core Concepts](#core-concepts) |
|||
- [Log Levels](#log-levels) |
|||
- [Configuring a logger](#configuring-a-logger) |
|||
- [Adding extra data in the records](#adding-extra-data-in-the-records) |
|||
- [Leveraging channels](#leveraging-channels) |
|||
- [Customizing the log format](#customizing-the-log-format) |
|||
|
|||
## Installation |
|||
|
|||
Monolog is available on Packagist ([monolog/monolog](http://packagist.org/packages/monolog/monolog)) |
|||
and as such installable via [Composer](http://getcomposer.org/). |
|||
|
|||
```bash |
|||
composer require monolog/monolog |
|||
``` |
|||
|
|||
If you do not use Composer, you can grab the code from GitHub, and use any |
|||
PSR-0 compatible autoloader (e.g. the [Symfony2 ClassLoader component](https://github.com/symfony/ClassLoader)) |
|||
to load Monolog classes. |
|||
|
|||
## Core Concepts |
|||
|
|||
Every `Logger` instance has a channel (name) and a stack of handlers. Whenever |
|||
you add a record to the logger, it traverses the handler stack. Each handler |
|||
decides whether it fully handled the record, and if so, the propagation of the |
|||
record ends there. |
|||
|
|||
This allows for flexible logging setups, for example having a `StreamHandler` at |
|||
the bottom of the stack that will log anything to disk, and on top of that add |
|||
a `MailHandler` that will send emails only when an error message is logged. |
|||
Handlers also have a `$bubble` property which defines whether they block the |
|||
record or not if they handled it. In this example, setting the `MailHandler`'s |
|||
`$bubble` argument to false means that records handled by the `MailHandler` will |
|||
not propagate to the `StreamHandler` anymore. |
|||
|
|||
You can create many `Logger`s, each defining a channel (e.g.: db, request, |
|||
router, ..) and each of them combining various handlers, which can be shared |
|||
or not. The channel is reflected in the logs and allows you to easily see or |
|||
filter records. |
|||
|
|||
Each Handler also has a Formatter, a default one with settings that make sense |
|||
will be created if you don't set one. The formatters normalize and format |
|||
incoming records so that they can be used by the handlers to output useful |
|||
information. |
|||
|
|||
Custom severity levels are not available. Only the eight |
|||
[RFC 5424](http://tools.ietf.org/html/rfc5424) levels (debug, info, notice, |
|||
warning, error, critical, alert, emergency) are present for basic filtering |
|||
purposes, but for sorting and other use cases that would require |
|||
flexibility, you should add Processors to the Logger that can add extra |
|||
information (tags, user ip, ..) to the records before they are handled. |
|||
|
|||
## Log Levels |
|||
|
|||
Monolog supports the logging levels described by [RFC 5424](http://tools.ietf.org/html/rfc5424). |
|||
|
|||
- **DEBUG** (100): Detailed debug information. |
|||
|
|||
- **INFO** (200): Interesting events. Examples: User logs in, SQL logs. |
|||
|
|||
- **NOTICE** (250): Normal but significant events. |
|||
|
|||
- **WARNING** (300): Exceptional occurrences that are not errors. Examples: |
|||
Use of deprecated APIs, poor use of an API, undesirable things that are not |
|||
necessarily wrong. |
|||
|
|||
- **ERROR** (400): Runtime errors that do not require immediate action but |
|||
should typically be logged and monitored. |
|||
|
|||
- **CRITICAL** (500): Critical conditions. Example: Application component |
|||
unavailable, unexpected exception. |
|||
|
|||
- **ALERT** (550): Action must be taken immediately. Example: Entire website |
|||
down, database unavailable, etc. This should trigger the SMS alerts and wake |
|||
you up. |
|||
|
|||
- **EMERGENCY** (600): Emergency: system is unusable. |
|||
|
|||
## Configuring a logger |
|||
|
|||
Here is a basic setup to log to a file and to firephp on the DEBUG level: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\StreamHandler; |
|||
use Monolog\Handler\FirePHPHandler; |
|||
|
|||
// Create the logger |
|||
$logger = new Logger('my_logger'); |
|||
// Now add some handlers |
|||
$logger->pushHandler(new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG)); |
|||
$logger->pushHandler(new FirePHPHandler()); |
|||
|
|||
// You can now use your logger |
|||
$logger->addInfo('My logger is now ready'); |
|||
``` |
|||
|
|||
Let's explain it. The first step is to create the logger instance which will |
|||
be used in your code. The argument is a channel name, which is useful when |
|||
you use several loggers (see below for more details about it). |
|||
|
|||
The logger itself does not know how to handle a record. It delegates it to |
|||
some handlers. The code above registers two handlers in the stack to allow |
|||
handling records in two different ways. |
|||
|
|||
Note that the FirePHPHandler is called first as it is added on top of the |
|||
stack. This allows you to temporarily add a logger with bubbling disabled if |
|||
you want to override other configured loggers. |
|||
|
|||
> If you use Monolog standalone and are looking for an easy way to |
|||
> configure many handlers, the [theorchard/monolog-cascade](https://github.com/theorchard/monolog-cascade) |
|||
> can help you build complex logging configs via PHP arrays, yaml or json configs. |
|||
|
|||
## Adding extra data in the records |
|||
|
|||
Monolog provides two different ways to add extra informations along the simple |
|||
textual message. |
|||
|
|||
### Using the logging context |
|||
|
|||
The first way is the context, allowing to pass an array of data along the |
|||
record: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
$logger->addInfo('Adding a new user', array('username' => 'Seldaek')); |
|||
``` |
|||
|
|||
Simple handlers (like the StreamHandler for instance) will simply format |
|||
the array to a string but richer handlers can take advantage of the context |
|||
(FirePHP is able to display arrays in pretty way for instance). |
|||
|
|||
### Using processors |
|||
|
|||
The second way is to add extra data for all records by using a processor. |
|||
Processors can be any callable. They will get the record as parameter and |
|||
must return it after having eventually changed the `extra` part of it. Let's |
|||
write a processor adding some dummy data in the record: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
$logger->pushProcessor(function ($record) { |
|||
$record['extra']['dummy'] = 'Hello world!'; |
|||
|
|||
return $record; |
|||
}); |
|||
``` |
|||
|
|||
Monolog provides some built-in processors that can be used in your project. |
|||
Look at the [dedicated chapter](https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md#processors) for the list. |
|||
|
|||
> Tip: processors can also be registered on a specific handler instead of |
|||
the logger to apply only for this handler. |
|||
|
|||
## Leveraging channels |
|||
|
|||
Channels are a great way to identify to which part of the application a record |
|||
is related. This is useful in big applications (and is leveraged by |
|||
MonologBundle in Symfony2). |
|||
|
|||
Picture two loggers sharing a handler that writes to a single log file. |
|||
Channels would allow you to identify the logger that issued every record. |
|||
You can easily grep through the log files filtering this or that channel. |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\StreamHandler; |
|||
use Monolog\Handler\FirePHPHandler; |
|||
|
|||
// Create some handlers |
|||
$stream = new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG); |
|||
$firephp = new FirePHPHandler(); |
|||
|
|||
// Create the main logger of the app |
|||
$logger = new Logger('my_logger'); |
|||
$logger->pushHandler($stream); |
|||
$logger->pushHandler($firephp); |
|||
|
|||
// Create a logger for the security-related stuff with a different channel |
|||
$securityLogger = new Logger('security'); |
|||
$securityLogger->pushHandler($stream); |
|||
$securityLogger->pushHandler($firephp); |
|||
|
|||
// Or clone the first one to only change the channel |
|||
$securityLogger = $logger->withName('security'); |
|||
``` |
|||
|
|||
## Customizing the log format |
|||
|
|||
In Monolog it's easy to customize the format of the logs written into files, |
|||
sockets, mails, databases and other handlers. Most of the handlers use the |
|||
|
|||
```php |
|||
$record['formatted'] |
|||
``` |
|||
|
|||
value to be automatically put into the log device. This value depends on the |
|||
formatter settings. You can choose between predefined formatter classes or |
|||
write your own (e.g. a multiline text file for human-readable output). |
|||
|
|||
To configure a predefined formatter class, just set it as the handler's field: |
|||
|
|||
```php |
|||
// the default date format is "Y-m-d H:i:s" |
|||
$dateFormat = "Y n j, g:i a"; |
|||
// the default output format is "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n" |
|||
$output = "%datetime% > %level_name% > %message% %context% %extra%\n"; |
|||
// finally, create a formatter |
|||
$formatter = new LineFormatter($output, $dateFormat); |
|||
|
|||
// Create a handler |
|||
$stream = new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG); |
|||
$stream->setFormatter($formatter); |
|||
// bind it to a logger object |
|||
$securityLogger = new Logger('security'); |
|||
$securityLogger->pushHandler($stream); |
|||
``` |
|||
|
|||
You may also reuse the same formatter between multiple handlers and share those |
|||
handlers between multiple loggers. |
|||
|
|||
[Handlers, Formatters and Processors](02-handlers-formatters-processors.md) → |
|||
@ -0,0 +1,158 @@ |
|||
# Handlers, Formatters and Processors |
|||
|
|||
- [Handlers](#handlers) |
|||
- [Log to files and syslog](#log-to-files-and-syslog) |
|||
- [Send alerts and emails](#send-alerts-and-emails) |
|||
- [Log specific servers and networked logging](#log-specific-servers-and-networked-logging) |
|||
- [Logging in development](#logging-in-development) |
|||
- [Log to databases](#log-to-databases) |
|||
- [Wrappers / Special Handlers](#wrappers--special-handlers) |
|||
- [Formatters](#formatters) |
|||
- [Processors](#processors) |
|||
- [Third Party Packages](#third-party-packages) |
|||
|
|||
## Handlers |
|||
|
|||
### Log to files and syslog |
|||
|
|||
- _StreamHandler_: Logs records into any PHP stream, use this for log files. |
|||
- _RotatingFileHandler_: Logs records to a file and creates one logfile per day. |
|||
It will also delete files older than `$maxFiles`. You should use |
|||
[logrotate](http://linuxcommand.org/man_pages/logrotate8.html) for high profile |
|||
setups though, this is just meant as a quick and dirty solution. |
|||
- _SyslogHandler_: Logs records to the syslog. |
|||
- _ErrorLogHandler_: Logs records to PHP's |
|||
[`error_log()`](http://docs.php.net/manual/en/function.error-log.php) function. |
|||
|
|||
### Send alerts and emails |
|||
|
|||
- _NativeMailerHandler_: Sends emails using PHP's |
|||
[`mail()`](http://php.net/manual/en/function.mail.php) function. |
|||
- _SwiftMailerHandler_: Sends emails using a [`Swift_Mailer`](http://swiftmailer.org/) instance. |
|||
- _PushoverHandler_: Sends mobile notifications via the [Pushover](https://www.pushover.net/) API. |
|||
- _HipChatHandler_: Logs records to a [HipChat](http://hipchat.com) chat room using its API. |
|||
- _FlowdockHandler_: Logs records to a [Flowdock](https://www.flowdock.com/) account. |
|||
- _SlackHandler_: Logs records to a [Slack](https://www.slack.com/) account using the Slack API. |
|||
- _SlackbotHandler_: Logs records to a [Slack](https://www.slack.com/) account using the Slackbot incoming hook. |
|||
- _SlackWebhookHandler_: Logs records to a [Slack](https://www.slack.com/) account using Slack Webhooks. |
|||
- _MandrillHandler_: Sends emails via the Mandrill API using a [`Swift_Message`](http://swiftmailer.org/) instance. |
|||
- _FleepHookHandler_: Logs records to a [Fleep](https://fleep.io/) conversation using Webhooks. |
|||
- _IFTTTHandler_: Notifies an [IFTTT](https://ifttt.com/maker) trigger with the log channel, level name and message. |
|||
|
|||
### Log specific servers and networked logging |
|||
|
|||
- _SocketHandler_: Logs records to [sockets](http://php.net/fsockopen), use this |
|||
for UNIX and TCP sockets. See an [example](sockets.md). |
|||
- _AmqpHandler_: Logs records to an [amqp](http://www.amqp.org/) compatible |
|||
server. Requires the [php-amqp](http://pecl.php.net/package/amqp) extension (1.0+). |
|||
- _GelfHandler_: Logs records to a [Graylog2](http://www.graylog2.org) server. |
|||
- _CubeHandler_: Logs records to a [Cube](http://square.github.com/cube/) server. |
|||
- _RavenHandler_: Logs records to a [Sentry](http://getsentry.com/) server using |
|||
[raven](https://packagist.org/packages/raven/raven). |
|||
- _ZendMonitorHandler_: Logs records to the Zend Monitor present in Zend Server. |
|||
- _NewRelicHandler_: Logs records to a [NewRelic](http://newrelic.com/) application. |
|||
- _LogglyHandler_: Logs records to a [Loggly](http://www.loggly.com/) account. |
|||
- _RollbarHandler_: Logs records to a [Rollbar](https://rollbar.com/) account. |
|||
- _SyslogUdpHandler_: Logs records to a remote [Syslogd](http://www.rsyslog.com/) server. |
|||
- _LogEntriesHandler_: Logs records to a [LogEntries](http://logentries.com/) account. |
|||
- _InsightOpsHandler_: Logs records to a [InsightOps](https://www.rapid7.com/products/insightops/) account. |
|||
|
|||
### Logging in development |
|||
|
|||
- _FirePHPHandler_: Handler for [FirePHP](http://www.firephp.org/), providing |
|||
inline `console` messages within [FireBug](http://getfirebug.com/). |
|||
- _ChromePHPHandler_: Handler for [ChromePHP](http://www.chromephp.com/), providing |
|||
inline `console` messages within Chrome. |
|||
- _BrowserConsoleHandler_: Handler to send logs to browser's Javascript `console` with |
|||
no browser extension required. Most browsers supporting `console` API are supported. |
|||
- _PHPConsoleHandler_: Handler for [PHP Console](https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef), providing |
|||
inline `console` and notification popup messages within Chrome. |
|||
|
|||
### Log to databases |
|||
|
|||
- _RedisHandler_: Logs records to a [redis](http://redis.io) server. |
|||
- _MongoDBHandler_: Handler to write records in MongoDB via a |
|||
[Mongo](http://pecl.php.net/package/mongo) extension connection. |
|||
- _CouchDBHandler_: Logs records to a CouchDB server. |
|||
- _DoctrineCouchDBHandler_: Logs records to a CouchDB server via the Doctrine CouchDB ODM. |
|||
- _ElasticSearchHandler_: Logs records to an Elastic Search server. |
|||
- _DynamoDbHandler_: Logs records to a DynamoDB table with the [AWS SDK](https://github.com/aws/aws-sdk-php). |
|||
|
|||
### Wrappers / Special Handlers |
|||
|
|||
- _FingersCrossedHandler_: A very interesting wrapper. It takes a logger as |
|||
parameter and will accumulate log records of all levels until a record |
|||
exceeds the defined severity level. At which point it delivers all records, |
|||
including those of lower severity, to the handler it wraps. This means that |
|||
until an error actually happens you will not see anything in your logs, but |
|||
when it happens you will have the full information, including debug and info |
|||
records. This provides you with all the information you need, but only when |
|||
you need it. |
|||
- _DeduplicationHandler_: Useful if you are sending notifications or emails |
|||
when critical errors occur. It takes a logger as parameter and will |
|||
accumulate log records of all levels until the end of the request (or |
|||
`flush()` is called). At that point it delivers all records to the handler |
|||
it wraps, but only if the records are unique over a given time period |
|||
(60seconds by default). If the records are duplicates they are simply |
|||
discarded. The main use of this is in case of critical failure like if your |
|||
database is unreachable for example all your requests will fail and that |
|||
can result in a lot of notifications being sent. Adding this handler reduces |
|||
the amount of notifications to a manageable level. |
|||
- _WhatFailureGroupHandler_: This handler extends the _GroupHandler_ ignoring |
|||
exceptions raised by each child handler. This allows you to ignore issues |
|||
where a remote tcp connection may have died but you do not want your entire |
|||
application to crash and may wish to continue to log to other handlers. |
|||
- _BufferHandler_: This handler will buffer all the log records it receives |
|||
until `close()` is called at which point it will call `handleBatch()` on the |
|||
handler it wraps with all the log messages at once. This is very useful to |
|||
send an email with all records at once for example instead of having one mail |
|||
for every log record. |
|||
- _GroupHandler_: This handler groups other handlers. Every record received is |
|||
sent to all the handlers it is configured with. |
|||
- _FilterHandler_: This handler only lets records of the given levels through |
|||
to the wrapped handler. |
|||
- _SamplingHandler_: Wraps around another handler and lets you sample records |
|||
if you only want to store some of them. |
|||
- _NullHandler_: Any record it can handle will be thrown away. This can be used |
|||
to put on top of an existing handler stack to disable it temporarily. |
|||
- _PsrHandler_: Can be used to forward log records to an existing PSR-3 logger |
|||
- _TestHandler_: Used for testing, it records everything that is sent to it and |
|||
has accessors to read out the information. |
|||
- _HandlerWrapper_: A simple handler wrapper you can inherit from to create |
|||
your own wrappers easily. |
|||
|
|||
## Formatters |
|||
|
|||
- _LineFormatter_: Formats a log record into a one-line string. |
|||
- _HtmlFormatter_: Used to format log records into a human readable html table, mainly suitable for emails. |
|||
- _NormalizerFormatter_: Normalizes objects/resources down to strings so a record can easily be serialized/encoded. |
|||
- _ScalarFormatter_: Used to format log records into an associative array of scalar values. |
|||
- _JsonFormatter_: Encodes a log record into json. |
|||
- _WildfireFormatter_: Used to format log records into the Wildfire/FirePHP protocol, only useful for the FirePHPHandler. |
|||
- _ChromePHPFormatter_: Used to format log records into the ChromePHP format, only useful for the ChromePHPHandler. |
|||
- _GelfMessageFormatter_: Used to format log records into Gelf message instances, only useful for the GelfHandler. |
|||
- _LogstashFormatter_: Used to format log records into [logstash](http://logstash.net/) event json, useful for any handler listed under inputs [here](http://logstash.net/docs/latest). |
|||
- _ElasticaFormatter_: Used to format log records into an Elastica\Document object, only useful for the ElasticSearchHandler. |
|||
- _LogglyFormatter_: Used to format log records into Loggly messages, only useful for the LogglyHandler. |
|||
- _FlowdockFormatter_: Used to format log records into Flowdock messages, only useful for the FlowdockHandler. |
|||
- _MongoDBFormatter_: Converts \DateTime instances to \MongoDate and objects recursively to arrays, only useful with the MongoDBHandler. |
|||
|
|||
## Processors |
|||
|
|||
- _PsrLogMessageProcessor_: Processes a log record's message according to PSR-3 rules, replacing `{foo}` with the value from `$context['foo']`. |
|||
- _IntrospectionProcessor_: Adds the line/file/class/method from which the log call originated. |
|||
- _WebProcessor_: Adds the current request URI, request method and client IP to a log record. |
|||
- _MemoryUsageProcessor_: Adds the current memory usage to a log record. |
|||
- _MemoryPeakUsageProcessor_: Adds the peak memory usage to a log record. |
|||
- _ProcessIdProcessor_: Adds the process id to a log record. |
|||
- _UidProcessor_: Adds a unique identifier to a log record. |
|||
- _GitProcessor_: Adds the current git branch and commit to a log record. |
|||
- _TagProcessor_: Adds an array of predefined tags to a log record. |
|||
|
|||
## Third Party Packages |
|||
|
|||
Third party handlers, formatters and processors are |
|||
[listed in the wiki](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages). You |
|||
can also add your own there if you publish one. |
|||
|
|||
← [Usage](01-usage.md) | [Utility classes](03-utilities.md) → |
|||
@ -0,0 +1,15 @@ |
|||
# Utilities |
|||
|
|||
- _Registry_: The `Monolog\Registry` class lets you configure global loggers that you |
|||
can then statically access from anywhere. It is not really a best practice but can |
|||
help in some older codebases or for ease of use. |
|||
- _ErrorHandler_: The `Monolog\ErrorHandler` class allows you to easily register |
|||
a Logger instance as an exception handler, error handler or fatal error handler. |
|||
- _SignalHandler_: The `Monolog\SignalHandler` class allows you to easily register |
|||
a Logger instance as a POSIX signal handler. |
|||
- _ErrorLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain log |
|||
level is reached. |
|||
- _ChannelLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain |
|||
log level is reached, depending on which channel received the log record. |
|||
|
|||
← [Handlers, Formatters and Processors](02-handlers-formatters-processors.md) | [Extending Monolog](04-extending.md) → |
|||
@ -0,0 +1,76 @@ |
|||
# Extending Monolog |
|||
|
|||
Monolog is fully extensible, allowing you to adapt your logger to your needs. |
|||
|
|||
## Writing your own handler |
|||
|
|||
Monolog provides many built-in handlers. But if the one you need does not |
|||
exist, you can write it and use it in your logger. The only requirement is |
|||
to implement `Monolog\Handler\HandlerInterface`. |
|||
|
|||
Let's write a PDOHandler to log records to a database. We will extend the |
|||
abstract class provided by Monolog to keep things DRY. |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\AbstractProcessingHandler; |
|||
|
|||
class PDOHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $initialized = false; |
|||
private $pdo; |
|||
private $statement; |
|||
|
|||
public function __construct(PDO $pdo, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
$this->pdo = $pdo; |
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
protected function write(array $record) |
|||
{ |
|||
if (!$this->initialized) { |
|||
$this->initialize(); |
|||
} |
|||
|
|||
$this->statement->execute(array( |
|||
'channel' => $record['channel'], |
|||
'level' => $record['level'], |
|||
'message' => $record['formatted'], |
|||
'time' => $record['datetime']->format('U'), |
|||
)); |
|||
} |
|||
|
|||
private function initialize() |
|||
{ |
|||
$this->pdo->exec( |
|||
'CREATE TABLE IF NOT EXISTS monolog ' |
|||
.'(channel VARCHAR(255), level INTEGER, message LONGTEXT, time INTEGER UNSIGNED)' |
|||
); |
|||
$this->statement = $this->pdo->prepare( |
|||
'INSERT INTO monolog (channel, level, message, time) VALUES (:channel, :level, :message, :time)' |
|||
); |
|||
|
|||
$this->initialized = true; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
You can now use this handler in your logger: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
$logger->pushHandler(new PDOHandler(new PDO('sqlite:logs.sqlite'))); |
|||
|
|||
// You can now use your logger |
|||
$logger->addInfo('My logger is now ready'); |
|||
``` |
|||
|
|||
The `Monolog\Handler\AbstractProcessingHandler` class provides most of the |
|||
logic needed for the handler, including the use of processors and the formatting |
|||
of the record (which is why we use ``$record['formatted']`` instead of ``$record['message']``). |
|||
|
|||
← [Utility classes](03-utilities.md) |
|||
@ -0,0 +1,39 @@ |
|||
Sockets Handler |
|||
=============== |
|||
|
|||
This handler allows you to write your logs to sockets using [fsockopen](http://php.net/fsockopen) |
|||
or [pfsockopen](http://php.net/pfsockopen). |
|||
|
|||
Persistent sockets are mainly useful in web environments where you gain some performance not closing/opening |
|||
the connections between requests. |
|||
|
|||
You can use a `unix://` prefix to access unix sockets and `udp://` to open UDP sockets instead of the default TCP. |
|||
|
|||
Basic Example |
|||
------------- |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\SocketHandler; |
|||
|
|||
// Create the logger |
|||
$logger = new Logger('my_logger'); |
|||
|
|||
// Create the handler |
|||
$handler = new SocketHandler('unix:///var/log/httpd_app_log.socket'); |
|||
$handler->setPersistent(true); |
|||
|
|||
// Now add the handler |
|||
$logger->pushHandler($handler, Logger::DEBUG); |
|||
|
|||
// You can now use your logger |
|||
$logger->addInfo('My logger is now ready'); |
|||
|
|||
``` |
|||
|
|||
In this example, using syslog-ng, you should see the log on the log server: |
|||
|
|||
cweb1 [2012-02-26 00:12:03] my_logger.INFO: My logger is now ready [] [] |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
|
|||
<phpunit bootstrap="vendor/autoload.php" colors="true"> |
|||
<testsuites> |
|||
<testsuite name="Monolog Test Suite"> |
|||
<directory>tests/Monolog/</directory> |
|||
</testsuite> |
|||
</testsuites> |
|||
|
|||
<filter> |
|||
<whitelist> |
|||
<directory suffix=".php">src/Monolog/</directory> |
|||
</whitelist> |
|||
</filter> |
|||
|
|||
<php> |
|||
<ini name="date.timezone" value="UTC"/> |
|||
</php> |
|||
</phpunit> |
|||
@ -0,0 +1,239 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog; |
|||
|
|||
use Psr\Log\LoggerInterface; |
|||
use Psr\Log\LogLevel; |
|||
use Monolog\Handler\AbstractHandler; |
|||
use Monolog\Registry; |
|||
|
|||
/** |
|||
* Monolog error handler |
|||
* |
|||
* A facility to enable logging of runtime errors, exceptions and fatal errors. |
|||
* |
|||
* Quick setup: <code>ErrorHandler::register($logger);</code> |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class ErrorHandler |
|||
{ |
|||
private $logger; |
|||
|
|||
private $previousExceptionHandler; |
|||
private $uncaughtExceptionLevel; |
|||
|
|||
private $previousErrorHandler; |
|||
private $errorLevelMap; |
|||
private $handleOnlyReportedErrors; |
|||
|
|||
private $hasFatalErrorHandler; |
|||
private $fatalLevel; |
|||
private $reservedMemory; |
|||
private $lastFatalTrace; |
|||
private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR); |
|||
|
|||
public function __construct(LoggerInterface $logger) |
|||
{ |
|||
$this->logger = $logger; |
|||
} |
|||
|
|||
/** |
|||
* Registers a new ErrorHandler for a given Logger |
|||
* |
|||
* By default it will handle errors, exceptions and fatal errors |
|||
* |
|||
* @param LoggerInterface $logger |
|||
* @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling |
|||
* @param int|false $exceptionLevel a LogLevel::* constant, or false to disable exception handling |
|||
* @param int|false $fatalLevel a LogLevel::* constant, or false to disable fatal error handling |
|||
* @return ErrorHandler |
|||
*/ |
|||
public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null) |
|||
{ |
|||
//Forces the autoloader to run for LogLevel. Fixes an autoload issue at compile-time on PHP5.3. See https://github.com/Seldaek/monolog/pull/929 |
|||
class_exists('\\Psr\\Log\\LogLevel', true); |
|||
|
|||
$handler = new static($logger); |
|||
if ($errorLevelMap !== false) { |
|||
$handler->registerErrorHandler($errorLevelMap); |
|||
} |
|||
if ($exceptionLevel !== false) { |
|||
$handler->registerExceptionHandler($exceptionLevel); |
|||
} |
|||
if ($fatalLevel !== false) { |
|||
$handler->registerFatalHandler($fatalLevel); |
|||
} |
|||
|
|||
return $handler; |
|||
} |
|||
|
|||
public function registerExceptionHandler($level = null, $callPrevious = true) |
|||
{ |
|||
$prev = set_exception_handler(array($this, 'handleException')); |
|||
$this->uncaughtExceptionLevel = $level; |
|||
if ($callPrevious && $prev) { |
|||
$this->previousExceptionHandler = $prev; |
|||
} |
|||
} |
|||
|
|||
public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true) |
|||
{ |
|||
$prev = set_error_handler(array($this, 'handleError'), $errorTypes); |
|||
$this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); |
|||
if ($callPrevious) { |
|||
$this->previousErrorHandler = $prev ?: true; |
|||
} |
|||
|
|||
$this->handleOnlyReportedErrors = $handleOnlyReportedErrors; |
|||
} |
|||
|
|||
public function registerFatalHandler($level = null, $reservedMemorySize = 20) |
|||
{ |
|||
register_shutdown_function(array($this, 'handleFatalError')); |
|||
|
|||
$this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); |
|||
$this->fatalLevel = $level; |
|||
$this->hasFatalErrorHandler = true; |
|||
} |
|||
|
|||
protected function defaultErrorLevelMap() |
|||
{ |
|||
return array( |
|||
E_ERROR => LogLevel::CRITICAL, |
|||
E_WARNING => LogLevel::WARNING, |
|||
E_PARSE => LogLevel::ALERT, |
|||
E_NOTICE => LogLevel::NOTICE, |
|||
E_CORE_ERROR => LogLevel::CRITICAL, |
|||
E_CORE_WARNING => LogLevel::WARNING, |
|||
E_COMPILE_ERROR => LogLevel::ALERT, |
|||
E_COMPILE_WARNING => LogLevel::WARNING, |
|||
E_USER_ERROR => LogLevel::ERROR, |
|||
E_USER_WARNING => LogLevel::WARNING, |
|||
E_USER_NOTICE => LogLevel::NOTICE, |
|||
E_STRICT => LogLevel::NOTICE, |
|||
E_RECOVERABLE_ERROR => LogLevel::ERROR, |
|||
E_DEPRECATED => LogLevel::NOTICE, |
|||
E_USER_DEPRECATED => LogLevel::NOTICE, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @private |
|||
*/ |
|||
public function handleException($e) |
|||
{ |
|||
$this->logger->log( |
|||
$this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel, |
|||
sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), |
|||
array('exception' => $e) |
|||
); |
|||
|
|||
if ($this->previousExceptionHandler) { |
|||
call_user_func($this->previousExceptionHandler, $e); |
|||
} |
|||
|
|||
exit(255); |
|||
} |
|||
|
|||
/** |
|||
* @private |
|||
*/ |
|||
public function handleError($code, $message, $file = '', $line = 0, $context = array()) |
|||
{ |
|||
if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { |
|||
return; |
|||
} |
|||
|
|||
// fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries |
|||
if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { |
|||
$level = isset($this->errorLevelMap[$code]) ? $this->errorLevelMap[$code] : LogLevel::CRITICAL; |
|||
$this->logger->log($level, self::codeToString($code).': '.$message, array('code' => $code, 'message' => $message, 'file' => $file, 'line' => $line)); |
|||
} else { |
|||
// http://php.net/manual/en/function.debug-backtrace.php |
|||
// As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added. |
|||
// Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'. |
|||
$trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS); |
|||
array_shift($trace); // Exclude handleError from trace |
|||
$this->lastFatalTrace = $trace; |
|||
} |
|||
|
|||
if ($this->previousErrorHandler === true) { |
|||
return false; |
|||
} elseif ($this->previousErrorHandler) { |
|||
return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @private |
|||
*/ |
|||
public function handleFatalError() |
|||
{ |
|||
$this->reservedMemory = null; |
|||
|
|||
$lastError = error_get_last(); |
|||
if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { |
|||
$this->logger->log( |
|||
$this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel, |
|||
'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], |
|||
array('code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace) |
|||
); |
|||
|
|||
if ($this->logger instanceof Logger) { |
|||
foreach ($this->logger->getHandlers() as $handler) { |
|||
if ($handler instanceof AbstractHandler) { |
|||
$handler->close(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static function codeToString($code) |
|||
{ |
|||
switch ($code) { |
|||
case E_ERROR: |
|||
return 'E_ERROR'; |
|||
case E_WARNING: |
|||
return 'E_WARNING'; |
|||
case E_PARSE: |
|||
return 'E_PARSE'; |
|||
case E_NOTICE: |
|||
return 'E_NOTICE'; |
|||
case E_CORE_ERROR: |
|||
return 'E_CORE_ERROR'; |
|||
case E_CORE_WARNING: |
|||
return 'E_CORE_WARNING'; |
|||
case E_COMPILE_ERROR: |
|||
return 'E_COMPILE_ERROR'; |
|||
case E_COMPILE_WARNING: |
|||
return 'E_COMPILE_WARNING'; |
|||
case E_USER_ERROR: |
|||
return 'E_USER_ERROR'; |
|||
case E_USER_WARNING: |
|||
return 'E_USER_WARNING'; |
|||
case E_USER_NOTICE: |
|||
return 'E_USER_NOTICE'; |
|||
case E_STRICT: |
|||
return 'E_STRICT'; |
|||
case E_RECOVERABLE_ERROR: |
|||
return 'E_RECOVERABLE_ERROR'; |
|||
case E_DEPRECATED: |
|||
return 'E_DEPRECATED'; |
|||
case E_USER_DEPRECATED: |
|||
return 'E_USER_DEPRECATED'; |
|||
} |
|||
|
|||
return 'Unknown PHP error'; |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Formats a log message according to the ChromePHP array format |
|||
* |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
*/ |
|||
class ChromePHPFormatter implements FormatterInterface |
|||
{ |
|||
/** |
|||
* Translates Monolog log levels to Wildfire levels. |
|||
*/ |
|||
private $logLevels = array( |
|||
Logger::DEBUG => 'log', |
|||
Logger::INFO => 'info', |
|||
Logger::NOTICE => 'info', |
|||
Logger::WARNING => 'warn', |
|||
Logger::ERROR => 'error', |
|||
Logger::CRITICAL => 'error', |
|||
Logger::ALERT => 'error', |
|||
Logger::EMERGENCY => 'error', |
|||
); |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
// Retrieve the line and file if set and remove them from the formatted extra |
|||
$backtrace = 'unknown'; |
|||
if (isset($record['extra']['file'], $record['extra']['line'])) { |
|||
$backtrace = $record['extra']['file'].' : '.$record['extra']['line']; |
|||
unset($record['extra']['file'], $record['extra']['line']); |
|||
} |
|||
|
|||
$message = array('message' => $record['message']); |
|||
if ($record['context']) { |
|||
$message['context'] = $record['context']; |
|||
} |
|||
if ($record['extra']) { |
|||
$message['extra'] = $record['extra']; |
|||
} |
|||
if (count($message) === 1) { |
|||
$message = reset($message); |
|||
} |
|||
|
|||
return array( |
|||
$record['channel'], |
|||
$message, |
|||
$backtrace, |
|||
$this->logLevels[$record['level']], |
|||
); |
|||
} |
|||
|
|||
public function formatBatch(array $records) |
|||
{ |
|||
$formatted = array(); |
|||
|
|||
foreach ($records as $record) { |
|||
$formatted[] = $this->format($record); |
|||
} |
|||
|
|||
return $formatted; |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Elastica\Document; |
|||
|
|||
/** |
|||
* Format a log message into an Elastica Document |
|||
* |
|||
* @author Jelle Vink <jelle.vink@gmail.com> |
|||
*/ |
|||
class ElasticaFormatter extends NormalizerFormatter |
|||
{ |
|||
/** |
|||
* @var string Elastic search index name |
|||
*/ |
|||
protected $index; |
|||
|
|||
/** |
|||
* @var string Elastic search document type |
|||
*/ |
|||
protected $type; |
|||
|
|||
/** |
|||
* @param string $index Elastic Search index name |
|||
* @param string $type Elastic Search document type |
|||
*/ |
|||
public function __construct($index, $type) |
|||
{ |
|||
// elasticsearch requires a ISO 8601 format date with optional millisecond precision. |
|||
parent::__construct('Y-m-d\TH:i:s.uP'); |
|||
|
|||
$this->index = $index; |
|||
$this->type = $type; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$record = parent::format($record); |
|||
|
|||
return $this->getDocument($record); |
|||
} |
|||
|
|||
/** |
|||
* Getter index |
|||
* @return string |
|||
*/ |
|||
public function getIndex() |
|||
{ |
|||
return $this->index; |
|||
} |
|||
|
|||
/** |
|||
* Getter type |
|||
* @return string |
|||
*/ |
|||
public function getType() |
|||
{ |
|||
return $this->type; |
|||
} |
|||
|
|||
/** |
|||
* Convert a log message into an Elastica Document |
|||
* |
|||
* @param array $record Log message |
|||
* @return Document |
|||
*/ |
|||
protected function getDocument($record) |
|||
{ |
|||
$document = new Document(); |
|||
$document->setData($record); |
|||
$document->setType($this->type); |
|||
$document->setIndex($this->index); |
|||
|
|||
return $document; |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* formats the record to be used in the FlowdockHandler |
|||
* |
|||
* @author Dominik Liebler <liebler.dominik@gmail.com> |
|||
*/ |
|||
class FlowdockFormatter implements FormatterInterface |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $source; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $sourceEmail; |
|||
|
|||
/** |
|||
* @param string $source |
|||
* @param string $sourceEmail |
|||
*/ |
|||
public function __construct($source, $sourceEmail) |
|||
{ |
|||
$this->source = $source; |
|||
$this->sourceEmail = $sourceEmail; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$tags = array( |
|||
'#logs', |
|||
'#' . strtolower($record['level_name']), |
|||
'#' . $record['channel'], |
|||
); |
|||
|
|||
foreach ($record['extra'] as $value) { |
|||
$tags[] = '#' . $value; |
|||
} |
|||
|
|||
$subject = sprintf( |
|||
'in %s: %s - %s', |
|||
$this->source, |
|||
$record['level_name'], |
|||
$this->getShortMessage($record['message']) |
|||
); |
|||
|
|||
$record['flowdock'] = array( |
|||
'source' => $this->source, |
|||
'from_address' => $this->sourceEmail, |
|||
'subject' => $subject, |
|||
'content' => $record['message'], |
|||
'tags' => $tags, |
|||
'project' => $this->source, |
|||
); |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function formatBatch(array $records) |
|||
{ |
|||
$formatted = array(); |
|||
|
|||
foreach ($records as $record) { |
|||
$formatted[] = $this->format($record); |
|||
} |
|||
|
|||
return $formatted; |
|||
} |
|||
|
|||
/** |
|||
* @param string $message |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getShortMessage($message) |
|||
{ |
|||
static $hasMbString; |
|||
|
|||
if (null === $hasMbString) { |
|||
$hasMbString = function_exists('mb_strlen'); |
|||
} |
|||
|
|||
$maxLength = 45; |
|||
|
|||
if ($hasMbString) { |
|||
if (mb_strlen($message, 'UTF-8') > $maxLength) { |
|||
$message = mb_substr($message, 0, $maxLength - 4, 'UTF-8') . ' ...'; |
|||
} |
|||
} else { |
|||
if (strlen($message) > $maxLength) { |
|||
$message = substr($message, 0, $maxLength - 4) . ' ...'; |
|||
} |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* Class FluentdFormatter |
|||
* |
|||
* Serializes a log message to Fluentd unix socket protocol |
|||
* |
|||
* Fluentd config: |
|||
* |
|||
* <source> |
|||
* type unix |
|||
* path /var/run/td-agent/td-agent.sock |
|||
* </source> |
|||
* |
|||
* Monolog setup: |
|||
* |
|||
* $logger = new Monolog\Logger('fluent.tag'); |
|||
* $fluentHandler = new Monolog\Handler\SocketHandler('unix:///var/run/td-agent/td-agent.sock'); |
|||
* $fluentHandler->setFormatter(new Monolog\Formatter\FluentdFormatter()); |
|||
* $logger->pushHandler($fluentHandler); |
|||
* |
|||
* @author Andrius Putna <fordnox@gmail.com> |
|||
*/ |
|||
class FluentdFormatter implements FormatterInterface |
|||
{ |
|||
/** |
|||
* @var bool $levelTag should message level be a part of the fluentd tag |
|||
*/ |
|||
protected $levelTag = false; |
|||
|
|||
public function __construct($levelTag = false) |
|||
{ |
|||
if (!function_exists('json_encode')) { |
|||
throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s FluentdUnixFormatter'); |
|||
} |
|||
|
|||
$this->levelTag = (bool) $levelTag; |
|||
} |
|||
|
|||
public function isUsingLevelsInTag() |
|||
{ |
|||
return $this->levelTag; |
|||
} |
|||
|
|||
public function format(array $record) |
|||
{ |
|||
$tag = $record['channel']; |
|||
if ($this->levelTag) { |
|||
$tag .= '.' . strtolower($record['level_name']); |
|||
} |
|||
|
|||
$message = array( |
|||
'message' => $record['message'], |
|||
'context' => $record['context'], |
|||
'extra' => $record['extra'], |
|||
); |
|||
|
|||
if (!$this->levelTag) { |
|||
$message['level'] = $record['level']; |
|||
$message['level_name'] = $record['level_name']; |
|||
} |
|||
|
|||
return json_encode(array($tag, $record['datetime']->getTimestamp(), $message)); |
|||
} |
|||
|
|||
public function formatBatch(array $records) |
|||
{ |
|||
$message = ''; |
|||
foreach ($records as $record) { |
|||
$message .= $this->format($record); |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* Interface for formatters |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
interface FormatterInterface |
|||
{ |
|||
/** |
|||
* Formats a log record. |
|||
* |
|||
* @param array $record A record to format |
|||
* @return mixed The formatted record |
|||
*/ |
|||
public function format(array $record); |
|||
|
|||
/** |
|||
* Formats a set of log records. |
|||
* |
|||
* @param array $records A set of records to format |
|||
* @return mixed The formatted set of records |
|||
*/ |
|||
public function formatBatch(array $records); |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Logger; |
|||
use Gelf\Message; |
|||
|
|||
/** |
|||
* Serializes a log message to GELF |
|||
* @see http://www.graylog2.org/about/gelf |
|||
* |
|||
* @author Matt Lehner <mlehner@gmail.com> |
|||
*/ |
|||
class GelfMessageFormatter extends NormalizerFormatter |
|||
{ |
|||
const DEFAULT_MAX_LENGTH = 32766; |
|||
|
|||
/** |
|||
* @var string the name of the system for the Gelf log message |
|||
*/ |
|||
protected $systemName; |
|||
|
|||
/** |
|||
* @var string a prefix for 'extra' fields from the Monolog record (optional) |
|||
*/ |
|||
protected $extraPrefix; |
|||
|
|||
/** |
|||
* @var string a prefix for 'context' fields from the Monolog record (optional) |
|||
*/ |
|||
protected $contextPrefix; |
|||
|
|||
/** |
|||
* @var int max length per field |
|||
*/ |
|||
protected $maxLength; |
|||
|
|||
/** |
|||
* Translates Monolog log levels to Graylog2 log priorities. |
|||
*/ |
|||
private $logLevels = array( |
|||
Logger::DEBUG => 7, |
|||
Logger::INFO => 6, |
|||
Logger::NOTICE => 5, |
|||
Logger::WARNING => 4, |
|||
Logger::ERROR => 3, |
|||
Logger::CRITICAL => 2, |
|||
Logger::ALERT => 1, |
|||
Logger::EMERGENCY => 0, |
|||
); |
|||
|
|||
public function __construct($systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $maxLength = null) |
|||
{ |
|||
parent::__construct('U.u'); |
|||
|
|||
$this->systemName = $systemName ?: gethostname(); |
|||
|
|||
$this->extraPrefix = $extraPrefix; |
|||
$this->contextPrefix = $contextPrefix; |
|||
$this->maxLength = is_null($maxLength) ? self::DEFAULT_MAX_LENGTH : $maxLength; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$record = parent::format($record); |
|||
|
|||
if (!isset($record['datetime'], $record['message'], $record['level'])) { |
|||
throw new \InvalidArgumentException('The record should at least contain datetime, message and level keys, '.var_export($record, true).' given'); |
|||
} |
|||
|
|||
$message = new Message(); |
|||
$message |
|||
->setTimestamp($record['datetime']) |
|||
->setShortMessage((string) $record['message']) |
|||
->setHost($this->systemName) |
|||
->setLevel($this->logLevels[$record['level']]); |
|||
|
|||
// message length + system name length + 200 for padding / metadata |
|||
$len = 200 + strlen((string) $record['message']) + strlen($this->systemName); |
|||
|
|||
if ($len > $this->maxLength) { |
|||
$message->setShortMessage(substr($record['message'], 0, $this->maxLength)); |
|||
} |
|||
|
|||
if (isset($record['channel'])) { |
|||
$message->setFacility($record['channel']); |
|||
} |
|||
if (isset($record['extra']['line'])) { |
|||
$message->setLine($record['extra']['line']); |
|||
unset($record['extra']['line']); |
|||
} |
|||
if (isset($record['extra']['file'])) { |
|||
$message->setFile($record['extra']['file']); |
|||
unset($record['extra']['file']); |
|||
} |
|||
|
|||
foreach ($record['extra'] as $key => $val) { |
|||
$val = is_scalar($val) || null === $val ? $val : $this->toJson($val); |
|||
$len = strlen($this->extraPrefix . $key . $val); |
|||
if ($len > $this->maxLength) { |
|||
$message->setAdditional($this->extraPrefix . $key, substr($val, 0, $this->maxLength)); |
|||
break; |
|||
} |
|||
$message->setAdditional($this->extraPrefix . $key, $val); |
|||
} |
|||
|
|||
foreach ($record['context'] as $key => $val) { |
|||
$val = is_scalar($val) || null === $val ? $val : $this->toJson($val); |
|||
$len = strlen($this->contextPrefix . $key . $val); |
|||
if ($len > $this->maxLength) { |
|||
$message->setAdditional($this->contextPrefix . $key, substr($val, 0, $this->maxLength)); |
|||
break; |
|||
} |
|||
$message->setAdditional($this->contextPrefix . $key, $val); |
|||
} |
|||
|
|||
if (null === $message->getFile() && isset($record['context']['exception']['file'])) { |
|||
if (preg_match("/^(.+):([0-9]+)$/", $record['context']['exception']['file'], $matches)) { |
|||
$message->setFile($matches[1]); |
|||
$message->setLine($matches[2]); |
|||
} |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
<?php |
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Formats incoming records into an HTML table |
|||
* |
|||
* This is especially useful for html email logging |
|||
* |
|||
* @author Tiago Brito <tlfbrito@gmail.com> |
|||
*/ |
|||
class HtmlFormatter extends NormalizerFormatter |
|||
{ |
|||
/** |
|||
* Translates Monolog log levels to html color priorities. |
|||
*/ |
|||
protected $logLevels = array( |
|||
Logger::DEBUG => '#cccccc', |
|||
Logger::INFO => '#468847', |
|||
Logger::NOTICE => '#3a87ad', |
|||
Logger::WARNING => '#c09853', |
|||
Logger::ERROR => '#f0ad4e', |
|||
Logger::CRITICAL => '#FF7708', |
|||
Logger::ALERT => '#C12A19', |
|||
Logger::EMERGENCY => '#000000', |
|||
); |
|||
|
|||
/** |
|||
* @param string $dateFormat The format of the timestamp: one supported by DateTime::format |
|||
*/ |
|||
public function __construct($dateFormat = null) |
|||
{ |
|||
parent::__construct($dateFormat); |
|||
} |
|||
|
|||
/** |
|||
* Creates an HTML table row |
|||
* |
|||
* @param string $th Row header content |
|||
* @param string $td Row standard cell content |
|||
* @param bool $escapeTd false if td content must not be html escaped |
|||
* @return string |
|||
*/ |
|||
protected function addRow($th, $td = ' ', $escapeTd = true) |
|||
{ |
|||
$th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8'); |
|||
if ($escapeTd) { |
|||
$td = '<pre>'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'</pre>'; |
|||
} |
|||
|
|||
return "<tr style=\"padding: 4px;text-align: left;\">\n<th style=\"vertical-align: top;background: #ccc;color: #000\" width=\"100\">$th:</th>\n<td style=\"padding: 4px;text-align: left;vertical-align: top;background: #eee;color: #000\">".$td."</td>\n</tr>"; |
|||
} |
|||
|
|||
/** |
|||
* Create a HTML h1 tag |
|||
* |
|||
* @param string $title Text to be in the h1 |
|||
* @param int $level Error level |
|||
* @return string |
|||
*/ |
|||
protected function addTitle($title, $level) |
|||
{ |
|||
$title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8'); |
|||
|
|||
return '<h1 style="background: '.$this->logLevels[$level].';color: #ffffff;padding: 5px;" class="monolog-output">'.$title.'</h1>'; |
|||
} |
|||
|
|||
/** |
|||
* Formats a log record. |
|||
* |
|||
* @param array $record A record to format |
|||
* @return mixed The formatted record |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$output = $this->addTitle($record['level_name'], $record['level']); |
|||
$output .= '<table cellspacing="1" width="100%" class="monolog-output">'; |
|||
|
|||
$output .= $this->addRow('Message', (string) $record['message']); |
|||
$output .= $this->addRow('Time', $record['datetime']->format($this->dateFormat)); |
|||
$output .= $this->addRow('Channel', $record['channel']); |
|||
if ($record['context']) { |
|||
$embeddedTable = '<table cellspacing="1" width="100%">'; |
|||
foreach ($record['context'] as $key => $value) { |
|||
$embeddedTable .= $this->addRow($key, $this->convertToString($value)); |
|||
} |
|||
$embeddedTable .= '</table>'; |
|||
$output .= $this->addRow('Context', $embeddedTable, false); |
|||
} |
|||
if ($record['extra']) { |
|||
$embeddedTable = '<table cellspacing="1" width="100%">'; |
|||
foreach ($record['extra'] as $key => $value) { |
|||
$embeddedTable .= $this->addRow($key, $this->convertToString($value)); |
|||
} |
|||
$embeddedTable .= '</table>'; |
|||
$output .= $this->addRow('Extra', $embeddedTable, false); |
|||
} |
|||
|
|||
return $output.'</table>'; |
|||
} |
|||
|
|||
/** |
|||
* Formats a set of log records. |
|||
* |
|||
* @param array $records A set of records to format |
|||
* @return mixed The formatted set of records |
|||
*/ |
|||
public function formatBatch(array $records) |
|||
{ |
|||
$message = ''; |
|||
foreach ($records as $record) { |
|||
$message .= $this->format($record); |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
|
|||
protected function convertToString($data) |
|||
{ |
|||
if (null === $data || is_scalar($data)) { |
|||
return (string) $data; |
|||
} |
|||
|
|||
$data = $this->normalize($data); |
|||
if (version_compare(PHP_VERSION, '5.4.0', '>=')) { |
|||
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); |
|||
} |
|||
|
|||
return str_replace('\\/', '/', json_encode($data)); |
|||
} |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Exception; |
|||
use Monolog\Utils; |
|||
use Throwable; |
|||
|
|||
/** |
|||
* Encodes whatever record data is passed to it as json |
|||
* |
|||
* This can be useful to log to databases or remote APIs |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class JsonFormatter extends NormalizerFormatter |
|||
{ |
|||
const BATCH_MODE_JSON = 1; |
|||
const BATCH_MODE_NEWLINES = 2; |
|||
|
|||
protected $batchMode; |
|||
protected $appendNewline; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $includeStacktraces = false; |
|||
|
|||
/** |
|||
* @param int $batchMode |
|||
* @param bool $appendNewline |
|||
*/ |
|||
public function __construct($batchMode = self::BATCH_MODE_JSON, $appendNewline = true) |
|||
{ |
|||
$this->batchMode = $batchMode; |
|||
$this->appendNewline = $appendNewline; |
|||
} |
|||
|
|||
/** |
|||
* The batch mode option configures the formatting style for |
|||
* multiple records. By default, multiple records will be |
|||
* formatted as a JSON-encoded array. However, for |
|||
* compatibility with some API endpoints, alternative styles |
|||
* are available. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getBatchMode() |
|||
{ |
|||
return $this->batchMode; |
|||
} |
|||
|
|||
/** |
|||
* True if newlines are appended to every formatted record |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isAppendingNewlines() |
|||
{ |
|||
return $this->appendNewline; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : ''); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function formatBatch(array $records) |
|||
{ |
|||
switch ($this->batchMode) { |
|||
case static::BATCH_MODE_NEWLINES: |
|||
return $this->formatBatchNewlines($records); |
|||
|
|||
case static::BATCH_MODE_JSON: |
|||
default: |
|||
return $this->formatBatchJson($records); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param bool $include |
|||
*/ |
|||
public function includeStacktraces($include = true) |
|||
{ |
|||
$this->includeStacktraces = $include; |
|||
} |
|||
|
|||
/** |
|||
* Return a JSON-encoded array of records. |
|||
* |
|||
* @param array $records |
|||
* @return string |
|||
*/ |
|||
protected function formatBatchJson(array $records) |
|||
{ |
|||
return $this->toJson($this->normalize($records), true); |
|||
} |
|||
|
|||
/** |
|||
* Use new lines to separate records instead of a |
|||
* JSON-encoded array. |
|||
* |
|||
* @param array $records |
|||
* @return string |
|||
*/ |
|||
protected function formatBatchNewlines(array $records) |
|||
{ |
|||
$instance = $this; |
|||
|
|||
$oldNewline = $this->appendNewline; |
|||
$this->appendNewline = false; |
|||
array_walk($records, function (&$value, $key) use ($instance) { |
|||
$value = $instance->format($value); |
|||
}); |
|||
$this->appendNewline = $oldNewline; |
|||
|
|||
return implode("\n", $records); |
|||
} |
|||
|
|||
/** |
|||
* Normalizes given $data. |
|||
* |
|||
* @param mixed $data |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function normalize($data, $depth = 0) |
|||
{ |
|||
if ($depth > 9) { |
|||
return 'Over 9 levels deep, aborting normalization'; |
|||
} |
|||
|
|||
if (is_array($data) || $data instanceof \Traversable) { |
|||
$normalized = array(); |
|||
|
|||
$count = 1; |
|||
foreach ($data as $key => $value) { |
|||
if ($count++ > 1000) { |
|||
$normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization'; |
|||
break; |
|||
} |
|||
|
|||
$normalized[$key] = $this->normalize($value, $depth+1); |
|||
} |
|||
|
|||
return $normalized; |
|||
} |
|||
|
|||
if ($data instanceof Exception || $data instanceof Throwable) { |
|||
return $this->normalizeException($data); |
|||
} |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
/** |
|||
* Normalizes given exception with or without its own stack trace based on |
|||
* `includeStacktraces` property. |
|||
* |
|||
* @param Exception|Throwable $e |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function normalizeException($e) |
|||
{ |
|||
// TODO 2.0 only check for Throwable |
|||
if (!$e instanceof Exception && !$e instanceof Throwable) { |
|||
throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); |
|||
} |
|||
|
|||
$data = array( |
|||
'class' => Utils::getClass($e), |
|||
'message' => $e->getMessage(), |
|||
'code' => $e->getCode(), |
|||
'file' => $e->getFile().':'.$e->getLine(), |
|||
); |
|||
|
|||
if ($this->includeStacktraces) { |
|||
$trace = $e->getTrace(); |
|||
foreach ($trace as $frame) { |
|||
if (isset($frame['file'])) { |
|||
$data['trace'][] = $frame['file'].':'.$frame['line']; |
|||
} elseif (isset($frame['function']) && $frame['function'] === '{closure}') { |
|||
// We should again normalize the frames, because it might contain invalid items |
|||
$data['trace'][] = $frame['function']; |
|||
} else { |
|||
// We should again normalize the frames, because it might contain invalid items |
|||
$data['trace'][] = $this->normalize($frame); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if ($previous = $e->getPrevious()) { |
|||
$data['previous'] = $this->normalizeException($previous); |
|||
} |
|||
|
|||
return $data; |
|||
} |
|||
} |
|||
@ -0,0 +1,181 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Utils; |
|||
|
|||
/** |
|||
* Formats incoming records into a one-line string |
|||
* |
|||
* This is especially useful for logging to files |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
*/ |
|||
class LineFormatter extends NormalizerFormatter |
|||
{ |
|||
const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; |
|||
|
|||
protected $format; |
|||
protected $allowInlineLineBreaks; |
|||
protected $ignoreEmptyContextAndExtra; |
|||
protected $includeStacktraces; |
|||
|
|||
/** |
|||
* @param string $format The format of the message |
|||
* @param string $dateFormat The format of the timestamp: one supported by DateTime::format |
|||
* @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries |
|||
* @param bool $ignoreEmptyContextAndExtra |
|||
*/ |
|||
public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = false) |
|||
{ |
|||
$this->format = $format ?: static::SIMPLE_FORMAT; |
|||
$this->allowInlineLineBreaks = $allowInlineLineBreaks; |
|||
$this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; |
|||
parent::__construct($dateFormat); |
|||
} |
|||
|
|||
public function includeStacktraces($include = true) |
|||
{ |
|||
$this->includeStacktraces = $include; |
|||
if ($this->includeStacktraces) { |
|||
$this->allowInlineLineBreaks = true; |
|||
} |
|||
} |
|||
|
|||
public function allowInlineLineBreaks($allow = true) |
|||
{ |
|||
$this->allowInlineLineBreaks = $allow; |
|||
} |
|||
|
|||
public function ignoreEmptyContextAndExtra($ignore = true) |
|||
{ |
|||
$this->ignoreEmptyContextAndExtra = $ignore; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$vars = parent::format($record); |
|||
|
|||
$output = $this->format; |
|||
|
|||
foreach ($vars['extra'] as $var => $val) { |
|||
if (false !== strpos($output, '%extra.'.$var.'%')) { |
|||
$output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output); |
|||
unset($vars['extra'][$var]); |
|||
} |
|||
} |
|||
|
|||
|
|||
foreach ($vars['context'] as $var => $val) { |
|||
if (false !== strpos($output, '%context.'.$var.'%')) { |
|||
$output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); |
|||
unset($vars['context'][$var]); |
|||
} |
|||
} |
|||
|
|||
if ($this->ignoreEmptyContextAndExtra) { |
|||
if (empty($vars['context'])) { |
|||
unset($vars['context']); |
|||
$output = str_replace('%context%', '', $output); |
|||
} |
|||
|
|||
if (empty($vars['extra'])) { |
|||
unset($vars['extra']); |
|||
$output = str_replace('%extra%', '', $output); |
|||
} |
|||
} |
|||
|
|||
foreach ($vars as $var => $val) { |
|||
if (false !== strpos($output, '%'.$var.'%')) { |
|||
$output = str_replace('%'.$var.'%', $this->stringify($val), $output); |
|||
} |
|||
} |
|||
|
|||
// remove leftover %extra.xxx% and %context.xxx% if any |
|||
if (false !== strpos($output, '%')) { |
|||
$output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); |
|||
} |
|||
|
|||
return $output; |
|||
} |
|||
|
|||
public function formatBatch(array $records) |
|||
{ |
|||
$message = ''; |
|||
foreach ($records as $record) { |
|||
$message .= $this->format($record); |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
|
|||
public function stringify($value) |
|||
{ |
|||
return $this->replaceNewlines($this->convertToString($value)); |
|||
} |
|||
|
|||
protected function normalizeException($e) |
|||
{ |
|||
// TODO 2.0 only check for Throwable |
|||
if (!$e instanceof \Exception && !$e instanceof \Throwable) { |
|||
throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); |
|||
} |
|||
|
|||
$previousText = ''; |
|||
if ($previous = $e->getPrevious()) { |
|||
do { |
|||
$previousText .= ', '.Utils::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); |
|||
} while ($previous = $previous->getPrevious()); |
|||
} |
|||
|
|||
$str = '[object] ('.Utils::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; |
|||
if ($this->includeStacktraces) { |
|||
$str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n"; |
|||
} |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
protected function convertToString($data) |
|||
{ |
|||
if (null === $data || is_bool($data)) { |
|||
return var_export($data, true); |
|||
} |
|||
|
|||
if (is_scalar($data)) { |
|||
return (string) $data; |
|||
} |
|||
|
|||
if (version_compare(PHP_VERSION, '5.4.0', '>=')) { |
|||
return $this->toJson($data, true); |
|||
} |
|||
|
|||
return str_replace('\\/', '/', @json_encode($data)); |
|||
} |
|||
|
|||
protected function replaceNewlines($str) |
|||
{ |
|||
if ($this->allowInlineLineBreaks) { |
|||
if (0 === strpos($str, '{')) { |
|||
return str_replace(array('\r', '\n'), array("\r", "\n"), $str); |
|||
} |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
return str_replace(array("\r\n", "\r", "\n"), ' ', $str); |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* Encodes message information into JSON in a format compatible with Loggly. |
|||
* |
|||
* @author Adam Pancutt <adam@pancutt.com> |
|||
*/ |
|||
class LogglyFormatter extends JsonFormatter |
|||
{ |
|||
/** |
|||
* Overrides the default batch mode to new lines for compatibility with the |
|||
* Loggly bulk API. |
|||
* |
|||
* @param int $batchMode |
|||
*/ |
|||
public function __construct($batchMode = self::BATCH_MODE_NEWLINES, $appendNewline = false) |
|||
{ |
|||
parent::__construct($batchMode, $appendNewline); |
|||
} |
|||
|
|||
/** |
|||
* Appends the 'timestamp' parameter for indexing by Loggly. |
|||
* |
|||
* @see https://www.loggly.com/docs/automated-parsing/#json |
|||
* @see \Monolog\Formatter\JsonFormatter::format() |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTime)) { |
|||
$record["timestamp"] = $record["datetime"]->format("Y-m-d\TH:i:s.uO"); |
|||
// TODO 2.0 unset the 'datetime' parameter, retained for BC |
|||
} |
|||
|
|||
return parent::format($record); |
|||
} |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* Serializes a log message to Logstash Event Format |
|||
* |
|||
* @see http://logstash.net/ |
|||
* @see https://github.com/logstash/logstash/blob/master/lib/logstash/event.rb |
|||
* |
|||
* @author Tim Mower <timothy.mower@gmail.com> |
|||
*/ |
|||
class LogstashFormatter extends NormalizerFormatter |
|||
{ |
|||
const V0 = 0; |
|||
const V1 = 1; |
|||
|
|||
/** |
|||
* @var string the name of the system for the Logstash log message, used to fill the @source field |
|||
*/ |
|||
protected $systemName; |
|||
|
|||
/** |
|||
* @var string an application name for the Logstash log message, used to fill the @type field |
|||
*/ |
|||
protected $applicationName; |
|||
|
|||
/** |
|||
* @var string a prefix for 'extra' fields from the Monolog record (optional) |
|||
*/ |
|||
protected $extraPrefix; |
|||
|
|||
/** |
|||
* @var string a prefix for 'context' fields from the Monolog record (optional) |
|||
*/ |
|||
protected $contextPrefix; |
|||
|
|||
/** |
|||
* @var int logstash format version to use |
|||
*/ |
|||
protected $version; |
|||
|
|||
/** |
|||
* @param string $applicationName the application that sends the data, used as the "type" field of logstash |
|||
* @param string $systemName the system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine |
|||
* @param string $extraPrefix prefix for extra keys inside logstash "fields" |
|||
* @param string $contextPrefix prefix for context keys inside logstash "fields", defaults to ctxt_ |
|||
* @param int $version the logstash format version to use, defaults to 0 |
|||
*/ |
|||
public function __construct($applicationName, $systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $version = self::V0) |
|||
{ |
|||
// logstash requires a ISO 8601 format date with optional millisecond precision. |
|||
parent::__construct('Y-m-d\TH:i:s.uP'); |
|||
|
|||
$this->systemName = $systemName ?: gethostname(); |
|||
$this->applicationName = $applicationName; |
|||
$this->extraPrefix = $extraPrefix; |
|||
$this->contextPrefix = $contextPrefix; |
|||
$this->version = $version; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
$record = parent::format($record); |
|||
|
|||
if ($this->version === self::V1) { |
|||
$message = $this->formatV1($record); |
|||
} else { |
|||
$message = $this->formatV0($record); |
|||
} |
|||
|
|||
return $this->toJson($message) . "\n"; |
|||
} |
|||
|
|||
protected function formatV0(array $record) |
|||
{ |
|||
if (empty($record['datetime'])) { |
|||
$record['datetime'] = gmdate('c'); |
|||
} |
|||
$message = array( |
|||
'@timestamp' => $record['datetime'], |
|||
'@source' => $this->systemName, |
|||
'@fields' => array(), |
|||
); |
|||
if (isset($record['message'])) { |
|||
$message['@message'] = $record['message']; |
|||
} |
|||
if (isset($record['channel'])) { |
|||
$message['@tags'] = array($record['channel']); |
|||
$message['@fields']['channel'] = $record['channel']; |
|||
} |
|||
if (isset($record['level'])) { |
|||
$message['@fields']['level'] = $record['level']; |
|||
} |
|||
if ($this->applicationName) { |
|||
$message['@type'] = $this->applicationName; |
|||
} |
|||
if (isset($record['extra']['server'])) { |
|||
$message['@source_host'] = $record['extra']['server']; |
|||
} |
|||
if (isset($record['extra']['url'])) { |
|||
$message['@source_path'] = $record['extra']['url']; |
|||
} |
|||
if (!empty($record['extra'])) { |
|||
foreach ($record['extra'] as $key => $val) { |
|||
$message['@fields'][$this->extraPrefix . $key] = $val; |
|||
} |
|||
} |
|||
if (!empty($record['context'])) { |
|||
foreach ($record['context'] as $key => $val) { |
|||
$message['@fields'][$this->contextPrefix . $key] = $val; |
|||
} |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
|
|||
protected function formatV1(array $record) |
|||
{ |
|||
if (empty($record['datetime'])) { |
|||
$record['datetime'] = gmdate('c'); |
|||
} |
|||
$message = array( |
|||
'@timestamp' => $record['datetime'], |
|||
'@version' => 1, |
|||
'host' => $this->systemName, |
|||
); |
|||
if (isset($record['message'])) { |
|||
$message['message'] = $record['message']; |
|||
} |
|||
if (isset($record['channel'])) { |
|||
$message['type'] = $record['channel']; |
|||
$message['channel'] = $record['channel']; |
|||
} |
|||
if (isset($record['level_name'])) { |
|||
$message['level'] = $record['level_name']; |
|||
} |
|||
if ($this->applicationName) { |
|||
$message['type'] = $this->applicationName; |
|||
} |
|||
if (!empty($record['extra'])) { |
|||
foreach ($record['extra'] as $key => $val) { |
|||
$message[$this->extraPrefix . $key] = $val; |
|||
} |
|||
} |
|||
if (!empty($record['context'])) { |
|||
foreach ($record['context'] as $key => $val) { |
|||
$message[$this->contextPrefix . $key] = $val; |
|||
} |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Utils; |
|||
|
|||
/** |
|||
* Formats a record for use with the MongoDBHandler. |
|||
* |
|||
* @author Florian Plattner <me@florianplattner.de> |
|||
*/ |
|||
class MongoDBFormatter implements FormatterInterface |
|||
{ |
|||
private $exceptionTraceAsString; |
|||
private $maxNestingLevel; |
|||
|
|||
/** |
|||
* @param int $maxNestingLevel 0 means infinite nesting, the $record itself is level 1, $record['context'] is 2 |
|||
* @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings |
|||
*/ |
|||
public function __construct($maxNestingLevel = 3, $exceptionTraceAsString = true) |
|||
{ |
|||
$this->maxNestingLevel = max($maxNestingLevel, 0); |
|||
$this->exceptionTraceAsString = (bool) $exceptionTraceAsString; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
return $this->formatArray($record); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function formatBatch(array $records) |
|||
{ |
|||
foreach ($records as $key => $record) { |
|||
$records[$key] = $this->format($record); |
|||
} |
|||
|
|||
return $records; |
|||
} |
|||
|
|||
protected function formatArray(array $record, $nestingLevel = 0) |
|||
{ |
|||
if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) { |
|||
foreach ($record as $name => $value) { |
|||
if ($value instanceof \DateTime) { |
|||
$record[$name] = $this->formatDate($value, $nestingLevel + 1); |
|||
} elseif ($value instanceof \Exception) { |
|||
$record[$name] = $this->formatException($value, $nestingLevel + 1); |
|||
} elseif (is_array($value)) { |
|||
$record[$name] = $this->formatArray($value, $nestingLevel + 1); |
|||
} elseif (is_object($value)) { |
|||
$record[$name] = $this->formatObject($value, $nestingLevel + 1); |
|||
} |
|||
} |
|||
} else { |
|||
$record = '[...]'; |
|||
} |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
protected function formatObject($value, $nestingLevel) |
|||
{ |
|||
$objectVars = get_object_vars($value); |
|||
$objectVars['class'] = Utils::getClass($value); |
|||
|
|||
return $this->formatArray($objectVars, $nestingLevel); |
|||
} |
|||
|
|||
protected function formatException(\Exception $exception, $nestingLevel) |
|||
{ |
|||
$formattedException = array( |
|||
'class' => Utils::getClass($exception), |
|||
'message' => $exception->getMessage(), |
|||
'code' => $exception->getCode(), |
|||
'file' => $exception->getFile() . ':' . $exception->getLine(), |
|||
); |
|||
|
|||
if ($this->exceptionTraceAsString === true) { |
|||
$formattedException['trace'] = $exception->getTraceAsString(); |
|||
} else { |
|||
$formattedException['trace'] = $exception->getTrace(); |
|||
} |
|||
|
|||
return $this->formatArray($formattedException, $nestingLevel); |
|||
} |
|||
|
|||
protected function formatDate(\DateTime $value, $nestingLevel) |
|||
{ |
|||
return new \MongoDate($value->getTimestamp()); |
|||
} |
|||
} |
|||
@ -0,0 +1,314 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Exception; |
|||
use Monolog\Utils; |
|||
|
|||
/** |
|||
* Normalizes incoming records to remove objects/resources so it's easier to dump to various targets |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class NormalizerFormatter implements FormatterInterface |
|||
{ |
|||
const SIMPLE_DATE = "Y-m-d H:i:s"; |
|||
|
|||
protected $dateFormat; |
|||
|
|||
/** |
|||
* @param string $dateFormat The format of the timestamp: one supported by DateTime::format |
|||
*/ |
|||
public function __construct($dateFormat = null) |
|||
{ |
|||
$this->dateFormat = $dateFormat ?: static::SIMPLE_DATE; |
|||
if (!function_exists('json_encode')) { |
|||
throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
return $this->normalize($record); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function formatBatch(array $records) |
|||
{ |
|||
foreach ($records as $key => $record) { |
|||
$records[$key] = $this->format($record); |
|||
} |
|||
|
|||
return $records; |
|||
} |
|||
|
|||
protected function normalize($data, $depth = 0) |
|||
{ |
|||
if ($depth > 9) { |
|||
return 'Over 9 levels deep, aborting normalization'; |
|||
} |
|||
|
|||
if (null === $data || is_scalar($data)) { |
|||
if (is_float($data)) { |
|||
if (is_infinite($data)) { |
|||
return ($data > 0 ? '' : '-') . 'INF'; |
|||
} |
|||
if (is_nan($data)) { |
|||
return 'NaN'; |
|||
} |
|||
} |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
if (is_array($data)) { |
|||
$normalized = array(); |
|||
|
|||
$count = 1; |
|||
foreach ($data as $key => $value) { |
|||
if ($count++ > 1000) { |
|||
$normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization'; |
|||
break; |
|||
} |
|||
|
|||
$normalized[$key] = $this->normalize($value, $depth+1); |
|||
} |
|||
|
|||
return $normalized; |
|||
} |
|||
|
|||
if ($data instanceof \DateTime) { |
|||
return $data->format($this->dateFormat); |
|||
} |
|||
|
|||
if (is_object($data)) { |
|||
// TODO 2.0 only check for Throwable |
|||
if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) { |
|||
return $this->normalizeException($data); |
|||
} |
|||
|
|||
// non-serializable objects that implement __toString stringified |
|||
if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) { |
|||
$value = $data->__toString(); |
|||
} else { |
|||
// the rest is json-serialized in some way |
|||
$value = $this->toJson($data, true); |
|||
} |
|||
|
|||
return sprintf("[object] (%s: %s)", Utils::getClass($data), $value); |
|||
} |
|||
|
|||
if (is_resource($data)) { |
|||
return sprintf('[resource] (%s)', get_resource_type($data)); |
|||
} |
|||
|
|||
return '[unknown('.gettype($data).')]'; |
|||
} |
|||
|
|||
protected function normalizeException($e) |
|||
{ |
|||
// TODO 2.0 only check for Throwable |
|||
if (!$e instanceof Exception && !$e instanceof \Throwable) { |
|||
throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); |
|||
} |
|||
|
|||
$data = array( |
|||
'class' => Utils::getClass($e), |
|||
'message' => $e->getMessage(), |
|||
'code' => $e->getCode(), |
|||
'file' => $e->getFile().':'.$e->getLine(), |
|||
); |
|||
|
|||
if ($e instanceof \SoapFault) { |
|||
if (isset($e->faultcode)) { |
|||
$data['faultcode'] = $e->faultcode; |
|||
} |
|||
|
|||
if (isset($e->faultactor)) { |
|||
$data['faultactor'] = $e->faultactor; |
|||
} |
|||
|
|||
if (isset($e->detail)) { |
|||
$data['detail'] = $e->detail; |
|||
} |
|||
} |
|||
|
|||
$trace = $e->getTrace(); |
|||
foreach ($trace as $frame) { |
|||
if (isset($frame['file'])) { |
|||
$data['trace'][] = $frame['file'].':'.$frame['line']; |
|||
} elseif (isset($frame['function']) && $frame['function'] === '{closure}') { |
|||
// Simplify closures handling |
|||
$data['trace'][] = $frame['function']; |
|||
} else { |
|||
if (isset($frame['args'])) { |
|||
// Make sure that objects present as arguments are not serialized nicely but rather only |
|||
// as a class name to avoid any unexpected leak of sensitive information |
|||
$frame['args'] = array_map(function ($arg) { |
|||
if (is_object($arg) && !($arg instanceof \DateTime || $arg instanceof \DateTimeInterface)) { |
|||
return sprintf("[object] (%s)", Utils::getClass($arg)); |
|||
} |
|||
|
|||
return $arg; |
|||
}, $frame['args']); |
|||
} |
|||
// We should again normalize the frames, because it might contain invalid items |
|||
$data['trace'][] = $this->toJson($this->normalize($frame), true); |
|||
} |
|||
} |
|||
|
|||
if ($previous = $e->getPrevious()) { |
|||
$data['previous'] = $this->normalizeException($previous); |
|||
} |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
/** |
|||
* Return the JSON representation of a value |
|||
* |
|||
* @param mixed $data |
|||
* @param bool $ignoreErrors |
|||
* @throws \RuntimeException if encoding fails and errors are not ignored |
|||
* @return string |
|||
*/ |
|||
protected function toJson($data, $ignoreErrors = false) |
|||
{ |
|||
// suppress json_encode errors since it's twitchy with some inputs |
|||
if ($ignoreErrors) { |
|||
return @$this->jsonEncode($data); |
|||
} |
|||
|
|||
$json = $this->jsonEncode($data); |
|||
|
|||
if ($json === false) { |
|||
$json = $this->handleJsonError(json_last_error(), $data); |
|||
} |
|||
|
|||
return $json; |
|||
} |
|||
|
|||
/** |
|||
* @param mixed $data |
|||
* @return string JSON encoded data or null on failure |
|||
*/ |
|||
private function jsonEncode($data) |
|||
{ |
|||
if (version_compare(PHP_VERSION, '5.4.0', '>=')) { |
|||
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); |
|||
} |
|||
|
|||
return json_encode($data); |
|||
} |
|||
|
|||
/** |
|||
* Handle a json_encode failure. |
|||
* |
|||
* If the failure is due to invalid string encoding, try to clean the |
|||
* input and encode again. If the second encoding attempt fails, the |
|||
* inital error is not encoding related or the input can't be cleaned then |
|||
* raise a descriptive exception. |
|||
* |
|||
* @param int $code return code of json_last_error function |
|||
* @param mixed $data data that was meant to be encoded |
|||
* @throws \RuntimeException if failure can't be corrected |
|||
* @return string JSON encoded data after error correction |
|||
*/ |
|||
private function handleJsonError($code, $data) |
|||
{ |
|||
if ($code !== JSON_ERROR_UTF8) { |
|||
$this->throwEncodeError($code, $data); |
|||
} |
|||
|
|||
if (is_string($data)) { |
|||
$this->detectAndCleanUtf8($data); |
|||
} elseif (is_array($data)) { |
|||
array_walk_recursive($data, array($this, 'detectAndCleanUtf8')); |
|||
} else { |
|||
$this->throwEncodeError($code, $data); |
|||
} |
|||
|
|||
$json = $this->jsonEncode($data); |
|||
|
|||
if ($json === false) { |
|||
$this->throwEncodeError(json_last_error(), $data); |
|||
} |
|||
|
|||
return $json; |
|||
} |
|||
|
|||
/** |
|||
* Throws an exception according to a given code with a customized message |
|||
* |
|||
* @param int $code return code of json_last_error function |
|||
* @param mixed $data data that was meant to be encoded |
|||
* @throws \RuntimeException |
|||
*/ |
|||
private function throwEncodeError($code, $data) |
|||
{ |
|||
switch ($code) { |
|||
case JSON_ERROR_DEPTH: |
|||
$msg = 'Maximum stack depth exceeded'; |
|||
break; |
|||
case JSON_ERROR_STATE_MISMATCH: |
|||
$msg = 'Underflow or the modes mismatch'; |
|||
break; |
|||
case JSON_ERROR_CTRL_CHAR: |
|||
$msg = 'Unexpected control character found'; |
|||
break; |
|||
case JSON_ERROR_UTF8: |
|||
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; |
|||
break; |
|||
default: |
|||
$msg = 'Unknown error'; |
|||
} |
|||
|
|||
throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); |
|||
} |
|||
|
|||
/** |
|||
* Detect invalid UTF-8 string characters and convert to valid UTF-8. |
|||
* |
|||
* Valid UTF-8 input will be left unmodified, but strings containing |
|||
* invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed |
|||
* original encoding of ISO-8859-15. This conversion may result in |
|||
* incorrect output if the actual encoding was not ISO-8859-15, but it |
|||
* will be clean UTF-8 output and will not rely on expensive and fragile |
|||
* detection algorithms. |
|||
* |
|||
* Function converts the input in place in the passed variable so that it |
|||
* can be used as a callback for array_walk_recursive. |
|||
* |
|||
* @param mixed &$data Input to check and convert if needed |
|||
* @private |
|||
*/ |
|||
public function detectAndCleanUtf8(&$data) |
|||
{ |
|||
if (is_string($data) && !preg_match('//u', $data)) { |
|||
$data = preg_replace_callback( |
|||
'/[\x80-\xFF]+/', |
|||
function ($m) { return utf8_encode($m[0]); }, |
|||
$data |
|||
); |
|||
$data = str_replace( |
|||
array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'), |
|||
array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'), |
|||
$data |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
/** |
|||
* Formats data into an associative array of scalar values. |
|||
* Objects and arrays will be JSON encoded. |
|||
* |
|||
* @author Andrew Lawson <adlawson@gmail.com> |
|||
*/ |
|||
class ScalarFormatter extends NormalizerFormatter |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
foreach ($record as $key => $value) { |
|||
$record[$key] = $this->normalizeValue($value); |
|||
} |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
/** |
|||
* @param mixed $value |
|||
* @return mixed |
|||
*/ |
|||
protected function normalizeValue($value) |
|||
{ |
|||
$normalized = $this->normalize($value); |
|||
|
|||
if (is_array($normalized) || is_object($normalized)) { |
|||
return $this->toJson($normalized, true); |
|||
} |
|||
|
|||
return $normalized; |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Formatter; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Serializes a log message according to Wildfire's header requirements |
|||
* |
|||
* @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com> |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
* @author Kirill chEbba Chebunin <iam@chebba.org> |
|||
*/ |
|||
class WildfireFormatter extends NormalizerFormatter |
|||
{ |
|||
const TABLE = 'table'; |
|||
|
|||
/** |
|||
* Translates Monolog log levels to Wildfire levels. |
|||
*/ |
|||
private $logLevels = array( |
|||
Logger::DEBUG => 'LOG', |
|||
Logger::INFO => 'INFO', |
|||
Logger::NOTICE => 'INFO', |
|||
Logger::WARNING => 'WARN', |
|||
Logger::ERROR => 'ERROR', |
|||
Logger::CRITICAL => 'ERROR', |
|||
Logger::ALERT => 'ERROR', |
|||
Logger::EMERGENCY => 'ERROR', |
|||
); |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function format(array $record) |
|||
{ |
|||
// Retrieve the line and file if set and remove them from the formatted extra |
|||
$file = $line = ''; |
|||
if (isset($record['extra']['file'])) { |
|||
$file = $record['extra']['file']; |
|||
unset($record['extra']['file']); |
|||
} |
|||
if (isset($record['extra']['line'])) { |
|||
$line = $record['extra']['line']; |
|||
unset($record['extra']['line']); |
|||
} |
|||
|
|||
$record = $this->normalize($record); |
|||
$message = array('message' => $record['message']); |
|||
$handleError = false; |
|||
if ($record['context']) { |
|||
$message['context'] = $record['context']; |
|||
$handleError = true; |
|||
} |
|||
if ($record['extra']) { |
|||
$message['extra'] = $record['extra']; |
|||
$handleError = true; |
|||
} |
|||
if (count($message) === 1) { |
|||
$message = reset($message); |
|||
} |
|||
|
|||
if (isset($record['context'][self::TABLE])) { |
|||
$type = 'TABLE'; |
|||
$label = $record['channel'] .': '. $record['message']; |
|||
$message = $record['context'][self::TABLE]; |
|||
} else { |
|||
$type = $this->logLevels[$record['level']]; |
|||
$label = $record['channel']; |
|||
} |
|||
|
|||
// Create JSON object describing the appearance of the message in the console |
|||
$json = $this->toJson(array( |
|||
array( |
|||
'Type' => $type, |
|||
'File' => $file, |
|||
'Line' => $line, |
|||
'Label' => $label, |
|||
), |
|||
$message, |
|||
), $handleError); |
|||
|
|||
// The message itself is a serialization of the above JSON object + it's length |
|||
return sprintf( |
|||
'%s|%s|', |
|||
strlen($json), |
|||
$json |
|||
); |
|||
} |
|||
|
|||
public function formatBatch(array $records) |
|||
{ |
|||
throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter'); |
|||
} |
|||
|
|||
protected function normalize($data, $depth = 0) |
|||
{ |
|||
if (is_object($data) && !$data instanceof \DateTime) { |
|||
return $data; |
|||
} |
|||
|
|||
return parent::normalize($data, $depth); |
|||
} |
|||
} |
|||
@ -0,0 +1,196 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Logger; |
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Base Handler class providing the Handler structure |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
abstract class AbstractHandler implements HandlerInterface, ResettableInterface |
|||
{ |
|||
protected $level = Logger::DEBUG; |
|||
protected $bubble = true; |
|||
|
|||
/** |
|||
* @var FormatterInterface |
|||
*/ |
|||
protected $formatter; |
|||
protected $processors = array(); |
|||
|
|||
/** |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
$this->setLevel($level); |
|||
$this->bubble = $bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHandling(array $record) |
|||
{ |
|||
return $record['level'] >= $this->level; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
foreach ($records as $record) { |
|||
$this->handle($record); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Closes the handler. |
|||
* |
|||
* This will be called automatically when the object is destroyed |
|||
*/ |
|||
public function close() |
|||
{ |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function pushProcessor($callback) |
|||
{ |
|||
if (!is_callable($callback)) { |
|||
throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given'); |
|||
} |
|||
array_unshift($this->processors, $callback); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function popProcessor() |
|||
{ |
|||
if (!$this->processors) { |
|||
throw new \LogicException('You tried to pop from an empty processor stack.'); |
|||
} |
|||
|
|||
return array_shift($this->processors); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
$this->formatter = $formatter; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getFormatter() |
|||
{ |
|||
if (!$this->formatter) { |
|||
$this->formatter = $this->getDefaultFormatter(); |
|||
} |
|||
|
|||
return $this->formatter; |
|||
} |
|||
|
|||
/** |
|||
* Sets minimum logging level at which this handler will be triggered. |
|||
* |
|||
* @param int|string $level Level or level name |
|||
* @return self |
|||
*/ |
|||
public function setLevel($level) |
|||
{ |
|||
$this->level = Logger::toMonologLevel($level); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Gets minimum logging level at which this handler will be triggered. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getLevel() |
|||
{ |
|||
return $this->level; |
|||
} |
|||
|
|||
/** |
|||
* Sets the bubbling behavior. |
|||
* |
|||
* @param bool $bubble true means that this handler allows bubbling. |
|||
* false means that bubbling is not permitted. |
|||
* @return self |
|||
*/ |
|||
public function setBubble($bubble) |
|||
{ |
|||
$this->bubble = $bubble; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Gets the bubbling behavior. |
|||
* |
|||
* @return bool true means that this handler allows bubbling. |
|||
* false means that bubbling is not permitted. |
|||
*/ |
|||
public function getBubble() |
|||
{ |
|||
return $this->bubble; |
|||
} |
|||
|
|||
public function __destruct() |
|||
{ |
|||
try { |
|||
$this->close(); |
|||
} catch (\Exception $e) { |
|||
// do nothing |
|||
} catch (\Throwable $e) { |
|||
// do nothing |
|||
} |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
foreach ($this->processors as $processor) { |
|||
if ($processor instanceof ResettableInterface) { |
|||
$processor->reset(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the default formatter. |
|||
* |
|||
* @return FormatterInterface |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Base Handler class providing the Handler structure |
|||
* |
|||
* Classes extending it should (in most cases) only implement write($record) |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
*/ |
|||
abstract class AbstractProcessingHandler extends AbstractHandler |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if (!$this->isHandling($record)) { |
|||
return false; |
|||
} |
|||
|
|||
$record = $this->processRecord($record); |
|||
|
|||
$record['formatted'] = $this->getFormatter()->format($record); |
|||
|
|||
$this->write($record); |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* Writes the record down to the log of the implementing handler |
|||
* |
|||
* @param array $record |
|||
* @return void |
|||
*/ |
|||
abstract protected function write(array $record); |
|||
|
|||
/** |
|||
* Processes a record. |
|||
* |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
protected function processRecord(array $record) |
|||
{ |
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\LineFormatter; |
|||
|
|||
/** |
|||
* Common syslog functionality |
|||
*/ |
|||
abstract class AbstractSyslogHandler extends AbstractProcessingHandler |
|||
{ |
|||
protected $facility; |
|||
|
|||
/** |
|||
* Translates Monolog log levels to syslog log priorities. |
|||
*/ |
|||
protected $logLevels = array( |
|||
Logger::DEBUG => LOG_DEBUG, |
|||
Logger::INFO => LOG_INFO, |
|||
Logger::NOTICE => LOG_NOTICE, |
|||
Logger::WARNING => LOG_WARNING, |
|||
Logger::ERROR => LOG_ERR, |
|||
Logger::CRITICAL => LOG_CRIT, |
|||
Logger::ALERT => LOG_ALERT, |
|||
Logger::EMERGENCY => LOG_EMERG, |
|||
); |
|||
|
|||
/** |
|||
* List of valid log facility names. |
|||
*/ |
|||
protected $facilities = array( |
|||
'auth' => LOG_AUTH, |
|||
'authpriv' => LOG_AUTHPRIV, |
|||
'cron' => LOG_CRON, |
|||
'daemon' => LOG_DAEMON, |
|||
'kern' => LOG_KERN, |
|||
'lpr' => LOG_LPR, |
|||
'mail' => LOG_MAIL, |
|||
'news' => LOG_NEWS, |
|||
'syslog' => LOG_SYSLOG, |
|||
'user' => LOG_USER, |
|||
'uucp' => LOG_UUCP, |
|||
); |
|||
|
|||
/** |
|||
* @param mixed $facility |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($facility = LOG_USER, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
if (!defined('PHP_WINDOWS_VERSION_BUILD')) { |
|||
$this->facilities['local0'] = LOG_LOCAL0; |
|||
$this->facilities['local1'] = LOG_LOCAL1; |
|||
$this->facilities['local2'] = LOG_LOCAL2; |
|||
$this->facilities['local3'] = LOG_LOCAL3; |
|||
$this->facilities['local4'] = LOG_LOCAL4; |
|||
$this->facilities['local5'] = LOG_LOCAL5; |
|||
$this->facilities['local6'] = LOG_LOCAL6; |
|||
$this->facilities['local7'] = LOG_LOCAL7; |
|||
} else { |
|||
$this->facilities['local0'] = 128; // LOG_LOCAL0 |
|||
$this->facilities['local1'] = 136; // LOG_LOCAL1 |
|||
$this->facilities['local2'] = 144; // LOG_LOCAL2 |
|||
$this->facilities['local3'] = 152; // LOG_LOCAL3 |
|||
$this->facilities['local4'] = 160; // LOG_LOCAL4 |
|||
$this->facilities['local5'] = 168; // LOG_LOCAL5 |
|||
$this->facilities['local6'] = 176; // LOG_LOCAL6 |
|||
$this->facilities['local7'] = 184; // LOG_LOCAL7 |
|||
} |
|||
|
|||
// convert textual description of facility to syslog constant |
|||
if (array_key_exists(strtolower($facility), $this->facilities)) { |
|||
$facility = $this->facilities[strtolower($facility)]; |
|||
} elseif (!in_array($facility, array_values($this->facilities), true)) { |
|||
throw new \UnexpectedValueException('Unknown facility value "'.$facility.'" given'); |
|||
} |
|||
|
|||
$this->facility = $facility; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%'); |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\JsonFormatter; |
|||
use PhpAmqpLib\Message\AMQPMessage; |
|||
use PhpAmqpLib\Channel\AMQPChannel; |
|||
use AMQPExchange; |
|||
|
|||
class AmqpHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* @var AMQPExchange|AMQPChannel $exchange |
|||
*/ |
|||
protected $exchange; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $exchangeName; |
|||
|
|||
/** |
|||
* @param AMQPExchange|AMQPChannel $exchange AMQPExchange (php AMQP ext) or PHP AMQP lib channel, ready for use |
|||
* @param string $exchangeName |
|||
* @param int $level |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($exchange, $exchangeName = 'log', $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if ($exchange instanceof AMQPExchange) { |
|||
$exchange->setName($exchangeName); |
|||
} elseif ($exchange instanceof AMQPChannel) { |
|||
$this->exchangeName = $exchangeName; |
|||
} else { |
|||
throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required'); |
|||
} |
|||
$this->exchange = $exchange; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$data = $record["formatted"]; |
|||
$routingKey = $this->getRoutingKey($record); |
|||
|
|||
if ($this->exchange instanceof AMQPExchange) { |
|||
$this->exchange->publish( |
|||
$data, |
|||
$routingKey, |
|||
0, |
|||
array( |
|||
'delivery_mode' => 2, |
|||
'content_type' => 'application/json', |
|||
) |
|||
); |
|||
} else { |
|||
$this->exchange->basic_publish( |
|||
$this->createAmqpMessage($data), |
|||
$this->exchangeName, |
|||
$routingKey |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
if ($this->exchange instanceof AMQPExchange) { |
|||
parent::handleBatch($records); |
|||
|
|||
return; |
|||
} |
|||
|
|||
foreach ($records as $record) { |
|||
if (!$this->isHandling($record)) { |
|||
continue; |
|||
} |
|||
|
|||
$record = $this->processRecord($record); |
|||
$data = $this->getFormatter()->format($record); |
|||
|
|||
$this->exchange->batch_basic_publish( |
|||
$this->createAmqpMessage($data), |
|||
$this->exchangeName, |
|||
$this->getRoutingKey($record) |
|||
); |
|||
} |
|||
|
|||
$this->exchange->publish_batch(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the routing key for the AMQP exchange |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function getRoutingKey(array $record) |
|||
{ |
|||
$routingKey = sprintf( |
|||
'%s.%s', |
|||
// TODO 2.0 remove substr call |
|||
substr($record['level_name'], 0, 4), |
|||
$record['channel'] |
|||
); |
|||
|
|||
return strtolower($routingKey); |
|||
} |
|||
|
|||
/** |
|||
* @param string $data |
|||
* @return AMQPMessage |
|||
*/ |
|||
private function createAmqpMessage($data) |
|||
{ |
|||
return new AMQPMessage( |
|||
(string) $data, |
|||
array( |
|||
'delivery_mode' => 2, |
|||
'content_type' => 'application/json', |
|||
) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,240 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\LineFormatter; |
|||
|
|||
/** |
|||
* Handler sending logs to browser's javascript console with no browser extension required |
|||
* |
|||
* @author Olivier Poitrey <rs@dailymotion.com> |
|||
*/ |
|||
class BrowserConsoleHandler extends AbstractProcessingHandler |
|||
{ |
|||
protected static $initialized = false; |
|||
protected static $records = array(); |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
* |
|||
* Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format. |
|||
* |
|||
* Example of formatted string: |
|||
* |
|||
* You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
// Accumulate records |
|||
static::$records[] = $record; |
|||
|
|||
// Register shutdown handler if not already done |
|||
if (!static::$initialized) { |
|||
static::$initialized = true; |
|||
$this->registerShutdownFunction(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Convert records to javascript console commands and send it to the browser. |
|||
* This method is automatically called on PHP shutdown if output is HTML or Javascript. |
|||
*/ |
|||
public static function send() |
|||
{ |
|||
$format = static::getResponseFormat(); |
|||
if ($format === 'unknown') { |
|||
return; |
|||
} |
|||
|
|||
if (count(static::$records)) { |
|||
if ($format === 'html') { |
|||
static::writeOutput('<script>' . static::generateScript() . '</script>'); |
|||
} elseif ($format === 'js') { |
|||
static::writeOutput(static::generateScript()); |
|||
} |
|||
static::resetStatic(); |
|||
} |
|||
} |
|||
|
|||
public function close() |
|||
{ |
|||
self::resetStatic(); |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
self::resetStatic(); |
|||
} |
|||
|
|||
/** |
|||
* Forget all logged records |
|||
*/ |
|||
public static function resetStatic() |
|||
{ |
|||
static::$records = array(); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper for register_shutdown_function to allow overriding |
|||
*/ |
|||
protected function registerShutdownFunction() |
|||
{ |
|||
if (PHP_SAPI !== 'cli') { |
|||
register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send')); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Wrapper for echo to allow overriding |
|||
* |
|||
* @param string $str |
|||
*/ |
|||
protected static function writeOutput($str) |
|||
{ |
|||
echo $str; |
|||
} |
|||
|
|||
/** |
|||
* Checks the format of the response |
|||
* |
|||
* If Content-Type is set to application/javascript or text/javascript -> js |
|||
* If Content-Type is set to text/html, or is unset -> html |
|||
* If Content-Type is anything else -> unknown |
|||
* |
|||
* @return string One of 'js', 'html' or 'unknown' |
|||
*/ |
|||
protected static function getResponseFormat() |
|||
{ |
|||
// Check content type |
|||
foreach (headers_list() as $header) { |
|||
if (stripos($header, 'content-type:') === 0) { |
|||
// This handler only works with HTML and javascript outputs |
|||
// text/javascript is obsolete in favour of application/javascript, but still used |
|||
if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) { |
|||
return 'js'; |
|||
} |
|||
if (stripos($header, 'text/html') === false) { |
|||
return 'unknown'; |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return 'html'; |
|||
} |
|||
|
|||
private static function generateScript() |
|||
{ |
|||
$script = array(); |
|||
foreach (static::$records as $record) { |
|||
$context = static::dump('Context', $record['context']); |
|||
$extra = static::dump('Extra', $record['extra']); |
|||
|
|||
if (empty($context) && empty($extra)) { |
|||
$script[] = static::call_array('log', static::handleStyles($record['formatted'])); |
|||
} else { |
|||
$script = array_merge($script, |
|||
array(static::call_array('groupCollapsed', static::handleStyles($record['formatted']))), |
|||
$context, |
|||
$extra, |
|||
array(static::call('groupEnd')) |
|||
); |
|||
} |
|||
} |
|||
|
|||
return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);"; |
|||
} |
|||
|
|||
private static function handleStyles($formatted) |
|||
{ |
|||
$args = array(static::quote('font-weight: normal')); |
|||
$format = '%c' . $formatted; |
|||
preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); |
|||
|
|||
foreach (array_reverse($matches) as $match) { |
|||
$args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); |
|||
$args[] = '"font-weight: normal"'; |
|||
|
|||
$pos = $match[0][1]; |
|||
$format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0])); |
|||
} |
|||
|
|||
array_unshift($args, static::quote($format)); |
|||
|
|||
return $args; |
|||
} |
|||
|
|||
private static function handleCustomStyles($style, $string) |
|||
{ |
|||
static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey'); |
|||
static $labels = array(); |
|||
|
|||
return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) { |
|||
if (trim($m[1]) === 'autolabel') { |
|||
// Format the string as a label with consistent auto assigned background color |
|||
if (!isset($labels[$string])) { |
|||
$labels[$string] = $colors[count($labels) % count($colors)]; |
|||
} |
|||
$color = $labels[$string]; |
|||
|
|||
return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px"; |
|||
} |
|||
|
|||
return $m[1]; |
|||
}, $style); |
|||
} |
|||
|
|||
private static function dump($title, array $dict) |
|||
{ |
|||
$script = array(); |
|||
$dict = array_filter($dict); |
|||
if (empty($dict)) { |
|||
return $script; |
|||
} |
|||
$script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title)); |
|||
foreach ($dict as $key => $value) { |
|||
$value = json_encode($value); |
|||
if (empty($value)) { |
|||
$value = static::quote(''); |
|||
} |
|||
$script[] = static::call('log', static::quote('%s: %o'), static::quote($key), $value); |
|||
} |
|||
|
|||
return $script; |
|||
} |
|||
|
|||
private static function quote($arg) |
|||
{ |
|||
return '"' . addcslashes($arg, "\"\n\\") . '"'; |
|||
} |
|||
|
|||
private static function call() |
|||
{ |
|||
$args = func_get_args(); |
|||
$method = array_shift($args); |
|||
|
|||
return static::call_array($method, $args); |
|||
} |
|||
|
|||
private static function call_array($method, array $args) |
|||
{ |
|||
return 'c.' . $method . '(' . implode(', ', $args) . ');'; |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Buffers all records until closing the handler and then pass them as batch. |
|||
* |
|||
* This is useful for a MailHandler to send only one mail per request instead of |
|||
* sending one per log message. |
|||
* |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
*/ |
|||
class BufferHandler extends AbstractHandler |
|||
{ |
|||
protected $handler; |
|||
protected $bufferSize = 0; |
|||
protected $bufferLimit; |
|||
protected $flushOnOverflow; |
|||
protected $buffer = array(); |
|||
protected $initialized = false; |
|||
|
|||
/** |
|||
* @param HandlerInterface $handler Handler. |
|||
* @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded |
|||
*/ |
|||
public function __construct(HandlerInterface $handler, $bufferLimit = 0, $level = Logger::DEBUG, $bubble = true, $flushOnOverflow = false) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
$this->handler = $handler; |
|||
$this->bufferLimit = (int) $bufferLimit; |
|||
$this->flushOnOverflow = $flushOnOverflow; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if ($record['level'] < $this->level) { |
|||
return false; |
|||
} |
|||
|
|||
if (!$this->initialized) { |
|||
// __destructor() doesn't get called on Fatal errors |
|||
register_shutdown_function(array($this, 'close')); |
|||
$this->initialized = true; |
|||
} |
|||
|
|||
if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) { |
|||
if ($this->flushOnOverflow) { |
|||
$this->flush(); |
|||
} else { |
|||
array_shift($this->buffer); |
|||
$this->bufferSize--; |
|||
} |
|||
} |
|||
|
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
$this->buffer[] = $record; |
|||
$this->bufferSize++; |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
public function flush() |
|||
{ |
|||
if ($this->bufferSize === 0) { |
|||
return; |
|||
} |
|||
|
|||
$this->handler->handleBatch($this->buffer); |
|||
$this->clear(); |
|||
} |
|||
|
|||
public function __destruct() |
|||
{ |
|||
// suppress the parent behavior since we already have register_shutdown_function() |
|||
// to call close(), and the reference contained there will prevent this from being |
|||
// GC'd until the end of the request |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$this->flush(); |
|||
} |
|||
|
|||
/** |
|||
* Clears the buffer without flushing any messages down to the wrapped handler. |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$this->bufferSize = 0; |
|||
$this->buffer = array(); |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
$this->flush(); |
|||
|
|||
parent::reset(); |
|||
|
|||
if ($this->handler instanceof ResettableInterface) { |
|||
$this->handler->reset(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\ChromePHPFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) |
|||
* |
|||
* This also works out of the box with Firefox 43+ |
|||
* |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
*/ |
|||
class ChromePHPHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Version of the extension |
|||
*/ |
|||
const VERSION = '4.0'; |
|||
|
|||
/** |
|||
* Header name |
|||
*/ |
|||
const HEADER_NAME = 'X-ChromeLogger-Data'; |
|||
|
|||
/** |
|||
* Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+) |
|||
*/ |
|||
const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}'; |
|||
|
|||
protected static $initialized = false; |
|||
|
|||
/** |
|||
* Tracks whether we sent too much data |
|||
* |
|||
* Chrome limits the headers to 256KB, so when we sent 240KB we stop sending |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected static $overflowed = false; |
|||
|
|||
protected static $json = array( |
|||
'version' => self::VERSION, |
|||
'columns' => array('label', 'log', 'backtrace', 'type'), |
|||
'rows' => array(), |
|||
); |
|||
|
|||
protected static $sendHeaders = true; |
|||
|
|||
/** |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
if (!function_exists('json_encode')) { |
|||
throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
$messages = array(); |
|||
|
|||
foreach ($records as $record) { |
|||
if ($record['level'] < $this->level) { |
|||
continue; |
|||
} |
|||
$messages[] = $this->processRecord($record); |
|||
} |
|||
|
|||
if (!empty($messages)) { |
|||
$messages = $this->getFormatter()->formatBatch($messages); |
|||
self::$json['rows'] = array_merge(self::$json['rows'], $messages); |
|||
$this->send(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new ChromePHPFormatter(); |
|||
} |
|||
|
|||
/** |
|||
* Creates & sends header for a record |
|||
* |
|||
* @see sendHeader() |
|||
* @see send() |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
self::$json['rows'][] = $record['formatted']; |
|||
|
|||
$this->send(); |
|||
} |
|||
|
|||
/** |
|||
* Sends the log header |
|||
* |
|||
* @see sendHeader() |
|||
*/ |
|||
protected function send() |
|||
{ |
|||
if (self::$overflowed || !self::$sendHeaders) { |
|||
return; |
|||
} |
|||
|
|||
if (!self::$initialized) { |
|||
self::$initialized = true; |
|||
|
|||
self::$sendHeaders = $this->headersAccepted(); |
|||
if (!self::$sendHeaders) { |
|||
return; |
|||
} |
|||
|
|||
self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; |
|||
} |
|||
|
|||
$json = @json_encode(self::$json); |
|||
$data = base64_encode(utf8_encode($json)); |
|||
if (strlen($data) > 240 * 1024) { |
|||
self::$overflowed = true; |
|||
|
|||
$record = array( |
|||
'message' => 'Incomplete logs, chrome header size limit reached', |
|||
'context' => array(), |
|||
'level' => Logger::WARNING, |
|||
'level_name' => Logger::getLevelName(Logger::WARNING), |
|||
'channel' => 'monolog', |
|||
'datetime' => new \DateTime(), |
|||
'extra' => array(), |
|||
); |
|||
self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record); |
|||
$json = @json_encode(self::$json); |
|||
$data = base64_encode(utf8_encode($json)); |
|||
} |
|||
|
|||
if (trim($data) !== '') { |
|||
$this->sendHeader(self::HEADER_NAME, $data); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send header string to the client |
|||
* |
|||
* @param string $header |
|||
* @param string $content |
|||
*/ |
|||
protected function sendHeader($header, $content) |
|||
{ |
|||
if (!headers_sent() && self::$sendHeaders) { |
|||
header(sprintf('%s: %s', $header, $content)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Verifies if the headers are accepted by the current user agent |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function headersAccepted() |
|||
{ |
|||
if (empty($_SERVER['HTTP_USER_AGENT'])) { |
|||
return false; |
|||
} |
|||
|
|||
return preg_match(self::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']); |
|||
} |
|||
|
|||
/** |
|||
* BC getter for the sendHeaders property that has been made static |
|||
*/ |
|||
public function __get($property) |
|||
{ |
|||
if ('sendHeaders' !== $property) { |
|||
throw new \InvalidArgumentException('Undefined property '.$property); |
|||
} |
|||
|
|||
return static::$sendHeaders; |
|||
} |
|||
|
|||
/** |
|||
* BC setter for the sendHeaders property that has been made static |
|||
*/ |
|||
public function __set($property, $value) |
|||
{ |
|||
if ('sendHeaders' !== $property) { |
|||
throw new \InvalidArgumentException('Undefined property '.$property); |
|||
} |
|||
|
|||
static::$sendHeaders = $value; |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\JsonFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* CouchDB handler |
|||
* |
|||
* @author Markus Bachmann <markus.bachmann@bachi.biz> |
|||
*/ |
|||
class CouchDBHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $options; |
|||
|
|||
public function __construct(array $options = array(), $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
$this->options = array_merge(array( |
|||
'host' => 'localhost', |
|||
'port' => 5984, |
|||
'dbname' => 'logger', |
|||
'username' => null, |
|||
'password' => null, |
|||
), $options); |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$basicAuth = null; |
|||
if ($this->options['username']) { |
|||
$basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']); |
|||
} |
|||
|
|||
$url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname']; |
|||
$context = stream_context_create(array( |
|||
'http' => array( |
|||
'method' => 'POST', |
|||
'content' => $record['formatted'], |
|||
'ignore_errors' => true, |
|||
'max_redirects' => 0, |
|||
'header' => 'Content-type: application/json', |
|||
), |
|||
)); |
|||
|
|||
if (false === @file_get_contents($url, null, $context)) { |
|||
throw new \RuntimeException(sprintf('Could not connect to %s', $url)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Logs to Cube. |
|||
* |
|||
* @link http://square.github.com/cube/ |
|||
* @author Wan Chen <kami@kamisama.me> |
|||
*/ |
|||
class CubeHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $udpConnection; |
|||
private $httpConnection; |
|||
private $scheme; |
|||
private $host; |
|||
private $port; |
|||
private $acceptedSchemes = array('http', 'udp'); |
|||
|
|||
/** |
|||
* Create a Cube handler |
|||
* |
|||
* @throws \UnexpectedValueException when given url is not a valid url. |
|||
* A valid url must consist of three parts : protocol://host:port |
|||
* Only valid protocols used by Cube are http and udp |
|||
*/ |
|||
public function __construct($url, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
$urlInfo = parse_url($url); |
|||
|
|||
if (!isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) { |
|||
throw new \UnexpectedValueException('URL "'.$url.'" is not valid'); |
|||
} |
|||
|
|||
if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { |
|||
throw new \UnexpectedValueException( |
|||
'Invalid protocol (' . $urlInfo['scheme'] . ').' |
|||
. ' Valid options are ' . implode(', ', $this->acceptedSchemes)); |
|||
} |
|||
|
|||
$this->scheme = $urlInfo['scheme']; |
|||
$this->host = $urlInfo['host']; |
|||
$this->port = $urlInfo['port']; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* Establish a connection to an UDP socket |
|||
* |
|||
* @throws \LogicException when unable to connect to the socket |
|||
* @throws MissingExtensionException when there is no socket extension |
|||
*/ |
|||
protected function connectUdp() |
|||
{ |
|||
if (!extension_loaded('sockets')) { |
|||
throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler'); |
|||
} |
|||
|
|||
$this->udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0); |
|||
if (!$this->udpConnection) { |
|||
throw new \LogicException('Unable to create a socket'); |
|||
} |
|||
|
|||
if (!socket_connect($this->udpConnection, $this->host, $this->port)) { |
|||
throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Establish a connection to a http server |
|||
* @throws \LogicException when no curl extension |
|||
*/ |
|||
protected function connectHttp() |
|||
{ |
|||
if (!extension_loaded('curl')) { |
|||
throw new \LogicException('The curl extension is needed to use http URLs with the CubeHandler'); |
|||
} |
|||
|
|||
$this->httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put'); |
|||
|
|||
if (!$this->httpConnection) { |
|||
throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port); |
|||
} |
|||
|
|||
curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST"); |
|||
curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$date = $record['datetime']; |
|||
|
|||
$data = array('time' => $date->format('Y-m-d\TH:i:s.uO')); |
|||
unset($record['datetime']); |
|||
|
|||
if (isset($record['context']['type'])) { |
|||
$data['type'] = $record['context']['type']; |
|||
unset($record['context']['type']); |
|||
} else { |
|||
$data['type'] = $record['channel']; |
|||
} |
|||
|
|||
$data['data'] = $record['context']; |
|||
$data['data']['level'] = $record['level']; |
|||
|
|||
if ($this->scheme === 'http') { |
|||
$this->writeHttp(json_encode($data)); |
|||
} else { |
|||
$this->writeUdp(json_encode($data)); |
|||
} |
|||
} |
|||
|
|||
private function writeUdp($data) |
|||
{ |
|||
if (!$this->udpConnection) { |
|||
$this->connectUdp(); |
|||
} |
|||
|
|||
socket_send($this->udpConnection, $data, strlen($data), 0); |
|||
} |
|||
|
|||
private function writeHttp($data) |
|||
{ |
|||
if (!$this->httpConnection) { |
|||
$this->connectHttp(); |
|||
} |
|||
|
|||
curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']'); |
|||
curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, array( |
|||
'Content-Type: application/json', |
|||
'Content-Length: ' . strlen('['.$data.']'), |
|||
)); |
|||
|
|||
Curl\Util::execute($this->httpConnection, 5, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\Curl; |
|||
|
|||
class Util |
|||
{ |
|||
private static $retriableErrorCodes = array( |
|||
CURLE_COULDNT_RESOLVE_HOST, |
|||
CURLE_COULDNT_CONNECT, |
|||
CURLE_HTTP_NOT_FOUND, |
|||
CURLE_READ_ERROR, |
|||
CURLE_OPERATION_TIMEOUTED, |
|||
CURLE_HTTP_POST_ERROR, |
|||
CURLE_SSL_CONNECT_ERROR, |
|||
); |
|||
|
|||
/** |
|||
* Executes a CURL request with optional retries and exception on failure |
|||
* |
|||
* @param resource $ch curl handler |
|||
* @throws \RuntimeException |
|||
*/ |
|||
public static function execute($ch, $retries = 5, $closeAfterDone = true) |
|||
{ |
|||
while ($retries--) { |
|||
if (curl_exec($ch) === false) { |
|||
$curlErrno = curl_errno($ch); |
|||
|
|||
if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) { |
|||
$curlError = curl_error($ch); |
|||
|
|||
if ($closeAfterDone) { |
|||
curl_close($ch); |
|||
} |
|||
|
|||
throw new \RuntimeException(sprintf('Curl error (code %s): %s', $curlErrno, $curlError)); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
|
|||
if ($closeAfterDone) { |
|||
curl_close($ch); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,169 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Simple handler wrapper that deduplicates log records across multiple requests |
|||
* |
|||
* It also includes the BufferHandler functionality and will buffer |
|||
* all messages until the end of the request or flush() is called. |
|||
* |
|||
* This works by storing all log records' messages above $deduplicationLevel |
|||
* to the file specified by $deduplicationStore. When further logs come in at the end of the |
|||
* request (or when flush() is called), all those above $deduplicationLevel are checked |
|||
* against the existing stored logs. If they match and the timestamps in the stored log is |
|||
* not older than $time seconds, the new log record is discarded. If no log record is new, the |
|||
* whole data set is discarded. |
|||
* |
|||
* This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers |
|||
* that send messages to people, to avoid spamming with the same message over and over in case of |
|||
* a major component failure like a database server being down which makes all requests fail in the |
|||
* same way. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class DeduplicationHandler extends BufferHandler |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $deduplicationStore; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $deduplicationLevel; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $time; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
private $gc = false; |
|||
|
|||
/** |
|||
* @param HandlerInterface $handler Handler. |
|||
* @param string $deduplicationStore The file/path where the deduplication log should be kept |
|||
* @param int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes |
|||
* @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true) |
|||
{ |
|||
parent::__construct($handler, 0, Logger::DEBUG, $bubble, false); |
|||
|
|||
$this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore; |
|||
$this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel); |
|||
$this->time = $time; |
|||
} |
|||
|
|||
public function flush() |
|||
{ |
|||
if ($this->bufferSize === 0) { |
|||
return; |
|||
} |
|||
|
|||
$passthru = null; |
|||
|
|||
foreach ($this->buffer as $record) { |
|||
if ($record['level'] >= $this->deduplicationLevel) { |
|||
|
|||
$passthru = $passthru || !$this->isDuplicate($record); |
|||
if ($passthru) { |
|||
$this->appendRecord($record); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// default of null is valid as well as if no record matches duplicationLevel we just pass through |
|||
if ($passthru === true || $passthru === null) { |
|||
$this->handler->handleBatch($this->buffer); |
|||
} |
|||
|
|||
$this->clear(); |
|||
|
|||
if ($this->gc) { |
|||
$this->collectLogs(); |
|||
} |
|||
} |
|||
|
|||
private function isDuplicate(array $record) |
|||
{ |
|||
if (!file_exists($this->deduplicationStore)) { |
|||
return false; |
|||
} |
|||
|
|||
$store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); |
|||
if (!is_array($store)) { |
|||
return false; |
|||
} |
|||
|
|||
$yesterday = time() - 86400; |
|||
$timestampValidity = $record['datetime']->getTimestamp() - $this->time; |
|||
$expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']); |
|||
|
|||
for ($i = count($store) - 1; $i >= 0; $i--) { |
|||
list($timestamp, $level, $message) = explode(':', $store[$i], 3); |
|||
|
|||
if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) { |
|||
return true; |
|||
} |
|||
|
|||
if ($timestamp < $yesterday) { |
|||
$this->gc = true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private function collectLogs() |
|||
{ |
|||
if (!file_exists($this->deduplicationStore)) { |
|||
return false; |
|||
} |
|||
|
|||
$handle = fopen($this->deduplicationStore, 'rw+'); |
|||
flock($handle, LOCK_EX); |
|||
$validLogs = array(); |
|||
|
|||
$timestampValidity = time() - $this->time; |
|||
|
|||
while (!feof($handle)) { |
|||
$log = fgets($handle); |
|||
if (substr($log, 0, 10) >= $timestampValidity) { |
|||
$validLogs[] = $log; |
|||
} |
|||
} |
|||
|
|||
ftruncate($handle, 0); |
|||
rewind($handle); |
|||
foreach ($validLogs as $log) { |
|||
fwrite($handle, $log); |
|||
} |
|||
|
|||
flock($handle, LOCK_UN); |
|||
fclose($handle); |
|||
|
|||
$this->gc = false; |
|||
} |
|||
|
|||
private function appendRecord(array $record) |
|||
{ |
|||
file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND); |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\NormalizerFormatter; |
|||
use Doctrine\CouchDB\CouchDBClient; |
|||
|
|||
/** |
|||
* CouchDB handler for Doctrine CouchDB ODM |
|||
* |
|||
* @author Markus Bachmann <markus.bachmann@bachi.biz> |
|||
*/ |
|||
class DoctrineCouchDBHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $client; |
|||
|
|||
public function __construct(CouchDBClient $client, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
$this->client = $client; |
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->client->postDocument($record['formatted']); |
|||
} |
|||
|
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new NormalizerFormatter; |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Aws\Sdk; |
|||
use Aws\DynamoDb\DynamoDbClient; |
|||
use Aws\DynamoDb\Marshaler; |
|||
use Monolog\Formatter\ScalarFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/) |
|||
* |
|||
* @link https://github.com/aws/aws-sdk-php/ |
|||
* @author Andrew Lawson <adlawson@gmail.com> |
|||
*/ |
|||
class DynamoDbHandler extends AbstractProcessingHandler |
|||
{ |
|||
const DATE_FORMAT = 'Y-m-d\TH:i:s.uO'; |
|||
|
|||
/** |
|||
* @var DynamoDbClient |
|||
*/ |
|||
protected $client; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $table; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $version; |
|||
|
|||
/** |
|||
* @var Marshaler |
|||
*/ |
|||
protected $marshaler; |
|||
|
|||
/** |
|||
* @param DynamoDbClient $client |
|||
* @param string $table |
|||
* @param int $level |
|||
* @param bool $bubble |
|||
*/ |
|||
public function __construct(DynamoDbClient $client, $table, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) { |
|||
$this->version = 3; |
|||
$this->marshaler = new Marshaler; |
|||
} else { |
|||
$this->version = 2; |
|||
} |
|||
|
|||
$this->client = $client; |
|||
$this->table = $table; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$filtered = $this->filterEmptyFields($record['formatted']); |
|||
if ($this->version === 3) { |
|||
$formatted = $this->marshaler->marshalItem($filtered); |
|||
} else { |
|||
$formatted = $this->client->formatAttributes($filtered); |
|||
} |
|||
|
|||
$this->client->putItem(array( |
|||
'TableName' => $this->table, |
|||
'Item' => $formatted, |
|||
)); |
|||
} |
|||
|
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
protected function filterEmptyFields(array $record) |
|||
{ |
|||
return array_filter($record, function ($value) { |
|||
return !empty($value) || false === $value || 0 === $value; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new ScalarFormatter(self::DATE_FORMAT); |
|||
} |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Formatter\ElasticaFormatter; |
|||
use Monolog\Logger; |
|||
use Elastica\Client; |
|||
use Elastica\Exception\ExceptionInterface; |
|||
|
|||
/** |
|||
* Elastic Search handler |
|||
* |
|||
* Usage example: |
|||
* |
|||
* $client = new \Elastica\Client(); |
|||
* $options = array( |
|||
* 'index' => 'elastic_index_name', |
|||
* 'type' => 'elastic_doc_type', |
|||
* ); |
|||
* $handler = new ElasticSearchHandler($client, $options); |
|||
* $log = new Logger('application'); |
|||
* $log->pushHandler($handler); |
|||
* |
|||
* @author Jelle Vink <jelle.vink@gmail.com> |
|||
*/ |
|||
class ElasticSearchHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* @var Client |
|||
*/ |
|||
protected $client; |
|||
|
|||
/** |
|||
* @var array Handler config options |
|||
*/ |
|||
protected $options = array(); |
|||
|
|||
/** |
|||
* @param Client $client Elastica Client object |
|||
* @param array $options Handler configuration |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(Client $client, array $options = array(), $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
$this->client = $client; |
|||
$this->options = array_merge( |
|||
array( |
|||
'index' => 'monolog', // Elastic index name |
|||
'type' => 'record', // Elastic document type |
|||
'ignore_error' => false, // Suppress Elastica exceptions |
|||
), |
|||
$options |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->bulkSend(array($record['formatted'])); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
if ($formatter instanceof ElasticaFormatter) { |
|||
return parent::setFormatter($formatter); |
|||
} |
|||
throw new \InvalidArgumentException('ElasticSearchHandler is only compatible with ElasticaFormatter'); |
|||
} |
|||
|
|||
/** |
|||
* Getter options |
|||
* @return array |
|||
*/ |
|||
public function getOptions() |
|||
{ |
|||
return $this->options; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new ElasticaFormatter($this->options['index'], $this->options['type']); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
$documents = $this->getFormatter()->formatBatch($records); |
|||
$this->bulkSend($documents); |
|||
} |
|||
|
|||
/** |
|||
* Use Elasticsearch bulk API to send list of documents |
|||
* @param array $documents |
|||
* @throws \RuntimeException |
|||
*/ |
|||
protected function bulkSend(array $documents) |
|||
{ |
|||
try { |
|||
$this->client->addDocuments($documents); |
|||
} catch (ExceptionInterface $e) { |
|||
if (!$this->options['ignore_error']) { |
|||
throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Stores to PHP error_log() handler. |
|||
* |
|||
* @author Elan Ruusamäe <glen@delfi.ee> |
|||
*/ |
|||
class ErrorLogHandler extends AbstractProcessingHandler |
|||
{ |
|||
const OPERATING_SYSTEM = 0; |
|||
const SAPI = 4; |
|||
|
|||
protected $messageType; |
|||
protected $expandNewlines; |
|||
|
|||
/** |
|||
* @param int $messageType Says where the error should go. |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries |
|||
*/ |
|||
public function __construct($messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, $bubble = true, $expandNewlines = false) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
if (false === in_array($messageType, self::getAvailableTypes())) { |
|||
$message = sprintf('The given message type "%s" is not supported', print_r($messageType, true)); |
|||
throw new \InvalidArgumentException($message); |
|||
} |
|||
|
|||
$this->messageType = $messageType; |
|||
$this->expandNewlines = $expandNewlines; |
|||
} |
|||
|
|||
/** |
|||
* @return array With all available types |
|||
*/ |
|||
public static function getAvailableTypes() |
|||
{ |
|||
return array( |
|||
self::OPERATING_SYSTEM, |
|||
self::SAPI, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if ($this->expandNewlines) { |
|||
$lines = preg_split('{[\r\n]+}', (string) $record['formatted']); |
|||
foreach ($lines as $line) { |
|||
error_log($line, $this->messageType); |
|||
} |
|||
} else { |
|||
error_log((string) $record['formatted'], $this->messageType); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Simple handler wrapper that filters records based on a list of levels |
|||
* |
|||
* It can be configured with an exact list of levels to allow, or a min/max level. |
|||
* |
|||
* @author Hennadiy Verkh |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class FilterHandler extends AbstractHandler |
|||
{ |
|||
/** |
|||
* Handler or factory callable($record, $this) |
|||
* |
|||
* @var callable|\Monolog\Handler\HandlerInterface |
|||
*/ |
|||
protected $handler; |
|||
|
|||
/** |
|||
* Minimum level for logs that are passed to handler |
|||
* |
|||
* @var int[] |
|||
*/ |
|||
protected $acceptedLevels; |
|||
|
|||
/** |
|||
* Whether the messages that are handled can bubble up the stack or not |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $bubble; |
|||
|
|||
/** |
|||
* @param callable|HandlerInterface $handler Handler or factory callable($record, $this). |
|||
* @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided |
|||
* @param int $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, $bubble = true) |
|||
{ |
|||
$this->handler = $handler; |
|||
$this->bubble = $bubble; |
|||
$this->setAcceptedLevels($minLevelOrList, $maxLevel); |
|||
|
|||
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { |
|||
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function getAcceptedLevels() |
|||
{ |
|||
return array_flip($this->acceptedLevels); |
|||
} |
|||
|
|||
/** |
|||
* @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided |
|||
* @param int|string $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array |
|||
*/ |
|||
public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY) |
|||
{ |
|||
if (is_array($minLevelOrList)) { |
|||
$acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList); |
|||
} else { |
|||
$minLevelOrList = Logger::toMonologLevel($minLevelOrList); |
|||
$maxLevel = Logger::toMonologLevel($maxLevel); |
|||
$acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) { |
|||
return $level >= $minLevelOrList && $level <= $maxLevel; |
|||
})); |
|||
} |
|||
$this->acceptedLevels = array_flip($acceptedLevels); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHandling(array $record) |
|||
{ |
|||
return isset($this->acceptedLevels[$record['level']]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if (!$this->isHandling($record)) { |
|||
return false; |
|||
} |
|||
|
|||
// The same logic as in FingersCrossedHandler |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
$this->handler = call_user_func($this->handler, $record, $this); |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
throw new \RuntimeException("The factory callable should return a HandlerInterface"); |
|||
} |
|||
} |
|||
|
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
$this->handler->handle($record); |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
$filtered = array(); |
|||
foreach ($records as $record) { |
|||
if ($this->isHandling($record)) { |
|||
$filtered[] = $record; |
|||
} |
|||
} |
|||
|
|||
$this->handler->handleBatch($filtered); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\FingersCrossed; |
|||
|
|||
/** |
|||
* Interface for activation strategies for the FingersCrossedHandler. |
|||
* |
|||
* @author Johannes M. Schmitt <schmittjoh@gmail.com> |
|||
*/ |
|||
interface ActivationStrategyInterface |
|||
{ |
|||
/** |
|||
* Returns whether the given record activates the handler. |
|||
* |
|||
* @param array $record |
|||
* @return bool |
|||
*/ |
|||
public function isHandlerActivated(array $record); |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\FingersCrossed; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Channel and Error level based monolog activation strategy. Allows to trigger activation |
|||
* based on level per channel. e.g. trigger activation on level 'ERROR' by default, except |
|||
* for records of the 'sql' channel; those should trigger activation on level 'WARN'. |
|||
* |
|||
* Example: |
|||
* |
|||
* <code> |
|||
* $activationStrategy = new ChannelLevelActivationStrategy( |
|||
* Logger::CRITICAL, |
|||
* array( |
|||
* 'request' => Logger::ALERT, |
|||
* 'sensitive' => Logger::ERROR, |
|||
* ) |
|||
* ); |
|||
* $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy); |
|||
* </code> |
|||
* |
|||
* @author Mike Meessen <netmikey@gmail.com> |
|||
*/ |
|||
class ChannelLevelActivationStrategy implements ActivationStrategyInterface |
|||
{ |
|||
private $defaultActionLevel; |
|||
private $channelToActionLevel; |
|||
|
|||
/** |
|||
* @param int $defaultActionLevel The default action level to be used if the record's category doesn't match any |
|||
* @param array $channelToActionLevel An array that maps channel names to action levels. |
|||
*/ |
|||
public function __construct($defaultActionLevel, $channelToActionLevel = array()) |
|||
{ |
|||
$this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel); |
|||
$this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel); |
|||
} |
|||
|
|||
public function isHandlerActivated(array $record) |
|||
{ |
|||
if (isset($this->channelToActionLevel[$record['channel']])) { |
|||
return $record['level'] >= $this->channelToActionLevel[$record['channel']]; |
|||
} |
|||
|
|||
return $record['level'] >= $this->defaultActionLevel; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\FingersCrossed; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Error level based activation strategy. |
|||
* |
|||
* @author Johannes M. Schmitt <schmittjoh@gmail.com> |
|||
*/ |
|||
class ErrorLevelActivationStrategy implements ActivationStrategyInterface |
|||
{ |
|||
private $actionLevel; |
|||
|
|||
public function __construct($actionLevel) |
|||
{ |
|||
$this->actionLevel = Logger::toMonologLevel($actionLevel); |
|||
} |
|||
|
|||
public function isHandlerActivated(array $record) |
|||
{ |
|||
return $record['level'] >= $this->actionLevel; |
|||
} |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; |
|||
use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; |
|||
use Monolog\Logger; |
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Buffers all records until a certain level is reached |
|||
* |
|||
* The advantage of this approach is that you don't get any clutter in your log files. |
|||
* Only requests which actually trigger an error (or whatever your actionLevel is) will be |
|||
* in the logs, but they will contain all records, not only those above the level threshold. |
|||
* |
|||
* You can find the various activation strategies in the |
|||
* Monolog\Handler\FingersCrossed\ namespace. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class FingersCrossedHandler extends AbstractHandler |
|||
{ |
|||
protected $handler; |
|||
protected $activationStrategy; |
|||
protected $buffering = true; |
|||
protected $bufferSize; |
|||
protected $buffer = array(); |
|||
protected $stopBuffering; |
|||
protected $passthruLevel; |
|||
|
|||
/** |
|||
* @param callable|HandlerInterface $handler Handler or factory callable($record, $fingersCrossedHandler). |
|||
* @param int|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action |
|||
* @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $stopBuffering Whether the handler should stop buffering after being triggered (default true) |
|||
* @param int $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered |
|||
*/ |
|||
public function __construct($handler, $activationStrategy = null, $bufferSize = 0, $bubble = true, $stopBuffering = true, $passthruLevel = null) |
|||
{ |
|||
if (null === $activationStrategy) { |
|||
$activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING); |
|||
} |
|||
|
|||
// convert simple int activationStrategy to an object |
|||
if (!$activationStrategy instanceof ActivationStrategyInterface) { |
|||
$activationStrategy = new ErrorLevelActivationStrategy($activationStrategy); |
|||
} |
|||
|
|||
$this->handler = $handler; |
|||
$this->activationStrategy = $activationStrategy; |
|||
$this->bufferSize = $bufferSize; |
|||
$this->bubble = $bubble; |
|||
$this->stopBuffering = $stopBuffering; |
|||
|
|||
if ($passthruLevel !== null) { |
|||
$this->passthruLevel = Logger::toMonologLevel($passthruLevel); |
|||
} |
|||
|
|||
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { |
|||
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHandling(array $record) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Manually activate this logger regardless of the activation strategy |
|||
*/ |
|||
public function activate() |
|||
{ |
|||
if ($this->stopBuffering) { |
|||
$this->buffering = false; |
|||
} |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
$record = end($this->buffer) ?: null; |
|||
|
|||
$this->handler = call_user_func($this->handler, $record, $this); |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
throw new \RuntimeException("The factory callable should return a HandlerInterface"); |
|||
} |
|||
} |
|||
$this->handler->handleBatch($this->buffer); |
|||
$this->buffer = array(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
if ($this->buffering) { |
|||
$this->buffer[] = $record; |
|||
if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) { |
|||
array_shift($this->buffer); |
|||
} |
|||
if ($this->activationStrategy->isHandlerActivated($record)) { |
|||
$this->activate(); |
|||
} |
|||
} else { |
|||
$this->handler->handle($record); |
|||
} |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$this->flushBuffer(); |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
$this->flushBuffer(); |
|||
|
|||
parent::reset(); |
|||
|
|||
if ($this->handler instanceof ResettableInterface) { |
|||
$this->handler->reset(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Clears the buffer without flushing any messages down to the wrapped handler. |
|||
* |
|||
* It also resets the handler to its initial buffering state. |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$this->buffer = array(); |
|||
$this->reset(); |
|||
} |
|||
|
|||
/** |
|||
* Resets the state of the handler. Stops forwarding records to the wrapped handler. |
|||
*/ |
|||
private function flushBuffer() |
|||
{ |
|||
if (null !== $this->passthruLevel) { |
|||
$level = $this->passthruLevel; |
|||
$this->buffer = array_filter($this->buffer, function ($record) use ($level) { |
|||
return $record['level'] >= $level; |
|||
}); |
|||
if (count($this->buffer) > 0) { |
|||
$this->handler->handleBatch($this->buffer); |
|||
} |
|||
} |
|||
|
|||
$this->buffer = array(); |
|||
$this->buffering = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,195 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\WildfireFormatter; |
|||
|
|||
/** |
|||
* Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. |
|||
* |
|||
* @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com> |
|||
*/ |
|||
class FirePHPHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* WildFire JSON header message format |
|||
*/ |
|||
const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'; |
|||
|
|||
/** |
|||
* FirePHP structure for parsing messages & their presentation |
|||
*/ |
|||
const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'; |
|||
|
|||
/** |
|||
* Must reference a "known" plugin, otherwise headers won't display in FirePHP |
|||
*/ |
|||
const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3'; |
|||
|
|||
/** |
|||
* Header prefix for Wildfire to recognize & parse headers |
|||
*/ |
|||
const HEADER_PREFIX = 'X-Wf'; |
|||
|
|||
/** |
|||
* Whether or not Wildfire vendor-specific headers have been generated & sent yet |
|||
*/ |
|||
protected static $initialized = false; |
|||
|
|||
/** |
|||
* Shared static message index between potentially multiple handlers |
|||
* @var int |
|||
*/ |
|||
protected static $messageIndex = 1; |
|||
|
|||
protected static $sendHeaders = true; |
|||
|
|||
/** |
|||
* Base header creation function used by init headers & record headers |
|||
* |
|||
* @param array $meta Wildfire Plugin, Protocol & Structure Indexes |
|||
* @param string $message Log message |
|||
* @return array Complete header string ready for the client as key and message as value |
|||
*/ |
|||
protected function createHeader(array $meta, $message) |
|||
{ |
|||
$header = sprintf('%s-%s', self::HEADER_PREFIX, join('-', $meta)); |
|||
|
|||
return array($header => $message); |
|||
} |
|||
|
|||
/** |
|||
* Creates message header from record |
|||
* |
|||
* @see createHeader() |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function createRecordHeader(array $record) |
|||
{ |
|||
// Wildfire is extensible to support multiple protocols & plugins in a single request, |
|||
// but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake. |
|||
return $this->createHeader( |
|||
array(1, 1, 1, self::$messageIndex++), |
|||
$record['formatted'] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new WildfireFormatter(); |
|||
} |
|||
|
|||
/** |
|||
* Wildfire initialization headers to enable message parsing |
|||
* |
|||
* @see createHeader() |
|||
* @see sendHeader() |
|||
* @return array |
|||
*/ |
|||
protected function getInitHeaders() |
|||
{ |
|||
// Initial payload consists of required headers for Wildfire |
|||
return array_merge( |
|||
$this->createHeader(array('Protocol', 1), self::PROTOCOL_URI), |
|||
$this->createHeader(array(1, 'Structure', 1), self::STRUCTURE_URI), |
|||
$this->createHeader(array(1, 'Plugin', 1), self::PLUGIN_URI) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Send header string to the client |
|||
* |
|||
* @param string $header |
|||
* @param string $content |
|||
*/ |
|||
protected function sendHeader($header, $content) |
|||
{ |
|||
if (!headers_sent() && self::$sendHeaders) { |
|||
header(sprintf('%s: %s', $header, $content)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Creates & sends header for a record, ensuring init headers have been sent prior |
|||
* |
|||
* @see sendHeader() |
|||
* @see sendInitHeaders() |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if (!self::$sendHeaders) { |
|||
return; |
|||
} |
|||
|
|||
// WildFire-specific headers must be sent prior to any messages |
|||
if (!self::$initialized) { |
|||
self::$initialized = true; |
|||
|
|||
self::$sendHeaders = $this->headersAccepted(); |
|||
if (!self::$sendHeaders) { |
|||
return; |
|||
} |
|||
|
|||
foreach ($this->getInitHeaders() as $header => $content) { |
|||
$this->sendHeader($header, $content); |
|||
} |
|||
} |
|||
|
|||
$header = $this->createRecordHeader($record); |
|||
if (trim(current($header)) !== '') { |
|||
$this->sendHeader(key($header), current($header)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Verifies if the headers are accepted by the current user agent |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function headersAccepted() |
|||
{ |
|||
if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { |
|||
return true; |
|||
} |
|||
|
|||
return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); |
|||
} |
|||
|
|||
/** |
|||
* BC getter for the sendHeaders property that has been made static |
|||
*/ |
|||
public function __get($property) |
|||
{ |
|||
if ('sendHeaders' !== $property) { |
|||
throw new \InvalidArgumentException('Undefined property '.$property); |
|||
} |
|||
|
|||
return static::$sendHeaders; |
|||
} |
|||
|
|||
/** |
|||
* BC setter for the sendHeaders property that has been made static |
|||
*/ |
|||
public function __set($property, $value) |
|||
{ |
|||
if ('sendHeaders' !== $property) { |
|||
throw new \InvalidArgumentException('Undefined property '.$property); |
|||
} |
|||
|
|||
static::$sendHeaders = $value; |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Sends logs to Fleep.io using Webhook integrations |
|||
* |
|||
* You'll need a Fleep.io account to use this handler. |
|||
* |
|||
* @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation |
|||
* @author Ando Roots <ando@sqroot.eu> |
|||
*/ |
|||
class FleepHookHandler extends SocketHandler |
|||
{ |
|||
const FLEEP_HOST = 'fleep.io'; |
|||
|
|||
const FLEEP_HOOK_URI = '/hook/'; |
|||
|
|||
/** |
|||
* @var string Webhook token (specifies the conversation where logs are sent) |
|||
*/ |
|||
protected $token; |
|||
|
|||
/** |
|||
* Construct a new Fleep.io Handler. |
|||
* |
|||
* For instructions on how to create a new web hook in your conversations |
|||
* see https://fleep.io/integrations/webhooks/ |
|||
* |
|||
* @param string $token Webhook token |
|||
* @param bool|int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @throws MissingExtensionException |
|||
*/ |
|||
public function __construct($token, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!extension_loaded('openssl')) { |
|||
throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler'); |
|||
} |
|||
|
|||
$this->token = $token; |
|||
|
|||
$connectionString = 'ssl://' . self::FLEEP_HOST . ':443'; |
|||
parent::__construct($connectionString, $level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* Returns the default formatter to use with this handler |
|||
* |
|||
* Overloaded to remove empty context and extra arrays from the end of the log message. |
|||
* |
|||
* @return LineFormatter |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter(null, null, true, true); |
|||
} |
|||
|
|||
/** |
|||
* Handles a log record |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
public function write(array $record) |
|||
{ |
|||
parent::write($record); |
|||
$this->closeSocket(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
$content = $this->buildContent($record); |
|||
|
|||
return $this->buildHeader($content) . $content; |
|||
} |
|||
|
|||
/** |
|||
* Builds the header of the API Call |
|||
* |
|||
* @param string $content |
|||
* @return string |
|||
*/ |
|||
private function buildHeader($content) |
|||
{ |
|||
$header = "POST " . self::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n"; |
|||
$header .= "Host: " . self::FLEEP_HOST . "\r\n"; |
|||
$header .= "Content-Type: application/x-www-form-urlencoded\r\n"; |
|||
$header .= "Content-Length: " . strlen($content) . "\r\n"; |
|||
$header .= "\r\n"; |
|||
|
|||
return $header; |
|||
} |
|||
|
|||
/** |
|||
* Builds the body of API call |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
private function buildContent($record) |
|||
{ |
|||
$dataArray = array( |
|||
'message' => $record['formatted'], |
|||
); |
|||
|
|||
return http_build_query($dataArray); |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\FlowdockFormatter; |
|||
use Monolog\Formatter\FormatterInterface; |
|||
|
|||
/** |
|||
* Sends notifications through the Flowdock push API |
|||
* |
|||
* This must be configured with a FlowdockFormatter instance via setFormatter() |
|||
* |
|||
* Notes: |
|||
* API token - Flowdock API token |
|||
* |
|||
* @author Dominik Liebler <liebler.dominik@gmail.com> |
|||
* @see https://www.flowdock.com/api/push |
|||
*/ |
|||
class FlowdockHandler extends SocketHandler |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $apiToken; |
|||
|
|||
/** |
|||
* @param string $apiToken |
|||
* @param bool|int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* |
|||
* @throws MissingExtensionException if OpenSSL is missing |
|||
*/ |
|||
public function __construct($apiToken, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!extension_loaded('openssl')) { |
|||
throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler'); |
|||
} |
|||
|
|||
parent::__construct('ssl://api.flowdock.com:443', $level, $bubble); |
|||
$this->apiToken = $apiToken; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
if (!$formatter instanceof FlowdockFormatter) { |
|||
throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); |
|||
} |
|||
|
|||
return parent::setFormatter($formatter); |
|||
} |
|||
|
|||
/** |
|||
* Gets the default formatter. |
|||
* |
|||
* @return FormatterInterface |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
parent::write($record); |
|||
|
|||
$this->closeSocket(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
$content = $this->buildContent($record); |
|||
|
|||
return $this->buildHeader($content) . $content; |
|||
} |
|||
|
|||
/** |
|||
* Builds the body of API call |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
private function buildContent($record) |
|||
{ |
|||
return json_encode($record['formatted']['flowdock']); |
|||
} |
|||
|
|||
/** |
|||
* Builds the header of the API Call |
|||
* |
|||
* @param string $content |
|||
* @return string |
|||
*/ |
|||
private function buildHeader($content) |
|||
{ |
|||
$header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n"; |
|||
$header .= "Host: api.flowdock.com\r\n"; |
|||
$header .= "Content-Type: application/json\r\n"; |
|||
$header .= "Content-Length: " . strlen($content) . "\r\n"; |
|||
$header .= "\r\n"; |
|||
|
|||
return $header; |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Gelf\IMessagePublisher; |
|||
use Gelf\PublisherInterface; |
|||
use Gelf\Publisher; |
|||
use InvalidArgumentException; |
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\GelfMessageFormatter; |
|||
|
|||
/** |
|||
* Handler to send messages to a Graylog2 (http://www.graylog2.org) server |
|||
* |
|||
* @author Matt Lehner <mlehner@gmail.com> |
|||
* @author Benjamin Zikarsky <benjamin@zikarsky.de> |
|||
*/ |
|||
class GelfHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* @var Publisher the publisher object that sends the message to the server |
|||
*/ |
|||
protected $publisher; |
|||
|
|||
/** |
|||
* @param PublisherInterface|IMessagePublisher|Publisher $publisher a publisher object |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($publisher, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
if (!$publisher instanceof Publisher && !$publisher instanceof IMessagePublisher && !$publisher instanceof PublisherInterface) { |
|||
throw new InvalidArgumentException('Invalid publisher, expected a Gelf\Publisher, Gelf\IMessagePublisher or Gelf\PublisherInterface instance'); |
|||
} |
|||
|
|||
$this->publisher = $publisher; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->publisher->publish($record['formatted']); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new GelfMessageFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Forwards records to multiple handlers |
|||
* |
|||
* @author Lenar Lõhmus <lenar@city.ee> |
|||
*/ |
|||
class GroupHandler extends AbstractHandler |
|||
{ |
|||
protected $handlers; |
|||
|
|||
/** |
|||
* @param array $handlers Array of Handlers. |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(array $handlers, $bubble = true) |
|||
{ |
|||
foreach ($handlers as $handler) { |
|||
if (!$handler instanceof HandlerInterface) { |
|||
throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.'); |
|||
} |
|||
} |
|||
|
|||
$this->handlers = $handlers; |
|||
$this->bubble = $bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHandling(array $record) |
|||
{ |
|||
foreach ($this->handlers as $handler) { |
|||
if ($handler->isHandling($record)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
$handler->handle($record); |
|||
} |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
if ($this->processors) { |
|||
$processed = array(); |
|||
foreach ($records as $record) { |
|||
foreach ($this->processors as $processor) { |
|||
$processed[] = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
$records = $processed; |
|||
} |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
$handler->handleBatch($records); |
|||
} |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
parent::reset(); |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
if ($handler instanceof ResettableInterface) { |
|||
$handler->reset(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
foreach ($this->handlers as $handler) { |
|||
$handler->setFormatter($formatter); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
|
|||
/** |
|||
* Interface that all Monolog Handlers must implement |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
interface HandlerInterface |
|||
{ |
|||
/** |
|||
* Checks whether the given record will be handled by this handler. |
|||
* |
|||
* This is mostly done for performance reasons, to avoid calling processors for nothing. |
|||
* |
|||
* Handlers should still check the record levels within handle(), returning false in isHandling() |
|||
* is no guarantee that handle() will not be called, and isHandling() might not be called |
|||
* for a given record. |
|||
* |
|||
* @param array $record Partial log record containing only a level key |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isHandling(array $record); |
|||
|
|||
/** |
|||
* Handles a record. |
|||
* |
|||
* All records may be passed to this method, and the handler should discard |
|||
* those that it does not want to handle. |
|||
* |
|||
* The return value of this function controls the bubbling process of the handler stack. |
|||
* Unless the bubbling is interrupted (by returning true), the Logger class will keep on |
|||
* calling further handlers in the stack with a given log record. |
|||
* |
|||
* @param array $record The record to handle |
|||
* @return bool true means that this handler handled the record, and that bubbling is not permitted. |
|||
* false means the record was either not processed or that this handler allows bubbling. |
|||
*/ |
|||
public function handle(array $record); |
|||
|
|||
/** |
|||
* Handles a set of records at once. |
|||
* |
|||
* @param array $records The records to handle (an array of record arrays) |
|||
*/ |
|||
public function handleBatch(array $records); |
|||
|
|||
/** |
|||
* Adds a processor in the stack. |
|||
* |
|||
* @param callable $callback |
|||
* @return self |
|||
*/ |
|||
public function pushProcessor($callback); |
|||
|
|||
/** |
|||
* Removes the processor on top of the stack and returns it. |
|||
* |
|||
* @return callable |
|||
*/ |
|||
public function popProcessor(); |
|||
|
|||
/** |
|||
* Sets the formatter. |
|||
* |
|||
* @param FormatterInterface $formatter |
|||
* @return self |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter); |
|||
|
|||
/** |
|||
* Gets the formatter. |
|||
* |
|||
* @return FormatterInterface |
|||
*/ |
|||
public function getFormatter(); |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\ResettableInterface; |
|||
use Monolog\Formatter\FormatterInterface; |
|||
|
|||
/** |
|||
* This simple wrapper class can be used to extend handlers functionality. |
|||
* |
|||
* Example: A custom filtering that can be applied to any handler. |
|||
* |
|||
* Inherit from this class and override handle() like this: |
|||
* |
|||
* public function handle(array $record) |
|||
* { |
|||
* if ($record meets certain conditions) { |
|||
* return false; |
|||
* } |
|||
* return $this->handler->handle($record); |
|||
* } |
|||
* |
|||
* @author Alexey Karapetov <alexey@karapetov.com> |
|||
*/ |
|||
class HandlerWrapper implements HandlerInterface, ResettableInterface |
|||
{ |
|||
/** |
|||
* @var HandlerInterface |
|||
*/ |
|||
protected $handler; |
|||
|
|||
/** |
|||
* HandlerWrapper constructor. |
|||
* @param HandlerInterface $handler |
|||
*/ |
|||
public function __construct(HandlerInterface $handler) |
|||
{ |
|||
$this->handler = $handler; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isHandling(array $record) |
|||
{ |
|||
return $this->handler->isHandling($record); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
return $this->handler->handle($record); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
return $this->handler->handleBatch($records); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function pushProcessor($callback) |
|||
{ |
|||
$this->handler->pushProcessor($callback); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function popProcessor() |
|||
{ |
|||
return $this->handler->popProcessor(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
$this->handler->setFormatter($formatter); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getFormatter() |
|||
{ |
|||
return $this->handler->getFormatter(); |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
if ($this->handler instanceof ResettableInterface) { |
|||
return $this->handler->reset(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,365 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Sends notifications through the hipchat api to a hipchat room |
|||
* |
|||
* Notes: |
|||
* API token - HipChat API token |
|||
* Room - HipChat Room Id or name, where messages are sent |
|||
* Name - Name used to send the message (from) |
|||
* notify - Should the message trigger a notification in the clients |
|||
* version - The API version to use (HipChatHandler::API_V1 | HipChatHandler::API_V2) |
|||
* |
|||
* @author Rafael Dohms <rafael@doh.ms> |
|||
* @see https://www.hipchat.com/docs/api |
|||
*/ |
|||
class HipChatHandler extends SocketHandler |
|||
{ |
|||
/** |
|||
* Use API version 1 |
|||
*/ |
|||
const API_V1 = 'v1'; |
|||
|
|||
/** |
|||
* Use API version v2 |
|||
*/ |
|||
const API_V2 = 'v2'; |
|||
|
|||
/** |
|||
* The maximum allowed length for the name used in the "from" field. |
|||
*/ |
|||
const MAXIMUM_NAME_LENGTH = 15; |
|||
|
|||
/** |
|||
* The maximum allowed length for the message. |
|||
*/ |
|||
const MAXIMUM_MESSAGE_LENGTH = 9500; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $token; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $room; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $name; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
private $notify; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $format; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $host; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $version; |
|||
|
|||
/** |
|||
* @param string $token HipChat API Token |
|||
* @param string $room The room that should be alerted of the message (Id or Name) |
|||
* @param string $name Name used in the "from" field. |
|||
* @param bool $notify Trigger a notification in clients or not |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $useSSL Whether to connect via SSL. |
|||
* @param string $format The format of the messages (default to text, can be set to html if you have html in the messages) |
|||
* @param string $host The HipChat server hostname. |
|||
* @param string $version The HipChat API version (default HipChatHandler::API_V1) |
|||
*/ |
|||
public function __construct($token, $room, $name = 'Monolog', $notify = false, $level = Logger::CRITICAL, $bubble = true, $useSSL = true, $format = 'text', $host = 'api.hipchat.com', $version = self::API_V1) |
|||
{ |
|||
if ($version == self::API_V1 && !$this->validateStringLength($name, static::MAXIMUM_NAME_LENGTH)) { |
|||
throw new \InvalidArgumentException('The supplied name is too long. HipChat\'s v1 API supports names up to 15 UTF-8 characters.'); |
|||
} |
|||
|
|||
$connectionString = $useSSL ? 'ssl://'.$host.':443' : $host.':80'; |
|||
parent::__construct($connectionString, $level, $bubble); |
|||
|
|||
$this->token = $token; |
|||
$this->name = $name; |
|||
$this->notify = $notify; |
|||
$this->room = $room; |
|||
$this->format = $format; |
|||
$this->host = $host; |
|||
$this->version = $version; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
$content = $this->buildContent($record); |
|||
|
|||
return $this->buildHeader($content) . $content; |
|||
} |
|||
|
|||
/** |
|||
* Builds the body of API call |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
private function buildContent($record) |
|||
{ |
|||
$dataArray = array( |
|||
'notify' => $this->version == self::API_V1 ? |
|||
($this->notify ? 1 : 0) : |
|||
($this->notify ? 'true' : 'false'), |
|||
'message' => $record['formatted'], |
|||
'message_format' => $this->format, |
|||
'color' => $this->getAlertColor($record['level']), |
|||
); |
|||
|
|||
if (!$this->validateStringLength($dataArray['message'], static::MAXIMUM_MESSAGE_LENGTH)) { |
|||
if (function_exists('mb_substr')) { |
|||
$dataArray['message'] = mb_substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]'; |
|||
} else { |
|||
$dataArray['message'] = substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]'; |
|||
} |
|||
} |
|||
|
|||
// if we are using the legacy API then we need to send some additional information |
|||
if ($this->version == self::API_V1) { |
|||
$dataArray['room_id'] = $this->room; |
|||
} |
|||
|
|||
// append the sender name if it is set |
|||
// always append it if we use the v1 api (it is required in v1) |
|||
if ($this->version == self::API_V1 || $this->name !== null) { |
|||
$dataArray['from'] = (string) $this->name; |
|||
} |
|||
|
|||
return http_build_query($dataArray); |
|||
} |
|||
|
|||
/** |
|||
* Builds the header of the API Call |
|||
* |
|||
* @param string $content |
|||
* @return string |
|||
*/ |
|||
private function buildHeader($content) |
|||
{ |
|||
if ($this->version == self::API_V1) { |
|||
$header = "POST /v1/rooms/message?format=json&auth_token={$this->token} HTTP/1.1\r\n"; |
|||
} else { |
|||
// needed for rooms with special (spaces, etc) characters in the name |
|||
$room = rawurlencode($this->room); |
|||
$header = "POST /v2/room/{$room}/notification?auth_token={$this->token} HTTP/1.1\r\n"; |
|||
} |
|||
|
|||
$header .= "Host: {$this->host}\r\n"; |
|||
$header .= "Content-Type: application/x-www-form-urlencoded\r\n"; |
|||
$header .= "Content-Length: " . strlen($content) . "\r\n"; |
|||
$header .= "\r\n"; |
|||
|
|||
return $header; |
|||
} |
|||
|
|||
/** |
|||
* Assigns a color to each level of log records. |
|||
* |
|||
* @param int $level |
|||
* @return string |
|||
*/ |
|||
protected function getAlertColor($level) |
|||
{ |
|||
switch (true) { |
|||
case $level >= Logger::ERROR: |
|||
return 'red'; |
|||
case $level >= Logger::WARNING: |
|||
return 'yellow'; |
|||
case $level >= Logger::INFO: |
|||
return 'green'; |
|||
case $level == Logger::DEBUG: |
|||
return 'gray'; |
|||
default: |
|||
return 'yellow'; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
parent::write($record); |
|||
$this->finalizeWrite(); |
|||
} |
|||
|
|||
/** |
|||
* Finalizes the request by reading some bytes and then closing the socket |
|||
* |
|||
* If we do not read some but close the socket too early, hipchat sometimes |
|||
* drops the request entirely. |
|||
*/ |
|||
protected function finalizeWrite() |
|||
{ |
|||
$res = $this->getResource(); |
|||
if (is_resource($res)) { |
|||
@fread($res, 2048); |
|||
} |
|||
$this->closeSocket(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
if (count($records) == 0) { |
|||
return true; |
|||
} |
|||
|
|||
$batchRecords = $this->combineRecords($records); |
|||
|
|||
$handled = false; |
|||
foreach ($batchRecords as $batchRecord) { |
|||
if ($this->isHandling($batchRecord)) { |
|||
$this->write($batchRecord); |
|||
$handled = true; |
|||
} |
|||
} |
|||
|
|||
if (!$handled) { |
|||
return false; |
|||
} |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* Combines multiple records into one. Error level of the combined record |
|||
* will be the highest level from the given records. Datetime will be taken |
|||
* from the first record. |
|||
* |
|||
* @param $records |
|||
* @return array |
|||
*/ |
|||
private function combineRecords($records) |
|||
{ |
|||
$batchRecord = null; |
|||
$batchRecords = array(); |
|||
$messages = array(); |
|||
$formattedMessages = array(); |
|||
$level = 0; |
|||
$levelName = null; |
|||
$datetime = null; |
|||
|
|||
foreach ($records as $record) { |
|||
$record = $this->processRecord($record); |
|||
|
|||
if ($record['level'] > $level) { |
|||
$level = $record['level']; |
|||
$levelName = $record['level_name']; |
|||
} |
|||
|
|||
if (null === $datetime) { |
|||
$datetime = $record['datetime']; |
|||
} |
|||
|
|||
$messages[] = $record['message']; |
|||
$messageStr = implode(PHP_EOL, $messages); |
|||
$formattedMessages[] = $this->getFormatter()->format($record); |
|||
$formattedMessageStr = implode('', $formattedMessages); |
|||
|
|||
$batchRecord = array( |
|||
'message' => $messageStr, |
|||
'formatted' => $formattedMessageStr, |
|||
'context' => array(), |
|||
'extra' => array(), |
|||
); |
|||
|
|||
if (!$this->validateStringLength($batchRecord['formatted'], static::MAXIMUM_MESSAGE_LENGTH)) { |
|||
// Pop the last message and implode the remaining messages |
|||
$lastMessage = array_pop($messages); |
|||
$lastFormattedMessage = array_pop($formattedMessages); |
|||
$batchRecord['message'] = implode(PHP_EOL, $messages); |
|||
$batchRecord['formatted'] = implode('', $formattedMessages); |
|||
|
|||
$batchRecords[] = $batchRecord; |
|||
$messages = array($lastMessage); |
|||
$formattedMessages = array($lastFormattedMessage); |
|||
|
|||
$batchRecord = null; |
|||
} |
|||
} |
|||
|
|||
if (null !== $batchRecord) { |
|||
$batchRecords[] = $batchRecord; |
|||
} |
|||
|
|||
// Set the max level and datetime for all records |
|||
foreach ($batchRecords as &$batchRecord) { |
|||
$batchRecord = array_merge( |
|||
$batchRecord, |
|||
array( |
|||
'level' => $level, |
|||
'level_name' => $levelName, |
|||
'datetime' => $datetime, |
|||
) |
|||
); |
|||
} |
|||
|
|||
return $batchRecords; |
|||
} |
|||
|
|||
/** |
|||
* Validates the length of a string. |
|||
* |
|||
* If the `mb_strlen()` function is available, it will use that, as HipChat |
|||
* allows UTF-8 characters. Otherwise, it will fall back to `strlen()`. |
|||
* |
|||
* Note that this might cause false failures in the specific case of using |
|||
* a valid name with less than 16 characters, but 16 or more bytes, on a |
|||
* system where `mb_strlen()` is unavailable. |
|||
* |
|||
* @param string $str |
|||
* @param int $length |
|||
* |
|||
* @return bool |
|||
*/ |
|||
private function validateStringLength($str, $length) |
|||
{ |
|||
if (function_exists('mb_strlen')) { |
|||
return (mb_strlen($str) <= $length); |
|||
} |
|||
|
|||
return (strlen($str) <= $length); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* IFTTTHandler uses cURL to trigger IFTTT Maker actions |
|||
* |
|||
* Register a secret key and trigger/event name at https://ifttt.com/maker |
|||
* |
|||
* value1 will be the channel from monolog's Logger constructor, |
|||
* value2 will be the level name (ERROR, WARNING, ..) |
|||
* value3 will be the log record's message |
|||
* |
|||
* @author Nehal Patel <nehal@nehalpatel.me> |
|||
*/ |
|||
class IFTTTHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $eventName; |
|||
private $secretKey; |
|||
|
|||
/** |
|||
* @param string $eventName The name of the IFTTT Maker event that should be triggered |
|||
* @param string $secretKey A valid IFTTT secret key |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($eventName, $secretKey, $level = Logger::ERROR, $bubble = true) |
|||
{ |
|||
$this->eventName = $eventName; |
|||
$this->secretKey = $secretKey; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function write(array $record) |
|||
{ |
|||
$postData = array( |
|||
"value1" => $record["channel"], |
|||
"value2" => $record["level_name"], |
|||
"value3" => $record["message"], |
|||
); |
|||
$postString = json_encode($postData); |
|||
|
|||
$ch = curl_init(); |
|||
curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey); |
|||
curl_setopt($ch, CURLOPT_POST, true); |
|||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
|||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postString); |
|||
curl_setopt($ch, CURLOPT_HTTPHEADER, array( |
|||
"Content-Type: application/json", |
|||
)); |
|||
|
|||
Curl\Util::execute($ch); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Inspired on LogEntriesHandler. |
|||
* |
|||
* @author Robert Kaufmann III <rok3@rok3.me> |
|||
* @author Gabriel Machado <gabriel.ms1@hotmail.com> |
|||
*/ |
|||
class InsightOpsHandler extends SocketHandler |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $logToken; |
|||
|
|||
/** |
|||
* @param string $token Log token supplied by InsightOps |
|||
* @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'. |
|||
* @param bool $useSSL Whether or not SSL encryption should be used |
|||
* @param int $level The minimum logging level to trigger this handler |
|||
* @param bool $bubble Whether or not messages that are handled should bubble up the stack. |
|||
* |
|||
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing |
|||
*/ |
|||
public function __construct($token, $region = 'us', $useSSL = true, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if ($useSSL && !extension_loaded('openssl')) { |
|||
throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); |
|||
} |
|||
|
|||
$endpoint = $useSSL |
|||
? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443' |
|||
: $region . '.data.logs.insight.rapid7.com:80'; |
|||
|
|||
parent::__construct($endpoint, $level, $bubble); |
|||
$this->logToken = $token; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
return $this->logToken . ' ' . $record['formatted']; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* @author Robert Kaufmann III <rok3@rok3.me> |
|||
*/ |
|||
class LogEntriesHandler extends SocketHandler |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $logToken; |
|||
|
|||
/** |
|||
* @param string $token Log token supplied by LogEntries |
|||
* @param bool $useSSL Whether or not SSL encryption should be used. |
|||
* @param int $level The minimum logging level to trigger this handler |
|||
* @param bool $bubble Whether or not messages that are handled should bubble up the stack. |
|||
* |
|||
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing |
|||
*/ |
|||
public function __construct($token, $useSSL = true, $level = Logger::DEBUG, $bubble = true, $host = 'data.logentries.com') |
|||
{ |
|||
if ($useSSL && !extension_loaded('openssl')) { |
|||
throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); |
|||
} |
|||
|
|||
$endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80'; |
|||
parent::__construct($endpoint, $level, $bubble); |
|||
$this->logToken = $token; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
return $this->logToken . ' ' . $record['formatted']; |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\LogglyFormatter; |
|||
|
|||
/** |
|||
* Sends errors to Loggly. |
|||
* |
|||
* @author Przemek Sobstel <przemek@sobstel.org> |
|||
* @author Adam Pancutt <adam@pancutt.com> |
|||
* @author Gregory Barchard <gregory@barchard.net> |
|||
*/ |
|||
class LogglyHandler extends AbstractProcessingHandler |
|||
{ |
|||
const HOST = 'logs-01.loggly.com'; |
|||
const ENDPOINT_SINGLE = 'inputs'; |
|||
const ENDPOINT_BATCH = 'bulk'; |
|||
|
|||
protected $token; |
|||
|
|||
protected $tag = array(); |
|||
|
|||
public function __construct($token, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!extension_loaded('curl')) { |
|||
throw new \LogicException('The curl extension is needed to use the LogglyHandler'); |
|||
} |
|||
|
|||
$this->token = $token; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
public function setTag($tag) |
|||
{ |
|||
$tag = !empty($tag) ? $tag : array(); |
|||
$this->tag = is_array($tag) ? $tag : array($tag); |
|||
} |
|||
|
|||
public function addTag($tag) |
|||
{ |
|||
if (!empty($tag)) { |
|||
$tag = is_array($tag) ? $tag : array($tag); |
|||
$this->tag = array_unique(array_merge($this->tag, $tag)); |
|||
} |
|||
} |
|||
|
|||
protected function write(array $record) |
|||
{ |
|||
$this->send($record["formatted"], self::ENDPOINT_SINGLE); |
|||
} |
|||
|
|||
public function handleBatch(array $records) |
|||
{ |
|||
$level = $this->level; |
|||
|
|||
$records = array_filter($records, function ($record) use ($level) { |
|||
return ($record['level'] >= $level); |
|||
}); |
|||
|
|||
if ($records) { |
|||
$this->send($this->getFormatter()->formatBatch($records), self::ENDPOINT_BATCH); |
|||
} |
|||
} |
|||
|
|||
protected function send($data, $endpoint) |
|||
{ |
|||
$url = sprintf("https://%s/%s/%s/", self::HOST, $endpoint, $this->token); |
|||
|
|||
$headers = array('Content-Type: application/json'); |
|||
|
|||
if (!empty($this->tag)) { |
|||
$headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag); |
|||
} |
|||
|
|||
$ch = curl_init(); |
|||
|
|||
curl_setopt($ch, CURLOPT_URL, $url); |
|||
curl_setopt($ch, CURLOPT_POST, true); |
|||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); |
|||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); |
|||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
|||
|
|||
Curl\Util::execute($ch); |
|||
} |
|||
|
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LogglyFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
/** |
|||
* Base class for all mail handlers |
|||
* |
|||
* @author Gyula Sallai |
|||
*/ |
|||
abstract class MailHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
$messages = array(); |
|||
|
|||
foreach ($records as $record) { |
|||
if ($record['level'] < $this->level) { |
|||
continue; |
|||
} |
|||
$messages[] = $this->processRecord($record); |
|||
} |
|||
|
|||
if (!empty($messages)) { |
|||
$this->send((string) $this->getFormatter()->formatBatch($messages), $messages); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send a mail with the given content |
|||
* |
|||
* @param string $content formatted email body to be sent |
|||
* @param array $records the array of log records that formed this content |
|||
*/ |
|||
abstract protected function send($content, array $records); |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->send((string) $record['formatted'], array($record)); |
|||
} |
|||
|
|||
protected function getHighestRecord(array $records) |
|||
{ |
|||
$highestRecord = null; |
|||
foreach ($records as $record) { |
|||
if ($highestRecord === null || $highestRecord['level'] < $record['level']) { |
|||
$highestRecord = $record; |
|||
} |
|||
} |
|||
|
|||
return $highestRecord; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* MandrillHandler uses cURL to send the emails to the Mandrill API |
|||
* |
|||
* @author Adam Nicholson <adamnicholson10@gmail.com> |
|||
*/ |
|||
class MandrillHandler extends MailHandler |
|||
{ |
|||
protected $message; |
|||
protected $apiKey; |
|||
|
|||
/** |
|||
* @param string $apiKey A valid Mandrill API key |
|||
* @param callable|\Swift_Message $message An example message for real messages, only the body will be replaced |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($apiKey, $message, $level = Logger::ERROR, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
if (!$message instanceof \Swift_Message && is_callable($message)) { |
|||
$message = call_user_func($message); |
|||
} |
|||
if (!$message instanceof \Swift_Message) { |
|||
throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it'); |
|||
} |
|||
$this->message = $message; |
|||
$this->apiKey = $apiKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function send($content, array $records) |
|||
{ |
|||
$message = clone $this->message; |
|||
$message->setBody($content); |
|||
$message->setDate(time()); |
|||
|
|||
$ch = curl_init(); |
|||
|
|||
curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json'); |
|||
curl_setopt($ch, CURLOPT_POST, 1); |
|||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); |
|||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array( |
|||
'key' => $this->apiKey, |
|||
'raw_message' => (string) $message, |
|||
'async' => false, |
|||
))); |
|||
|
|||
Curl\Util::execute($ch); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
/** |
|||
* Exception can be thrown if an extension for an handler is missing |
|||
* |
|||
* @author Christian Bergau <cbergau86@gmail.com> |
|||
*/ |
|||
class MissingExtensionException extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\NormalizerFormatter; |
|||
|
|||
/** |
|||
* Logs to a MongoDB database. |
|||
* |
|||
* usage example: |
|||
* |
|||
* $log = new Logger('application'); |
|||
* $mongodb = new MongoDBHandler(new \Mongo("mongodb://localhost:27017"), "logs", "prod"); |
|||
* $log->pushHandler($mongodb); |
|||
* |
|||
* @author Thomas Tourlourat <thomas@tourlourat.com> |
|||
*/ |
|||
class MongoDBHandler extends AbstractProcessingHandler |
|||
{ |
|||
protected $mongoCollection; |
|||
|
|||
public function __construct($mongo, $database, $collection, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo || $mongo instanceof \MongoDB\Client)) { |
|||
throw new \InvalidArgumentException('MongoClient, Mongo or MongoDB\Client instance required'); |
|||
} |
|||
|
|||
$this->mongoCollection = $mongo->selectCollection($database, $collection); |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
protected function write(array $record) |
|||
{ |
|||
if ($this->mongoCollection instanceof \MongoDB\Collection) { |
|||
$this->mongoCollection->insertOne($record["formatted"]); |
|||
} else { |
|||
$this->mongoCollection->save($record["formatted"]); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new NormalizerFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,185 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\LineFormatter; |
|||
|
|||
/** |
|||
* NativeMailerHandler uses the mail() function to send the emails |
|||
* |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
* @author Mark Garrett <mark@moderndeveloperllc.com> |
|||
*/ |
|||
class NativeMailerHandler extends MailHandler |
|||
{ |
|||
/** |
|||
* The email addresses to which the message will be sent |
|||
* @var array |
|||
*/ |
|||
protected $to; |
|||
|
|||
/** |
|||
* The subject of the email |
|||
* @var string |
|||
*/ |
|||
protected $subject; |
|||
|
|||
/** |
|||
* Optional headers for the message |
|||
* @var array |
|||
*/ |
|||
protected $headers = array(); |
|||
|
|||
/** |
|||
* Optional parameters for the message |
|||
* @var array |
|||
*/ |
|||
protected $parameters = array(); |
|||
|
|||
/** |
|||
* The wordwrap length for the message |
|||
* @var int |
|||
*/ |
|||
protected $maxColumnWidth; |
|||
|
|||
/** |
|||
* The Content-type for the message |
|||
* @var string |
|||
*/ |
|||
protected $contentType = 'text/plain'; |
|||
|
|||
/** |
|||
* The encoding for the message |
|||
* @var string |
|||
*/ |
|||
protected $encoding = 'utf-8'; |
|||
|
|||
/** |
|||
* @param string|array $to The receiver of the mail |
|||
* @param string $subject The subject of the mail |
|||
* @param string $from The sender of the mail |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param int $maxColumnWidth The maximum column width that the message lines will have |
|||
*/ |
|||
public function __construct($to, $subject, $from, $level = Logger::ERROR, $bubble = true, $maxColumnWidth = 70) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
$this->to = is_array($to) ? $to : array($to); |
|||
$this->subject = $subject; |
|||
$this->addHeader(sprintf('From: %s', $from)); |
|||
$this->maxColumnWidth = $maxColumnWidth; |
|||
} |
|||
|
|||
/** |
|||
* Add headers to the message |
|||
* |
|||
* @param string|array $headers Custom added headers |
|||
* @return self |
|||
*/ |
|||
public function addHeader($headers) |
|||
{ |
|||
foreach ((array) $headers as $header) { |
|||
if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) { |
|||
throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons'); |
|||
} |
|||
$this->headers[] = $header; |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Add parameters to the message |
|||
* |
|||
* @param string|array $parameters Custom added parameters |
|||
* @return self |
|||
*/ |
|||
public function addParameter($parameters) |
|||
{ |
|||
$this->parameters = array_merge($this->parameters, (array) $parameters); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function send($content, array $records) |
|||
{ |
|||
$content = wordwrap($content, $this->maxColumnWidth); |
|||
$headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n"); |
|||
$headers .= 'Content-type: ' . $this->getContentType() . '; charset=' . $this->getEncoding() . "\r\n"; |
|||
if ($this->getContentType() == 'text/html' && false === strpos($headers, 'MIME-Version:')) { |
|||
$headers .= 'MIME-Version: 1.0' . "\r\n"; |
|||
} |
|||
|
|||
$subject = $this->subject; |
|||
if ($records) { |
|||
$subjectFormatter = new LineFormatter($this->subject); |
|||
$subject = $subjectFormatter->format($this->getHighestRecord($records)); |
|||
} |
|||
|
|||
$parameters = implode(' ', $this->parameters); |
|||
foreach ($this->to as $to) { |
|||
mail($to, $subject, $content, $headers, $parameters); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return string $contentType |
|||
*/ |
|||
public function getContentType() |
|||
{ |
|||
return $this->contentType; |
|||
} |
|||
|
|||
/** |
|||
* @return string $encoding |
|||
*/ |
|||
public function getEncoding() |
|||
{ |
|||
return $this->encoding; |
|||
} |
|||
|
|||
/** |
|||
* @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML |
|||
* messages. |
|||
* @return self |
|||
*/ |
|||
public function setContentType($contentType) |
|||
{ |
|||
if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) { |
|||
throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection'); |
|||
} |
|||
|
|||
$this->contentType = $contentType; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param string $encoding |
|||
* @return self |
|||
*/ |
|||
public function setEncoding($encoding) |
|||
{ |
|||
if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) { |
|||
throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection'); |
|||
} |
|||
|
|||
$this->encoding = $encoding; |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,204 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\NormalizerFormatter; |
|||
|
|||
/** |
|||
* Class to record a log on a NewRelic application. |
|||
* Enabling New Relic High Security mode may prevent capture of useful information. |
|||
* |
|||
* This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] |
|||
* |
|||
* @see https://docs.newrelic.com/docs/agents/php-agent |
|||
* @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security |
|||
*/ |
|||
class NewRelicHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Name of the New Relic application that will receive logs from this handler. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $appName; |
|||
|
|||
/** |
|||
* Name of the current transaction |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $transactionName; |
|||
|
|||
/** |
|||
* Some context and extra data is passed into the handler as arrays of values. Do we send them as is |
|||
* (useful if we are using the API), or explode them for display on the NewRelic RPM website? |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $explodeArrays; |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
* |
|||
* @param string $appName |
|||
* @param bool $explodeArrays |
|||
* @param string $transactionName |
|||
*/ |
|||
public function __construct( |
|||
$level = Logger::ERROR, |
|||
$bubble = true, |
|||
$appName = null, |
|||
$explodeArrays = false, |
|||
$transactionName = null |
|||
) { |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->appName = $appName; |
|||
$this->explodeArrays = $explodeArrays; |
|||
$this->transactionName = $transactionName; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if (!$this->isNewRelicEnabled()) { |
|||
throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); |
|||
} |
|||
|
|||
if ($appName = $this->getAppName($record['context'])) { |
|||
$this->setNewRelicAppName($appName); |
|||
} |
|||
|
|||
if ($transactionName = $this->getTransactionName($record['context'])) { |
|||
$this->setNewRelicTransactionName($transactionName); |
|||
unset($record['formatted']['context']['transaction_name']); |
|||
} |
|||
|
|||
if (isset($record['context']['exception']) && ($record['context']['exception'] instanceof \Exception || (PHP_VERSION_ID >= 70000 && $record['context']['exception'] instanceof \Throwable))) { |
|||
newrelic_notice_error($record['message'], $record['context']['exception']); |
|||
unset($record['formatted']['context']['exception']); |
|||
} else { |
|||
newrelic_notice_error($record['message']); |
|||
} |
|||
|
|||
if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) { |
|||
foreach ($record['formatted']['context'] as $key => $parameter) { |
|||
if (is_array($parameter) && $this->explodeArrays) { |
|||
foreach ($parameter as $paramKey => $paramValue) { |
|||
$this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue); |
|||
} |
|||
} else { |
|||
$this->setNewRelicParameter('context_' . $key, $parameter); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) { |
|||
foreach ($record['formatted']['extra'] as $key => $parameter) { |
|||
if (is_array($parameter) && $this->explodeArrays) { |
|||
foreach ($parameter as $paramKey => $paramValue) { |
|||
$this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue); |
|||
} |
|||
} else { |
|||
$this->setNewRelicParameter('extra_' . $key, $parameter); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Checks whether the NewRelic extension is enabled in the system. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function isNewRelicEnabled() |
|||
{ |
|||
return extension_loaded('newrelic'); |
|||
} |
|||
|
|||
/** |
|||
* Returns the appname where this log should be sent. Each log can override the default appname, set in this |
|||
* handler's constructor, by providing the appname in it's context. |
|||
* |
|||
* @param array $context |
|||
* @return null|string |
|||
*/ |
|||
protected function getAppName(array $context) |
|||
{ |
|||
if (isset($context['appname'])) { |
|||
return $context['appname']; |
|||
} |
|||
|
|||
return $this->appName; |
|||
} |
|||
|
|||
/** |
|||
* Returns the name of the current transaction. Each log can override the default transaction name, set in this |
|||
* handler's constructor, by providing the transaction_name in it's context |
|||
* |
|||
* @param array $context |
|||
* |
|||
* @return null|string |
|||
*/ |
|||
protected function getTransactionName(array $context) |
|||
{ |
|||
if (isset($context['transaction_name'])) { |
|||
return $context['transaction_name']; |
|||
} |
|||
|
|||
return $this->transactionName; |
|||
} |
|||
|
|||
/** |
|||
* Sets the NewRelic application that should receive this log. |
|||
* |
|||
* @param string $appName |
|||
*/ |
|||
protected function setNewRelicAppName($appName) |
|||
{ |
|||
newrelic_set_appname($appName); |
|||
} |
|||
|
|||
/** |
|||
* Overwrites the name of the current transaction |
|||
* |
|||
* @param string $transactionName |
|||
*/ |
|||
protected function setNewRelicTransactionName($transactionName) |
|||
{ |
|||
newrelic_name_transaction($transactionName); |
|||
} |
|||
|
|||
/** |
|||
* @param string $key |
|||
* @param mixed $value |
|||
*/ |
|||
protected function setNewRelicParameter($key, $value) |
|||
{ |
|||
if (null === $value || is_scalar($value)) { |
|||
newrelic_add_custom_parameter($key, $value); |
|||
} else { |
|||
newrelic_add_custom_parameter($key, @json_encode($value)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new NormalizerFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Blackhole |
|||
* |
|||
* Any record it can handle will be thrown away. This can be used |
|||
* to put on top of an existing stack to override it temporarily. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class NullHandler extends AbstractHandler |
|||
{ |
|||
/** |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
*/ |
|||
public function __construct($level = Logger::DEBUG) |
|||
{ |
|||
parent::__construct($level, false); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if ($record['level'] < $this->level) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,242 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Exception; |
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Logger; |
|||
use PhpConsole\Connector; |
|||
use PhpConsole\Handler; |
|||
use PhpConsole\Helper; |
|||
|
|||
/** |
|||
* Monolog handler for Google Chrome extension "PHP Console" |
|||
* |
|||
* Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely |
|||
* |
|||
* Usage: |
|||
* 1. Install Google Chrome extension https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef |
|||
* 2. See overview https://github.com/barbushin/php-console#overview |
|||
* 3. Install PHP Console library https://github.com/barbushin/php-console#installation |
|||
* 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png) |
|||
* |
|||
* $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler())); |
|||
* \Monolog\ErrorHandler::register($logger); |
|||
* echo $undefinedVar; |
|||
* $logger->addDebug('SELECT * FROM users', array('db', 'time' => 0.012)); |
|||
* PC::debug($_SERVER); // PHP Console debugger for any type of vars |
|||
* |
|||
* @author Sergey Barbushin https://www.linkedin.com/in/barbushin |
|||
*/ |
|||
class PHPConsoleHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $options = array( |
|||
'enabled' => true, // bool Is PHP Console server enabled |
|||
'classesPartialsTraceIgnore' => array('Monolog\\'), // array Hide calls of classes started with... |
|||
'debugTagsKeysInContext' => array(0, 'tag'), // bool Is PHP Console server enabled |
|||
'useOwnErrorsHandler' => false, // bool Enable errors handling |
|||
'useOwnExceptionsHandler' => false, // bool Enable exceptions handling |
|||
'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths |
|||
'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s') |
|||
'serverEncoding' => null, // string|null Server internal encoding |
|||
'headersLimit' => null, // int|null Set headers size limit for your web-server |
|||
'password' => null, // string|null Protect PHP Console connection by password |
|||
'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed |
|||
'ipMasks' => array(), // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1') |
|||
'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required) |
|||
'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings |
|||
'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level |
|||
'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number |
|||
'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item |
|||
'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON |
|||
'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug |
|||
'dataStorage' => null, // PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ) |
|||
); |
|||
|
|||
/** @var Connector */ |
|||
private $connector; |
|||
|
|||
/** |
|||
* @param array $options See \Monolog\Handler\PHPConsoleHandler::$options for more details |
|||
* @param Connector|null $connector Instance of \PhpConsole\Connector class (optional) |
|||
* @param int $level |
|||
* @param bool $bubble |
|||
* @throws Exception |
|||
*/ |
|||
public function __construct(array $options = array(), Connector $connector = null, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!class_exists('PhpConsole\Connector')) { |
|||
throw new Exception('PHP Console library not found. See https://github.com/barbushin/php-console#installation'); |
|||
} |
|||
parent::__construct($level, $bubble); |
|||
$this->options = $this->initOptions($options); |
|||
$this->connector = $this->initConnector($connector); |
|||
} |
|||
|
|||
private function initOptions(array $options) |
|||
{ |
|||
$wrongOptions = array_diff(array_keys($options), array_keys($this->options)); |
|||
if ($wrongOptions) { |
|||
throw new Exception('Unknown options: ' . implode(', ', $wrongOptions)); |
|||
} |
|||
|
|||
return array_replace($this->options, $options); |
|||
} |
|||
|
|||
private function initConnector(Connector $connector = null) |
|||
{ |
|||
if (!$connector) { |
|||
if ($this->options['dataStorage']) { |
|||
Connector::setPostponeStorage($this->options['dataStorage']); |
|||
} |
|||
$connector = Connector::getInstance(); |
|||
} |
|||
|
|||
if ($this->options['registerHelper'] && !Helper::isRegistered()) { |
|||
Helper::register(); |
|||
} |
|||
|
|||
if ($this->options['enabled'] && $connector->isActiveClient()) { |
|||
if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) { |
|||
$handler = Handler::getInstance(); |
|||
$handler->setHandleErrors($this->options['useOwnErrorsHandler']); |
|||
$handler->setHandleExceptions($this->options['useOwnExceptionsHandler']); |
|||
$handler->start(); |
|||
} |
|||
if ($this->options['sourcesBasePath']) { |
|||
$connector->setSourcesBasePath($this->options['sourcesBasePath']); |
|||
} |
|||
if ($this->options['serverEncoding']) { |
|||
$connector->setServerEncoding($this->options['serverEncoding']); |
|||
} |
|||
if ($this->options['password']) { |
|||
$connector->setPassword($this->options['password']); |
|||
} |
|||
if ($this->options['enableSslOnlyMode']) { |
|||
$connector->enableSslOnlyMode(); |
|||
} |
|||
if ($this->options['ipMasks']) { |
|||
$connector->setAllowedIpMasks($this->options['ipMasks']); |
|||
} |
|||
if ($this->options['headersLimit']) { |
|||
$connector->setHeadersLimit($this->options['headersLimit']); |
|||
} |
|||
if ($this->options['detectDumpTraceAndSource']) { |
|||
$connector->getDebugDispatcher()->detectTraceAndSource = true; |
|||
} |
|||
$dumper = $connector->getDumper(); |
|||
$dumper->levelLimit = $this->options['dumperLevelLimit']; |
|||
$dumper->itemsCountLimit = $this->options['dumperItemsCountLimit']; |
|||
$dumper->itemSizeLimit = $this->options['dumperItemSizeLimit']; |
|||
$dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit']; |
|||
$dumper->detectCallbacks = $this->options['dumperDetectCallbacks']; |
|||
if ($this->options['enableEvalListener']) { |
|||
$connector->startEvalRequestsListener(); |
|||
} |
|||
} |
|||
|
|||
return $connector; |
|||
} |
|||
|
|||
public function getConnector() |
|||
{ |
|||
return $this->connector; |
|||
} |
|||
|
|||
public function getOptions() |
|||
{ |
|||
return $this->options; |
|||
} |
|||
|
|||
public function handle(array $record) |
|||
{ |
|||
if ($this->options['enabled'] && $this->connector->isActiveClient()) { |
|||
return parent::handle($record); |
|||
} |
|||
|
|||
return !$this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* Writes the record down to the log of the implementing handler |
|||
* |
|||
* @param array $record |
|||
* @return void |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if ($record['level'] < Logger::NOTICE) { |
|||
$this->handleDebugRecord($record); |
|||
} elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof Exception) { |
|||
$this->handleExceptionRecord($record); |
|||
} else { |
|||
$this->handleErrorRecord($record); |
|||
} |
|||
} |
|||
|
|||
private function handleDebugRecord(array $record) |
|||
{ |
|||
$tags = $this->getRecordTags($record); |
|||
$message = $record['message']; |
|||
if ($record['context']) { |
|||
$message .= ' ' . json_encode($this->connector->getDumper()->dump(array_filter($record['context']))); |
|||
} |
|||
$this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); |
|||
} |
|||
|
|||
private function handleExceptionRecord(array $record) |
|||
{ |
|||
$this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']); |
|||
} |
|||
|
|||
private function handleErrorRecord(array $record) |
|||
{ |
|||
$context = $record['context']; |
|||
|
|||
$this->connector->getErrorsDispatcher()->dispatchError( |
|||
isset($context['code']) ? $context['code'] : null, |
|||
isset($context['message']) ? $context['message'] : $record['message'], |
|||
isset($context['file']) ? $context['file'] : null, |
|||
isset($context['line']) ? $context['line'] : null, |
|||
$this->options['classesPartialsTraceIgnore'] |
|||
); |
|||
} |
|||
|
|||
private function getRecordTags(array &$record) |
|||
{ |
|||
$tags = null; |
|||
if (!empty($record['context'])) { |
|||
$context = & $record['context']; |
|||
foreach ($this->options['debugTagsKeysInContext'] as $key) { |
|||
if (!empty($context[$key])) { |
|||
$tags = $context[$key]; |
|||
if ($key === 0) { |
|||
array_shift($context); |
|||
} else { |
|||
unset($context[$key]); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return $tags ?: strtolower($record['level_name']); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter('%message%'); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Psr\Log\LoggerInterface; |
|||
|
|||
/** |
|||
* Proxies log messages to an existing PSR-3 compliant logger. |
|||
* |
|||
* @author Michael Moussa <michael.moussa@gmail.com> |
|||
*/ |
|||
class PsrHandler extends AbstractHandler |
|||
{ |
|||
/** |
|||
* PSR-3 compliant logger |
|||
* |
|||
* @var LoggerInterface |
|||
*/ |
|||
protected $logger; |
|||
|
|||
/** |
|||
* @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->logger = $logger; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if (!$this->isHandling($record)) { |
|||
return false; |
|||
} |
|||
|
|||
$this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']); |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
} |
|||
@ -0,0 +1,185 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Sends notifications through the pushover api to mobile phones |
|||
* |
|||
* @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com> |
|||
* @see https://www.pushover.net/api |
|||
*/ |
|||
class PushoverHandler extends SocketHandler |
|||
{ |
|||
private $token; |
|||
private $users; |
|||
private $title; |
|||
private $user; |
|||
private $retry; |
|||
private $expire; |
|||
|
|||
private $highPriorityLevel; |
|||
private $emergencyLevel; |
|||
private $useFormattedMessage = false; |
|||
|
|||
/** |
|||
* All parameters that can be sent to Pushover |
|||
* @see https://pushover.net/api |
|||
* @var array |
|||
*/ |
|||
private $parameterNames = array( |
|||
'token' => true, |
|||
'user' => true, |
|||
'message' => true, |
|||
'device' => true, |
|||
'title' => true, |
|||
'url' => true, |
|||
'url_title' => true, |
|||
'priority' => true, |
|||
'timestamp' => true, |
|||
'sound' => true, |
|||
'retry' => true, |
|||
'expire' => true, |
|||
'callback' => true, |
|||
); |
|||
|
|||
/** |
|||
* Sounds the api supports by default |
|||
* @see https://pushover.net/api#sounds |
|||
* @var array |
|||
*/ |
|||
private $sounds = array( |
|||
'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming', |
|||
'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb', |
|||
'persistent', 'echo', 'updown', 'none', |
|||
); |
|||
|
|||
/** |
|||
* @param string $token Pushover api token |
|||
* @param string|array $users Pushover user id or array of ids the message will be sent to |
|||
* @param string $title Title sent to the Pushover API |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not |
|||
* the pushover.net app owner. OpenSSL is required for this option. |
|||
* @param int $highPriorityLevel The minimum logging level at which this handler will start |
|||
* sending "high priority" requests to the Pushover API |
|||
* @param int $emergencyLevel The minimum logging level at which this handler will start |
|||
* sending "emergency" requests to the Pushover API |
|||
* @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will send the same notification to the user. |
|||
* @param int $expire The expire parameter specifies how many seconds your notification will continue to be retried for (every retry seconds). |
|||
*/ |
|||
public function __construct($token, $users, $title = null, $level = Logger::CRITICAL, $bubble = true, $useSSL = true, $highPriorityLevel = Logger::CRITICAL, $emergencyLevel = Logger::EMERGENCY, $retry = 30, $expire = 25200) |
|||
{ |
|||
$connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80'; |
|||
parent::__construct($connectionString, $level, $bubble); |
|||
|
|||
$this->token = $token; |
|||
$this->users = (array) $users; |
|||
$this->title = $title ?: gethostname(); |
|||
$this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel); |
|||
$this->emergencyLevel = Logger::toMonologLevel($emergencyLevel); |
|||
$this->retry = $retry; |
|||
$this->expire = $expire; |
|||
} |
|||
|
|||
protected function generateDataStream($record) |
|||
{ |
|||
$content = $this->buildContent($record); |
|||
|
|||
return $this->buildHeader($content) . $content; |
|||
} |
|||
|
|||
private function buildContent($record) |
|||
{ |
|||
// Pushover has a limit of 512 characters on title and message combined. |
|||
$maxMessageLength = 512 - strlen($this->title); |
|||
|
|||
$message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message']; |
|||
$message = substr($message, 0, $maxMessageLength); |
|||
|
|||
$timestamp = $record['datetime']->getTimestamp(); |
|||
|
|||
$dataArray = array( |
|||
'token' => $this->token, |
|||
'user' => $this->user, |
|||
'message' => $message, |
|||
'title' => $this->title, |
|||
'timestamp' => $timestamp, |
|||
); |
|||
|
|||
if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) { |
|||
$dataArray['priority'] = 2; |
|||
$dataArray['retry'] = $this->retry; |
|||
$dataArray['expire'] = $this->expire; |
|||
} elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) { |
|||
$dataArray['priority'] = 1; |
|||
} |
|||
|
|||
// First determine the available parameters |
|||
$context = array_intersect_key($record['context'], $this->parameterNames); |
|||
$extra = array_intersect_key($record['extra'], $this->parameterNames); |
|||
|
|||
// Least important info should be merged with subsequent info |
|||
$dataArray = array_merge($extra, $context, $dataArray); |
|||
|
|||
// Only pass sounds that are supported by the API |
|||
if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) { |
|||
unset($dataArray['sound']); |
|||
} |
|||
|
|||
return http_build_query($dataArray); |
|||
} |
|||
|
|||
private function buildHeader($content) |
|||
{ |
|||
$header = "POST /1/messages.json HTTP/1.1\r\n"; |
|||
$header .= "Host: api.pushover.net\r\n"; |
|||
$header .= "Content-Type: application/x-www-form-urlencoded\r\n"; |
|||
$header .= "Content-Length: " . strlen($content) . "\r\n"; |
|||
$header .= "\r\n"; |
|||
|
|||
return $header; |
|||
} |
|||
|
|||
protected function write(array $record) |
|||
{ |
|||
foreach ($this->users as $user) { |
|||
$this->user = $user; |
|||
|
|||
parent::write($record); |
|||
$this->closeSocket(); |
|||
} |
|||
|
|||
$this->user = null; |
|||
} |
|||
|
|||
public function setHighPriorityLevel($value) |
|||
{ |
|||
$this->highPriorityLevel = $value; |
|||
} |
|||
|
|||
public function setEmergencyLevel($value) |
|||
{ |
|||
$this->emergencyLevel = $value; |
|||
} |
|||
|
|||
/** |
|||
* Use the formatted message? |
|||
* @param bool $value |
|||
*/ |
|||
public function useFormattedMessage($value) |
|||
{ |
|||
$this->useFormattedMessage = (bool) $value; |
|||
} |
|||
} |
|||
@ -0,0 +1,232 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Logger; |
|||
use Raven_Client; |
|||
|
|||
/** |
|||
* Handler to send messages to a Sentry (https://github.com/getsentry/sentry) server |
|||
* using sentry-php (https://github.com/getsentry/sentry-php) |
|||
* |
|||
* @author Marc Abramowitz <marc@marc-abramowitz.com> |
|||
*/ |
|||
class RavenHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Translates Monolog log levels to Raven log levels. |
|||
*/ |
|||
protected $logLevels = array( |
|||
Logger::DEBUG => Raven_Client::DEBUG, |
|||
Logger::INFO => Raven_Client::INFO, |
|||
Logger::NOTICE => Raven_Client::INFO, |
|||
Logger::WARNING => Raven_Client::WARNING, |
|||
Logger::ERROR => Raven_Client::ERROR, |
|||
Logger::CRITICAL => Raven_Client::FATAL, |
|||
Logger::ALERT => Raven_Client::FATAL, |
|||
Logger::EMERGENCY => Raven_Client::FATAL, |
|||
); |
|||
|
|||
/** |
|||
* @var string should represent the current version of the calling |
|||
* software. Can be any string (git commit, version number) |
|||
*/ |
|||
protected $release; |
|||
|
|||
/** |
|||
* @var Raven_Client the client object that sends the message to the server |
|||
*/ |
|||
protected $ravenClient; |
|||
|
|||
/** |
|||
* @var LineFormatter The formatter to use for the logs generated via handleBatch() |
|||
*/ |
|||
protected $batchFormatter; |
|||
|
|||
/** |
|||
* @param Raven_Client $ravenClient |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(Raven_Client $ravenClient, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->ravenClient = $ravenClient; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
$level = $this->level; |
|||
|
|||
// filter records based on their level |
|||
$records = array_filter($records, function ($record) use ($level) { |
|||
return $record['level'] >= $level; |
|||
}); |
|||
|
|||
if (!$records) { |
|||
return; |
|||
} |
|||
|
|||
// the record with the highest severity is the "main" one |
|||
$record = array_reduce($records, function ($highest, $record) { |
|||
if ($record['level'] > $highest['level']) { |
|||
return $record; |
|||
} |
|||
|
|||
return $highest; |
|||
}); |
|||
|
|||
// the other ones are added as a context item |
|||
$logs = array(); |
|||
foreach ($records as $r) { |
|||
$logs[] = $this->processRecord($r); |
|||
} |
|||
|
|||
if ($logs) { |
|||
$record['context']['logs'] = (string) $this->getBatchFormatter()->formatBatch($logs); |
|||
} |
|||
|
|||
$this->handle($record); |
|||
} |
|||
|
|||
/** |
|||
* Sets the formatter for the logs generated by handleBatch(). |
|||
* |
|||
* @param FormatterInterface $formatter |
|||
*/ |
|||
public function setBatchFormatter(FormatterInterface $formatter) |
|||
{ |
|||
$this->batchFormatter = $formatter; |
|||
} |
|||
|
|||
/** |
|||
* Gets the formatter for the logs generated by handleBatch(). |
|||
* |
|||
* @return FormatterInterface |
|||
*/ |
|||
public function getBatchFormatter() |
|||
{ |
|||
if (!$this->batchFormatter) { |
|||
$this->batchFormatter = $this->getDefaultBatchFormatter(); |
|||
} |
|||
|
|||
return $this->batchFormatter; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$previousUserContext = false; |
|||
$options = array(); |
|||
$options['level'] = $this->logLevels[$record['level']]; |
|||
$options['tags'] = array(); |
|||
if (!empty($record['extra']['tags'])) { |
|||
$options['tags'] = array_merge($options['tags'], $record['extra']['tags']); |
|||
unset($record['extra']['tags']); |
|||
} |
|||
if (!empty($record['context']['tags'])) { |
|||
$options['tags'] = array_merge($options['tags'], $record['context']['tags']); |
|||
unset($record['context']['tags']); |
|||
} |
|||
if (!empty($record['context']['fingerprint'])) { |
|||
$options['fingerprint'] = $record['context']['fingerprint']; |
|||
unset($record['context']['fingerprint']); |
|||
} |
|||
if (!empty($record['context']['logger'])) { |
|||
$options['logger'] = $record['context']['logger']; |
|||
unset($record['context']['logger']); |
|||
} else { |
|||
$options['logger'] = $record['channel']; |
|||
} |
|||
foreach ($this->getExtraParameters() as $key) { |
|||
foreach (array('extra', 'context') as $source) { |
|||
if (!empty($record[$source][$key])) { |
|||
$options[$key] = $record[$source][$key]; |
|||
unset($record[$source][$key]); |
|||
} |
|||
} |
|||
} |
|||
if (!empty($record['context'])) { |
|||
$options['extra']['context'] = $record['context']; |
|||
if (!empty($record['context']['user'])) { |
|||
$previousUserContext = $this->ravenClient->context->user; |
|||
$this->ravenClient->user_context($record['context']['user']); |
|||
unset($options['extra']['context']['user']); |
|||
} |
|||
} |
|||
if (!empty($record['extra'])) { |
|||
$options['extra']['extra'] = $record['extra']; |
|||
} |
|||
|
|||
if (!empty($this->release) && !isset($options['release'])) { |
|||
$options['release'] = $this->release; |
|||
} |
|||
|
|||
if (isset($record['context']['exception']) && ($record['context']['exception'] instanceof \Exception || (PHP_VERSION_ID >= 70000 && $record['context']['exception'] instanceof \Throwable))) { |
|||
$options['message'] = $record['formatted']; |
|||
$this->ravenClient->captureException($record['context']['exception'], $options); |
|||
} else { |
|||
$this->ravenClient->captureMessage($record['formatted'], array(), $options); |
|||
} |
|||
|
|||
if ($previousUserContext !== false) { |
|||
$this->ravenClient->user_context($previousUserContext); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter('[%channel%] %message%'); |
|||
} |
|||
|
|||
/** |
|||
* Gets the default formatter for the logs generated by handleBatch(). |
|||
* |
|||
* @return FormatterInterface |
|||
*/ |
|||
protected function getDefaultBatchFormatter() |
|||
{ |
|||
return new LineFormatter(); |
|||
} |
|||
|
|||
/** |
|||
* Gets extra parameters supported by Raven that can be found in "extra" and "context" |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getExtraParameters() |
|||
{ |
|||
return array('contexts', 'checksum', 'release', 'event_id'); |
|||
} |
|||
|
|||
/** |
|||
* @param string $value |
|||
* @return self |
|||
*/ |
|||
public function setRelease($value) |
|||
{ |
|||
$this->release = $value; |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\LineFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Logs to a Redis key using rpush |
|||
* |
|||
* usage example: |
|||
* |
|||
* $log = new Logger('application'); |
|||
* $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod"); |
|||
* $log->pushHandler($redis); |
|||
* |
|||
* @author Thomas Tourlourat <thomas@tourlourat.com> |
|||
*/ |
|||
class RedisHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $redisClient; |
|||
private $redisKey; |
|||
protected $capSize; |
|||
|
|||
/** |
|||
* @param \Predis\Client|\Redis $redis The redis instance |
|||
* @param string $key The key name to push records to |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param int $capSize Number of entries to limit list size to |
|||
*/ |
|||
public function __construct($redis, $key, $level = Logger::DEBUG, $bubble = true, $capSize = false) |
|||
{ |
|||
if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { |
|||
throw new \InvalidArgumentException('Predis\Client or Redis instance required'); |
|||
} |
|||
|
|||
$this->redisClient = $redis; |
|||
$this->redisKey = $key; |
|||
$this->capSize = $capSize; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if ($this->capSize) { |
|||
$this->writeCapped($record); |
|||
} else { |
|||
$this->redisClient->rpush($this->redisKey, $record["formatted"]); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Write and cap the collection |
|||
* Writes the record to the redis list and caps its |
|||
* |
|||
* @param array $record associative record array |
|||
* @return void |
|||
*/ |
|||
protected function writeCapped(array $record) |
|||
{ |
|||
if ($this->redisClient instanceof \Redis) { |
|||
$this->redisClient->multi() |
|||
->rpush($this->redisKey, $record["formatted"]) |
|||
->ltrim($this->redisKey, -$this->capSize, -1) |
|||
->exec(); |
|||
} else { |
|||
$redisKey = $this->redisKey; |
|||
$capSize = $this->capSize; |
|||
$this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) { |
|||
$tx->rpush($redisKey, $record["formatted"]); |
|||
$tx->ltrim($redisKey, -$capSize, -1); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
protected function getDefaultFormatter() |
|||
{ |
|||
return new LineFormatter(); |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use RollbarNotifier; |
|||
use Exception; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Sends errors to Rollbar |
|||
* |
|||
* If the context data contains a `payload` key, that is used as an array |
|||
* of payload options to RollbarNotifier's report_message/report_exception methods. |
|||
* |
|||
* Rollbar's context info will contain the context + extra keys from the log record |
|||
* merged, and then on top of that a few keys: |
|||
* |
|||
* - level (rollbar level name) |
|||
* - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8) |
|||
* - channel |
|||
* - datetime (unix timestamp) |
|||
* |
|||
* @author Paul Statezny <paulstatezny@gmail.com> |
|||
*/ |
|||
class RollbarHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Rollbar notifier |
|||
* |
|||
* @var RollbarNotifier |
|||
*/ |
|||
protected $rollbarNotifier; |
|||
|
|||
protected $levelMap = array( |
|||
Logger::DEBUG => 'debug', |
|||
Logger::INFO => 'info', |
|||
Logger::NOTICE => 'info', |
|||
Logger::WARNING => 'warning', |
|||
Logger::ERROR => 'error', |
|||
Logger::CRITICAL => 'critical', |
|||
Logger::ALERT => 'critical', |
|||
Logger::EMERGENCY => 'critical', |
|||
); |
|||
|
|||
/** |
|||
* Records whether any log records have been added since the last flush of the rollbar notifier |
|||
* |
|||
* @var bool |
|||
*/ |
|||
private $hasRecords = false; |
|||
|
|||
protected $initialized = false; |
|||
|
|||
/** |
|||
* @param RollbarNotifier $rollbarNotifier RollbarNotifier object constructed with valid token |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(RollbarNotifier $rollbarNotifier, $level = Logger::ERROR, $bubble = true) |
|||
{ |
|||
$this->rollbarNotifier = $rollbarNotifier; |
|||
|
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if (!$this->initialized) { |
|||
// __destructor() doesn't get called on Fatal errors |
|||
register_shutdown_function(array($this, 'close')); |
|||
$this->initialized = true; |
|||
} |
|||
|
|||
$context = $record['context']; |
|||
$payload = array(); |
|||
if (isset($context['payload'])) { |
|||
$payload = $context['payload']; |
|||
unset($context['payload']); |
|||
} |
|||
$context = array_merge($context, $record['extra'], array( |
|||
'level' => $this->levelMap[$record['level']], |
|||
'monolog_level' => $record['level_name'], |
|||
'channel' => $record['channel'], |
|||
'datetime' => $record['datetime']->format('U'), |
|||
)); |
|||
|
|||
if (isset($context['exception']) && $context['exception'] instanceof Exception) { |
|||
$payload['level'] = $context['level']; |
|||
$exception = $context['exception']; |
|||
unset($context['exception']); |
|||
|
|||
$this->rollbarNotifier->report_exception($exception, $context, $payload); |
|||
} else { |
|||
$this->rollbarNotifier->report_message( |
|||
$record['message'], |
|||
$context['level'], |
|||
$context, |
|||
$payload |
|||
); |
|||
} |
|||
|
|||
$this->hasRecords = true; |
|||
} |
|||
|
|||
public function flush() |
|||
{ |
|||
if ($this->hasRecords) { |
|||
$this->rollbarNotifier->flush(); |
|||
$this->hasRecords = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$this->flush(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
$this->flush(); |
|||
|
|||
parent::reset(); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Stores logs to files that are rotated every day and a limited number of files are kept. |
|||
* |
|||
* This rotation is only intended to be used as a workaround. Using logrotate to |
|||
* handle the rotation is strongly encouraged when you can use it. |
|||
* |
|||
* @author Christophe Coevoet <stof@notk.org> |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class RotatingFileHandler extends StreamHandler |
|||
{ |
|||
const FILE_PER_DAY = 'Y-m-d'; |
|||
const FILE_PER_MONTH = 'Y-m'; |
|||
const FILE_PER_YEAR = 'Y'; |
|||
|
|||
protected $filename; |
|||
protected $maxFiles; |
|||
protected $mustRotate; |
|||
protected $nextRotation; |
|||
protected $filenameFormat; |
|||
protected $dateFormat; |
|||
|
|||
/** |
|||
* @param string $filename |
|||
* @param int $maxFiles The maximal amount of files to keep (0 means unlimited) |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) |
|||
* @param bool $useLocking Try to lock log file before doing any writes |
|||
*/ |
|||
public function __construct($filename, $maxFiles = 0, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false) |
|||
{ |
|||
$this->filename = $filename; |
|||
$this->maxFiles = (int) $maxFiles; |
|||
$this->nextRotation = new \DateTime('tomorrow'); |
|||
$this->filenameFormat = '{filename}-{date}'; |
|||
$this->dateFormat = 'Y-m-d'; |
|||
|
|||
parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
parent::close(); |
|||
|
|||
if (true === $this->mustRotate) { |
|||
$this->rotate(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
parent::reset(); |
|||
|
|||
if (true === $this->mustRotate) { |
|||
$this->rotate(); |
|||
} |
|||
} |
|||
|
|||
public function setFilenameFormat($filenameFormat, $dateFormat) |
|||
{ |
|||
if (!preg_match('{^Y(([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) { |
|||
trigger_error( |
|||
'Invalid date format - format must be one of '. |
|||
'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '. |
|||
'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '. |
|||
'date formats using slashes, underscores and/or dots instead of dashes.', |
|||
E_USER_DEPRECATED |
|||
); |
|||
} |
|||
if (substr_count($filenameFormat, '{date}') === 0) { |
|||
trigger_error( |
|||
'Invalid filename format - format should contain at least `{date}`, because otherwise rotating is impossible.', |
|||
E_USER_DEPRECATED |
|||
); |
|||
} |
|||
$this->filenameFormat = $filenameFormat; |
|||
$this->dateFormat = $dateFormat; |
|||
$this->url = $this->getTimedFilename(); |
|||
$this->close(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
// on the first record written, if the log is new, we should rotate (once per day) |
|||
if (null === $this->mustRotate) { |
|||
$this->mustRotate = !file_exists($this->url); |
|||
} |
|||
|
|||
if ($this->nextRotation < $record['datetime']) { |
|||
$this->mustRotate = true; |
|||
$this->close(); |
|||
} |
|||
|
|||
parent::write($record); |
|||
} |
|||
|
|||
/** |
|||
* Rotates the files. |
|||
*/ |
|||
protected function rotate() |
|||
{ |
|||
// update filename |
|||
$this->url = $this->getTimedFilename(); |
|||
$this->nextRotation = new \DateTime('tomorrow'); |
|||
|
|||
// skip GC of old logs if files are unlimited |
|||
if (0 === $this->maxFiles) { |
|||
return; |
|||
} |
|||
|
|||
$logFiles = glob($this->getGlobPattern()); |
|||
if ($this->maxFiles >= count($logFiles)) { |
|||
// no files to remove |
|||
return; |
|||
} |
|||
|
|||
// Sorting the files by name to remove the older ones |
|||
usort($logFiles, function ($a, $b) { |
|||
return strcmp($b, $a); |
|||
}); |
|||
|
|||
foreach (array_slice($logFiles, $this->maxFiles) as $file) { |
|||
if (is_writable($file)) { |
|||
// suppress errors here as unlink() might fail if two processes |
|||
// are cleaning up/rotating at the same time |
|||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {}); |
|||
unlink($file); |
|||
restore_error_handler(); |
|||
} |
|||
} |
|||
|
|||
$this->mustRotate = false; |
|||
} |
|||
|
|||
protected function getTimedFilename() |
|||
{ |
|||
$fileInfo = pathinfo($this->filename); |
|||
$timedFilename = str_replace( |
|||
array('{filename}', '{date}'), |
|||
array($fileInfo['filename'], date($this->dateFormat)), |
|||
$fileInfo['dirname'] . '/' . $this->filenameFormat |
|||
); |
|||
|
|||
if (!empty($fileInfo['extension'])) { |
|||
$timedFilename .= '.'.$fileInfo['extension']; |
|||
} |
|||
|
|||
return $timedFilename; |
|||
} |
|||
|
|||
protected function getGlobPattern() |
|||
{ |
|||
$fileInfo = pathinfo($this->filename); |
|||
$glob = str_replace( |
|||
array('{filename}', '{date}'), |
|||
array($fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'), |
|||
$fileInfo['dirname'] . '/' . $this->filenameFormat |
|||
); |
|||
if (!empty($fileInfo['extension'])) { |
|||
$glob .= '.'.$fileInfo['extension']; |
|||
} |
|||
|
|||
return $glob; |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
/** |
|||
* Sampling handler |
|||
* |
|||
* A sampled event stream can be useful for logging high frequency events in |
|||
* a production environment where you only need an idea of what is happening |
|||
* and are not concerned with capturing every occurrence. Since the decision to |
|||
* handle or not handle a particular event is determined randomly, the |
|||
* resulting sampled log is not guaranteed to contain 1/N of the events that |
|||
* occurred in the application, but based on the Law of large numbers, it will |
|||
* tend to be close to this ratio with a large number of attempts. |
|||
* |
|||
* @author Bryan Davis <bd808@wikimedia.org> |
|||
* @author Kunal Mehta <legoktm@gmail.com> |
|||
*/ |
|||
class SamplingHandler extends AbstractHandler |
|||
{ |
|||
/** |
|||
* @var callable|HandlerInterface $handler |
|||
*/ |
|||
protected $handler; |
|||
|
|||
/** |
|||
* @var int $factor |
|||
*/ |
|||
protected $factor; |
|||
|
|||
/** |
|||
* @param callable|HandlerInterface $handler Handler or factory callable($record, $fingersCrossedHandler). |
|||
* @param int $factor Sample factor |
|||
*/ |
|||
public function __construct($handler, $factor) |
|||
{ |
|||
parent::__construct(); |
|||
$this->handler = $handler; |
|||
$this->factor = $factor; |
|||
|
|||
if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { |
|||
throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); |
|||
} |
|||
} |
|||
|
|||
public function isHandling(array $record) |
|||
{ |
|||
return $this->handler->isHandling($record); |
|||
} |
|||
|
|||
public function handle(array $record) |
|||
{ |
|||
if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) { |
|||
// The same logic as in FingersCrossedHandler |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
$this->handler = call_user_func($this->handler, $record, $this); |
|||
if (!$this->handler instanceof HandlerInterface) { |
|||
throw new \RuntimeException("The factory callable should return a HandlerInterface"); |
|||
} |
|||
} |
|||
|
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
$this->handler->handle($record); |
|||
} |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
} |
|||
@ -0,0 +1,294 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\Slack; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\NormalizerFormatter; |
|||
use Monolog\Formatter\FormatterInterface; |
|||
|
|||
/** |
|||
* Slack record utility helping to log to Slack webhooks or API. |
|||
* |
|||
* @author Greg Kedzierski <greg@gregkedzierski.com> |
|||
* @author Haralan Dobrev <hkdobrev@gmail.com> |
|||
* @see https://api.slack.com/incoming-webhooks |
|||
* @see https://api.slack.com/docs/message-attachments |
|||
*/ |
|||
class SlackRecord |
|||
{ |
|||
const COLOR_DANGER = 'danger'; |
|||
|
|||
const COLOR_WARNING = 'warning'; |
|||
|
|||
const COLOR_GOOD = 'good'; |
|||
|
|||
const COLOR_DEFAULT = '#e3e4e6'; |
|||
|
|||
/** |
|||
* Slack channel (encoded ID or name) |
|||
* @var string|null |
|||
*/ |
|||
private $channel; |
|||
|
|||
/** |
|||
* Name of a bot |
|||
* @var string|null |
|||
*/ |
|||
private $username; |
|||
|
|||
/** |
|||
* User icon e.g. 'ghost', 'http://example.com/user.png' |
|||
* @var string |
|||
*/ |
|||
private $userIcon; |
|||
|
|||
/** |
|||
* Whether the message should be added to Slack as attachment (plain text otherwise) |
|||
* @var bool |
|||
*/ |
|||
private $useAttachment; |
|||
|
|||
/** |
|||
* Whether the the context/extra messages added to Slack as attachments are in a short style |
|||
* @var bool |
|||
*/ |
|||
private $useShortAttachment; |
|||
|
|||
/** |
|||
* Whether the attachment should include context and extra data |
|||
* @var bool |
|||
*/ |
|||
private $includeContextAndExtra; |
|||
|
|||
/** |
|||
* Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] |
|||
* @var array |
|||
*/ |
|||
private $excludeFields; |
|||
|
|||
/** |
|||
* @var FormatterInterface |
|||
*/ |
|||
private $formatter; |
|||
|
|||
/** |
|||
* @var NormalizerFormatter |
|||
*/ |
|||
private $normalizerFormatter; |
|||
|
|||
public function __construct($channel = null, $username = null, $useAttachment = true, $userIcon = null, $useShortAttachment = false, $includeContextAndExtra = false, array $excludeFields = array(), FormatterInterface $formatter = null) |
|||
{ |
|||
$this->channel = $channel; |
|||
$this->username = $username; |
|||
$this->userIcon = trim($userIcon, ':'); |
|||
$this->useAttachment = $useAttachment; |
|||
$this->useShortAttachment = $useShortAttachment; |
|||
$this->includeContextAndExtra = $includeContextAndExtra; |
|||
$this->excludeFields = $excludeFields; |
|||
$this->formatter = $formatter; |
|||
|
|||
if ($this->includeContextAndExtra) { |
|||
$this->normalizerFormatter = new NormalizerFormatter(); |
|||
} |
|||
} |
|||
|
|||
public function getSlackData(array $record) |
|||
{ |
|||
$dataArray = array(); |
|||
$record = $this->excludeFields($record); |
|||
|
|||
if ($this->username) { |
|||
$dataArray['username'] = $this->username; |
|||
} |
|||
|
|||
if ($this->channel) { |
|||
$dataArray['channel'] = $this->channel; |
|||
} |
|||
|
|||
if ($this->formatter && !$this->useAttachment) { |
|||
$message = $this->formatter->format($record); |
|||
} else { |
|||
$message = $record['message']; |
|||
} |
|||
|
|||
if ($this->useAttachment) { |
|||
$attachment = array( |
|||
'fallback' => $message, |
|||
'text' => $message, |
|||
'color' => $this->getAttachmentColor($record['level']), |
|||
'fields' => array(), |
|||
'mrkdwn_in' => array('fields'), |
|||
'ts' => $record['datetime']->getTimestamp() |
|||
); |
|||
|
|||
if ($this->useShortAttachment) { |
|||
$attachment['title'] = $record['level_name']; |
|||
} else { |
|||
$attachment['title'] = 'Message'; |
|||
$attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']); |
|||
} |
|||
|
|||
|
|||
if ($this->includeContextAndExtra) { |
|||
foreach (array('extra', 'context') as $key) { |
|||
if (empty($record[$key])) { |
|||
continue; |
|||
} |
|||
|
|||
if ($this->useShortAttachment) { |
|||
$attachment['fields'][] = $this->generateAttachmentField( |
|||
$key, |
|||
$record[$key] |
|||
); |
|||
} else { |
|||
// Add all extra fields as individual fields in attachment |
|||
$attachment['fields'] = array_merge( |
|||
$attachment['fields'], |
|||
$this->generateAttachmentFields($record[$key]) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
$dataArray['attachments'] = array($attachment); |
|||
} else { |
|||
$dataArray['text'] = $message; |
|||
} |
|||
|
|||
if ($this->userIcon) { |
|||
if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) { |
|||
$dataArray['icon_url'] = $this->userIcon; |
|||
} else { |
|||
$dataArray['icon_emoji'] = ":{$this->userIcon}:"; |
|||
} |
|||
} |
|||
|
|||
return $dataArray; |
|||
} |
|||
|
|||
/** |
|||
* Returned a Slack message attachment color associated with |
|||
* provided level. |
|||
* |
|||
* @param int $level |
|||
* @return string |
|||
*/ |
|||
public function getAttachmentColor($level) |
|||
{ |
|||
switch (true) { |
|||
case $level >= Logger::ERROR: |
|||
return self::COLOR_DANGER; |
|||
case $level >= Logger::WARNING: |
|||
return self::COLOR_WARNING; |
|||
case $level >= Logger::INFO: |
|||
return self::COLOR_GOOD; |
|||
default: |
|||
return self::COLOR_DEFAULT; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Stringifies an array of key/value pairs to be used in attachment fields |
|||
* |
|||
* @param array $fields |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function stringify($fields) |
|||
{ |
|||
$normalized = $this->normalizerFormatter->format($fields); |
|||
$prettyPrintFlag = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 128; |
|||
|
|||
$hasSecondDimension = count(array_filter($normalized, 'is_array')); |
|||
$hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric')); |
|||
|
|||
return $hasSecondDimension || $hasNonNumericKeys |
|||
? json_encode($normalized, $prettyPrintFlag) |
|||
: json_encode($normalized); |
|||
} |
|||
|
|||
/** |
|||
* Sets the formatter |
|||
* |
|||
* @param FormatterInterface $formatter |
|||
*/ |
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
$this->formatter = $formatter; |
|||
} |
|||
|
|||
/** |
|||
* Generates attachment field |
|||
* |
|||
* @param string $title |
|||
* @param string|array $value |
|||
* |
|||
* @return array |
|||
*/ |
|||
private function generateAttachmentField($title, $value) |
|||
{ |
|||
$value = is_array($value) |
|||
? sprintf('```%s```', $this->stringify($value)) |
|||
: $value; |
|||
|
|||
return array( |
|||
'title' => ucfirst($title), |
|||
'value' => $value, |
|||
'short' => false |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Generates a collection of attachment fields from array |
|||
* |
|||
* @param array $data |
|||
* |
|||
* @return array |
|||
*/ |
|||
private function generateAttachmentFields(array $data) |
|||
{ |
|||
$fields = array(); |
|||
foreach ($this->normalizerFormatter->format($data) as $key => $value) { |
|||
$fields[] = $this->generateAttachmentField($key, $value); |
|||
} |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* Get a copy of record with fields excluded according to $this->excludeFields |
|||
* |
|||
* @param array $record |
|||
* |
|||
* @return array |
|||
*/ |
|||
private function excludeFields(array $record) |
|||
{ |
|||
foreach ($this->excludeFields as $field) { |
|||
$keys = explode('.', $field); |
|||
$node = &$record; |
|||
$lastKey = end($keys); |
|||
foreach ($keys as $key) { |
|||
if (!isset($node[$key])) { |
|||
break; |
|||
} |
|||
if ($lastKey === $key) { |
|||
unset($node[$key]); |
|||
break; |
|||
} |
|||
$node = &$node[$key]; |
|||
} |
|||
} |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Logger; |
|||
use Monolog\Handler\Slack\SlackRecord; |
|||
|
|||
/** |
|||
* Sends notifications through Slack API |
|||
* |
|||
* @author Greg Kedzierski <greg@gregkedzierski.com> |
|||
* @see https://api.slack.com/ |
|||
*/ |
|||
class SlackHandler extends SocketHandler |
|||
{ |
|||
/** |
|||
* Slack API token |
|||
* @var string |
|||
*/ |
|||
private $token; |
|||
|
|||
/** |
|||
* Instance of the SlackRecord util class preparing data for Slack API. |
|||
* @var SlackRecord |
|||
*/ |
|||
private $slackRecord; |
|||
|
|||
/** |
|||
* @param string $token Slack API token |
|||
* @param string $channel Slack channel (encoded ID or name) |
|||
* @param string|null $username Name of a bot |
|||
* @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) |
|||
* @param string|null $iconEmoji The emoji name to use (or null) |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style |
|||
* @param bool $includeContextAndExtra Whether the attachment should include context and extra data |
|||
* @param array $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] |
|||
* @throws MissingExtensionException If no OpenSSL PHP extension configured |
|||
*/ |
|||
public function __construct($token, $channel, $username = null, $useAttachment = true, $iconEmoji = null, $level = Logger::CRITICAL, $bubble = true, $useShortAttachment = false, $includeContextAndExtra = false, array $excludeFields = array()) |
|||
{ |
|||
if (!extension_loaded('openssl')) { |
|||
throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); |
|||
} |
|||
|
|||
parent::__construct('ssl://slack.com:443', $level, $bubble); |
|||
|
|||
$this->slackRecord = new SlackRecord( |
|||
$channel, |
|||
$username, |
|||
$useAttachment, |
|||
$iconEmoji, |
|||
$useShortAttachment, |
|||
$includeContextAndExtra, |
|||
$excludeFields, |
|||
$this->formatter |
|||
); |
|||
|
|||
$this->token = $token; |
|||
} |
|||
|
|||
public function getSlackRecord() |
|||
{ |
|||
return $this->slackRecord; |
|||
} |
|||
|
|||
public function getToken() |
|||
{ |
|||
return $this->token; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
protected function generateDataStream($record) |
|||
{ |
|||
$content = $this->buildContent($record); |
|||
|
|||
return $this->buildHeader($content) . $content; |
|||
} |
|||
|
|||
/** |
|||
* Builds the body of API call |
|||
* |
|||
* @param array $record |
|||
* @return string |
|||
*/ |
|||
private function buildContent($record) |
|||
{ |
|||
$dataArray = $this->prepareContentData($record); |
|||
|
|||
return http_build_query($dataArray); |
|||
} |
|||
|
|||
/** |
|||
* Prepares content data |
|||
* |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
protected function prepareContentData($record) |
|||
{ |
|||
$dataArray = $this->slackRecord->getSlackData($record); |
|||
$dataArray['token'] = $this->token; |
|||
|
|||
if (!empty($dataArray['attachments'])) { |
|||
$dataArray['attachments'] = json_encode($dataArray['attachments']); |
|||
} |
|||
|
|||
return $dataArray; |
|||
} |
|||
|
|||
/** |
|||
* Builds the header of the API Call |
|||
* |
|||
* @param string $content |
|||
* @return string |
|||
*/ |
|||
private function buildHeader($content) |
|||
{ |
|||
$header = "POST /api/chat.postMessage HTTP/1.1\r\n"; |
|||
$header .= "Host: slack.com\r\n"; |
|||
$header .= "Content-Type: application/x-www-form-urlencoded\r\n"; |
|||
$header .= "Content-Length: " . strlen($content) . "\r\n"; |
|||
$header .= "\r\n"; |
|||
|
|||
return $header; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
parent::write($record); |
|||
$this->finalizeWrite(); |
|||
} |
|||
|
|||
/** |
|||
* Finalizes the request by reading some bytes and then closing the socket |
|||
* |
|||
* If we do not read some but close the socket too early, slack sometimes |
|||
* drops the request entirely. |
|||
*/ |
|||
protected function finalizeWrite() |
|||
{ |
|||
$res = $this->getResource(); |
|||
if (is_resource($res)) { |
|||
@fread($res, 2048); |
|||
} |
|||
$this->closeSocket(); |
|||
} |
|||
|
|||
/** |
|||
* Returned a Slack message attachment color associated with |
|||
* provided level. |
|||
* |
|||
* @param int $level |
|||
* @return string |
|||
* @deprecated Use underlying SlackRecord instead |
|||
*/ |
|||
protected function getAttachmentColor($level) |
|||
{ |
|||
trigger_error( |
|||
'SlackHandler::getAttachmentColor() is deprecated. Use underlying SlackRecord instead.', |
|||
E_USER_DEPRECATED |
|||
); |
|||
|
|||
return $this->slackRecord->getAttachmentColor($level); |
|||
} |
|||
|
|||
/** |
|||
* Stringifies an array of key/value pairs to be used in attachment fields |
|||
* |
|||
* @param array $fields |
|||
* @return string |
|||
* @deprecated Use underlying SlackRecord instead |
|||
*/ |
|||
protected function stringify($fields) |
|||
{ |
|||
trigger_error( |
|||
'SlackHandler::stringify() is deprecated. Use underlying SlackRecord instead.', |
|||
E_USER_DEPRECATED |
|||
); |
|||
|
|||
return $this->slackRecord->stringify($fields); |
|||
} |
|||
|
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
parent::setFormatter($formatter); |
|||
$this->slackRecord->setFormatter($formatter); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
public function getFormatter() |
|||
{ |
|||
$formatter = parent::getFormatter(); |
|||
$this->slackRecord->setFormatter($formatter); |
|||
|
|||
return $formatter; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Logger; |
|||
use Monolog\Handler\Slack\SlackRecord; |
|||
|
|||
/** |
|||
* Sends notifications through Slack Webhooks |
|||
* |
|||
* @author Haralan Dobrev <hkdobrev@gmail.com> |
|||
* @see https://api.slack.com/incoming-webhooks |
|||
*/ |
|||
class SlackWebhookHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Slack Webhook token |
|||
* @var string |
|||
*/ |
|||
private $webhookUrl; |
|||
|
|||
/** |
|||
* Instance of the SlackRecord util class preparing data for Slack API. |
|||
* @var SlackRecord |
|||
*/ |
|||
private $slackRecord; |
|||
|
|||
/** |
|||
* @param string $webhookUrl Slack Webhook URL |
|||
* @param string|null $channel Slack channel (encoded ID or name) |
|||
* @param string|null $username Name of a bot |
|||
* @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) |
|||
* @param string|null $iconEmoji The emoji name to use (or null) |
|||
* @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style |
|||
* @param bool $includeContextAndExtra Whether the attachment should include context and extra data |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param array $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] |
|||
*/ |
|||
public function __construct($webhookUrl, $channel = null, $username = null, $useAttachment = true, $iconEmoji = null, $useShortAttachment = false, $includeContextAndExtra = false, $level = Logger::CRITICAL, $bubble = true, array $excludeFields = array()) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->webhookUrl = $webhookUrl; |
|||
|
|||
$this->slackRecord = new SlackRecord( |
|||
$channel, |
|||
$username, |
|||
$useAttachment, |
|||
$iconEmoji, |
|||
$useShortAttachment, |
|||
$includeContextAndExtra, |
|||
$excludeFields, |
|||
$this->formatter |
|||
); |
|||
} |
|||
|
|||
public function getSlackRecord() |
|||
{ |
|||
return $this->slackRecord; |
|||
} |
|||
|
|||
public function getWebhookUrl() |
|||
{ |
|||
return $this->webhookUrl; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$postData = $this->slackRecord->getSlackData($record); |
|||
$postString = json_encode($postData); |
|||
|
|||
$ch = curl_init(); |
|||
$options = array( |
|||
CURLOPT_URL => $this->webhookUrl, |
|||
CURLOPT_POST => true, |
|||
CURLOPT_RETURNTRANSFER => true, |
|||
CURLOPT_HTTPHEADER => array('Content-type: application/json'), |
|||
CURLOPT_POSTFIELDS => $postString |
|||
); |
|||
if (defined('CURLOPT_SAFE_UPLOAD')) { |
|||
$options[CURLOPT_SAFE_UPLOAD] = true; |
|||
} |
|||
|
|||
curl_setopt_array($ch, $options); |
|||
|
|||
Curl\Util::execute($ch); |
|||
} |
|||
|
|||
public function setFormatter(FormatterInterface $formatter) |
|||
{ |
|||
parent::setFormatter($formatter); |
|||
$this->slackRecord->setFormatter($formatter); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
public function getFormatter() |
|||
{ |
|||
$formatter = parent::getFormatter(); |
|||
$this->slackRecord->setFormatter($formatter); |
|||
|
|||
return $formatter; |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Sends notifications through Slack's Slackbot |
|||
* |
|||
* @author Haralan Dobrev <hkdobrev@gmail.com> |
|||
* @see https://slack.com/apps/A0F81R8ET-slackbot |
|||
*/ |
|||
class SlackbotHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* The slug of the Slack team |
|||
* @var string |
|||
*/ |
|||
private $slackTeam; |
|||
|
|||
/** |
|||
* Slackbot token |
|||
* @var string |
|||
*/ |
|||
private $token; |
|||
|
|||
/** |
|||
* Slack channel name |
|||
* @var string |
|||
*/ |
|||
private $channel; |
|||
|
|||
/** |
|||
* @param string $slackTeam Slack team slug |
|||
* @param string $token Slackbot token |
|||
* @param string $channel Slack channel (encoded ID or name) |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($slackTeam, $token, $channel, $level = Logger::CRITICAL, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->slackTeam = $slackTeam; |
|||
$this->token = $token; |
|||
$this->channel = $channel; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @param array $record |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$slackbotUrl = sprintf( |
|||
'https://%s.slack.com/services/hooks/slackbot?token=%s&channel=%s', |
|||
$this->slackTeam, |
|||
$this->token, |
|||
$this->channel |
|||
); |
|||
|
|||
$ch = curl_init(); |
|||
curl_setopt($ch, CURLOPT_URL, $slackbotUrl); |
|||
curl_setopt($ch, CURLOPT_POST, true); |
|||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
|||
curl_setopt($ch, CURLOPT_POSTFIELDS, $record['message']); |
|||
|
|||
Curl\Util::execute($ch); |
|||
} |
|||
} |
|||
@ -0,0 +1,385 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Stores to any socket - uses fsockopen() or pfsockopen(). |
|||
* |
|||
* @author Pablo de Leon Belloc <pablolb@gmail.com> |
|||
* @see http://php.net/manual/en/function.fsockopen.php |
|||
*/ |
|||
class SocketHandler extends AbstractProcessingHandler |
|||
{ |
|||
private $connectionString; |
|||
private $connectionTimeout; |
|||
private $resource; |
|||
private $timeout = 0; |
|||
private $writingTimeout = 10; |
|||
private $lastSentBytes = null; |
|||
private $chunkSize = null; |
|||
private $persistent = false; |
|||
private $errno; |
|||
private $errstr; |
|||
private $lastWritingAt; |
|||
|
|||
/** |
|||
* @param string $connectionString Socket connection string |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
$this->connectionString = $connectionString; |
|||
$this->connectionTimeout = (float) ini_get('default_socket_timeout'); |
|||
} |
|||
|
|||
/** |
|||
* Connect (if necessary) and write to the socket |
|||
* |
|||
* @param array $record |
|||
* |
|||
* @throws \UnexpectedValueException |
|||
* @throws \RuntimeException |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->connectIfNotConnected(); |
|||
$data = $this->generateDataStream($record); |
|||
$this->writeToSocket($data); |
|||
} |
|||
|
|||
/** |
|||
* We will not close a PersistentSocket instance so it can be reused in other requests. |
|||
*/ |
|||
public function close() |
|||
{ |
|||
if (!$this->isPersistent()) { |
|||
$this->closeSocket(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Close socket, if open |
|||
*/ |
|||
public function closeSocket() |
|||
{ |
|||
if (is_resource($this->resource)) { |
|||
fclose($this->resource); |
|||
$this->resource = null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Set socket connection to nbe persistent. It only has effect before the connection is initiated. |
|||
* |
|||
* @param bool $persistent |
|||
*/ |
|||
public function setPersistent($persistent) |
|||
{ |
|||
$this->persistent = (bool) $persistent; |
|||
} |
|||
|
|||
/** |
|||
* Set connection timeout. Only has effect before we connect. |
|||
* |
|||
* @param float $seconds |
|||
* |
|||
* @see http://php.net/manual/en/function.fsockopen.php |
|||
*/ |
|||
public function setConnectionTimeout($seconds) |
|||
{ |
|||
$this->validateTimeout($seconds); |
|||
$this->connectionTimeout = (float) $seconds; |
|||
} |
|||
|
|||
/** |
|||
* Set write timeout. Only has effect before we connect. |
|||
* |
|||
* @param float $seconds |
|||
* |
|||
* @see http://php.net/manual/en/function.stream-set-timeout.php |
|||
*/ |
|||
public function setTimeout($seconds) |
|||
{ |
|||
$this->validateTimeout($seconds); |
|||
$this->timeout = (float) $seconds; |
|||
} |
|||
|
|||
/** |
|||
* Set writing timeout. Only has effect during connection in the writing cycle. |
|||
* |
|||
* @param float $seconds 0 for no timeout |
|||
*/ |
|||
public function setWritingTimeout($seconds) |
|||
{ |
|||
$this->validateTimeout($seconds); |
|||
$this->writingTimeout = (float) $seconds; |
|||
} |
|||
|
|||
/** |
|||
* Set chunk size. Only has effect during connection in the writing cycle. |
|||
* |
|||
* @param float $bytes |
|||
*/ |
|||
public function setChunkSize($bytes) |
|||
{ |
|||
$this->chunkSize = $bytes; |
|||
} |
|||
|
|||
/** |
|||
* Get current connection string |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getConnectionString() |
|||
{ |
|||
return $this->connectionString; |
|||
} |
|||
|
|||
/** |
|||
* Get persistent setting |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isPersistent() |
|||
{ |
|||
return $this->persistent; |
|||
} |
|||
|
|||
/** |
|||
* Get current connection timeout setting |
|||
* |
|||
* @return float |
|||
*/ |
|||
public function getConnectionTimeout() |
|||
{ |
|||
return $this->connectionTimeout; |
|||
} |
|||
|
|||
/** |
|||
* Get current in-transfer timeout |
|||
* |
|||
* @return float |
|||
*/ |
|||
public function getTimeout() |
|||
{ |
|||
return $this->timeout; |
|||
} |
|||
|
|||
/** |
|||
* Get current local writing timeout |
|||
* |
|||
* @return float |
|||
*/ |
|||
public function getWritingTimeout() |
|||
{ |
|||
return $this->writingTimeout; |
|||
} |
|||
|
|||
/** |
|||
* Get current chunk size |
|||
* |
|||
* @return float |
|||
*/ |
|||
public function getChunkSize() |
|||
{ |
|||
return $this->chunkSize; |
|||
} |
|||
|
|||
/** |
|||
* Check to see if the socket is currently available. |
|||
* |
|||
* UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isConnected() |
|||
{ |
|||
return is_resource($this->resource) |
|||
&& !feof($this->resource); // on TCP - other party can close connection. |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
*/ |
|||
protected function pfsockopen() |
|||
{ |
|||
return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
*/ |
|||
protected function fsockopen() |
|||
{ |
|||
return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
* |
|||
* @see http://php.net/manual/en/function.stream-set-timeout.php |
|||
*/ |
|||
protected function streamSetTimeout() |
|||
{ |
|||
$seconds = floor($this->timeout); |
|||
$microseconds = round(($this->timeout - $seconds) * 1e6); |
|||
|
|||
return stream_set_timeout($this->resource, $seconds, $microseconds); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
* |
|||
* @see http://php.net/manual/en/function.stream-set-chunk-size.php |
|||
*/ |
|||
protected function streamSetChunkSize() |
|||
{ |
|||
return stream_set_chunk_size($this->resource, $this->chunkSize); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
*/ |
|||
protected function fwrite($data) |
|||
{ |
|||
return @fwrite($this->resource, $data); |
|||
} |
|||
|
|||
/** |
|||
* Wrapper to allow mocking |
|||
*/ |
|||
protected function streamGetMetadata() |
|||
{ |
|||
return stream_get_meta_data($this->resource); |
|||
} |
|||
|
|||
private function validateTimeout($value) |
|||
{ |
|||
$ok = filter_var($value, FILTER_VALIDATE_FLOAT); |
|||
if ($ok === false || $value < 0) { |
|||
throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)"); |
|||
} |
|||
} |
|||
|
|||
private function connectIfNotConnected() |
|||
{ |
|||
if ($this->isConnected()) { |
|||
return; |
|||
} |
|||
$this->connect(); |
|||
} |
|||
|
|||
protected function generateDataStream($record) |
|||
{ |
|||
return (string) $record['formatted']; |
|||
} |
|||
|
|||
/** |
|||
* @return resource|null |
|||
*/ |
|||
protected function getResource() |
|||
{ |
|||
return $this->resource; |
|||
} |
|||
|
|||
private function connect() |
|||
{ |
|||
$this->createSocketResource(); |
|||
$this->setSocketTimeout(); |
|||
$this->setStreamChunkSize(); |
|||
} |
|||
|
|||
private function createSocketResource() |
|||
{ |
|||
if ($this->isPersistent()) { |
|||
$resource = $this->pfsockopen(); |
|||
} else { |
|||
$resource = $this->fsockopen(); |
|||
} |
|||
if (!$resource) { |
|||
throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)"); |
|||
} |
|||
$this->resource = $resource; |
|||
} |
|||
|
|||
private function setSocketTimeout() |
|||
{ |
|||
if (!$this->streamSetTimeout()) { |
|||
throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()"); |
|||
} |
|||
} |
|||
|
|||
private function setStreamChunkSize() |
|||
{ |
|||
if ($this->chunkSize && !$this->streamSetChunkSize()) { |
|||
throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); |
|||
} |
|||
} |
|||
|
|||
private function writeToSocket($data) |
|||
{ |
|||
$length = strlen($data); |
|||
$sent = 0; |
|||
$this->lastSentBytes = $sent; |
|||
while ($this->isConnected() && $sent < $length) { |
|||
if (0 == $sent) { |
|||
$chunk = $this->fwrite($data); |
|||
} else { |
|||
$chunk = $this->fwrite(substr($data, $sent)); |
|||
} |
|||
if ($chunk === false) { |
|||
throw new \RuntimeException("Could not write to socket"); |
|||
} |
|||
$sent += $chunk; |
|||
$socketInfo = $this->streamGetMetadata(); |
|||
if ($socketInfo['timed_out']) { |
|||
throw new \RuntimeException("Write timed-out"); |
|||
} |
|||
|
|||
if ($this->writingIsTimedOut($sent)) { |
|||
throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)"); |
|||
} |
|||
} |
|||
if (!$this->isConnected() && $sent < $length) { |
|||
throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); |
|||
} |
|||
} |
|||
|
|||
private function writingIsTimedOut($sent) |
|||
{ |
|||
$writingTimeout = (int) floor($this->writingTimeout); |
|||
if (0 === $writingTimeout) { |
|||
return false; |
|||
} |
|||
|
|||
if ($sent !== $this->lastSentBytes) { |
|||
$this->lastWritingAt = time(); |
|||
$this->lastSentBytes = $sent; |
|||
|
|||
return false; |
|||
} else { |
|||
usleep(100); |
|||
} |
|||
|
|||
if ((time() - $this->lastWritingAt) >= $writingTimeout) { |
|||
$this->closeSocket(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,176 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Stores to any stream resource |
|||
* |
|||
* Can be used to store into php://stderr, remote and local files, etc. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class StreamHandler extends AbstractProcessingHandler |
|||
{ |
|||
protected $stream; |
|||
protected $url; |
|||
private $errorMessage; |
|||
protected $filePermission; |
|||
protected $useLocking; |
|||
private $dirCreated; |
|||
|
|||
/** |
|||
* @param resource|string $stream |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) |
|||
* @param bool $useLocking Try to lock log file before doing any writes |
|||
* |
|||
* @throws \Exception If a missing directory is not buildable |
|||
* @throws \InvalidArgumentException If stream is not a resource or string |
|||
*/ |
|||
public function __construct($stream, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
if (is_resource($stream)) { |
|||
$this->stream = $stream; |
|||
} elseif (is_string($stream)) { |
|||
$this->url = $stream; |
|||
} else { |
|||
throw new \InvalidArgumentException('A stream must either be a resource or a string.'); |
|||
} |
|||
|
|||
$this->filePermission = $filePermission; |
|||
$this->useLocking = $useLocking; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
if ($this->url && is_resource($this->stream)) { |
|||
fclose($this->stream); |
|||
} |
|||
$this->stream = null; |
|||
} |
|||
|
|||
/** |
|||
* Return the currently active stream if it is open |
|||
* |
|||
* @return resource|null |
|||
*/ |
|||
public function getStream() |
|||
{ |
|||
return $this->stream; |
|||
} |
|||
|
|||
/** |
|||
* Return the stream URL if it was configured with a URL and not an active resource |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
public function getUrl() |
|||
{ |
|||
return $this->url; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if (!is_resource($this->stream)) { |
|||
if (null === $this->url || '' === $this->url) { |
|||
throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); |
|||
} |
|||
$this->createDir(); |
|||
$this->errorMessage = null; |
|||
set_error_handler(array($this, 'customErrorHandler')); |
|||
$this->stream = fopen($this->url, 'a'); |
|||
if ($this->filePermission !== null) { |
|||
@chmod($this->url, $this->filePermission); |
|||
} |
|||
restore_error_handler(); |
|||
if (!is_resource($this->stream)) { |
|||
$this->stream = null; |
|||
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url)); |
|||
} |
|||
} |
|||
|
|||
if ($this->useLocking) { |
|||
// ignoring errors here, there's not much we can do about them |
|||
flock($this->stream, LOCK_EX); |
|||
} |
|||
|
|||
$this->streamWrite($this->stream, $record); |
|||
|
|||
if ($this->useLocking) { |
|||
flock($this->stream, LOCK_UN); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Write to stream |
|||
* @param resource $stream |
|||
* @param array $record |
|||
*/ |
|||
protected function streamWrite($stream, array $record) |
|||
{ |
|||
fwrite($stream, (string) $record['formatted']); |
|||
} |
|||
|
|||
private function customErrorHandler($code, $msg) |
|||
{ |
|||
$this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); |
|||
} |
|||
|
|||
/** |
|||
* @param string $stream |
|||
* |
|||
* @return null|string |
|||
*/ |
|||
private function getDirFromStream($stream) |
|||
{ |
|||
$pos = strpos($stream, '://'); |
|||
if ($pos === false) { |
|||
return dirname($stream); |
|||
} |
|||
|
|||
if ('file://' === substr($stream, 0, 7)) { |
|||
return dirname(substr($stream, 7)); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
private function createDir() |
|||
{ |
|||
// Do not try to create dir if it has already been tried. |
|||
if ($this->dirCreated) { |
|||
return; |
|||
} |
|||
|
|||
$dir = $this->getDirFromStream($this->url); |
|||
if (null !== $dir && !is_dir($dir)) { |
|||
$this->errorMessage = null; |
|||
set_error_handler(array($this, 'customErrorHandler')); |
|||
$status = mkdir($dir, 0777, true); |
|||
restore_error_handler(); |
|||
if (false === $status && !is_dir($dir)) { |
|||
throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and its not buildable: '.$this->errorMessage, $dir)); |
|||
} |
|||
} |
|||
$this->dirCreated = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Formatter\FormatterInterface; |
|||
use Monolog\Formatter\LineFormatter; |
|||
use Swift; |
|||
|
|||
/** |
|||
* SwiftMailerHandler uses Swift_Mailer to send the emails |
|||
* |
|||
* @author Gyula Sallai |
|||
*/ |
|||
class SwiftMailerHandler extends MailHandler |
|||
{ |
|||
protected $mailer; |
|||
private $messageTemplate; |
|||
|
|||
/** |
|||
* @param \Swift_Mailer $mailer The mailer to use |
|||
* @param callable|\Swift_Message $message An example message for real messages, only the body will be replaced |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
*/ |
|||
public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, $bubble = true) |
|||
{ |
|||
parent::__construct($level, $bubble); |
|||
|
|||
$this->mailer = $mailer; |
|||
$this->messageTemplate = $message; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function send($content, array $records) |
|||
{ |
|||
$this->mailer->send($this->buildMessage($content, $records)); |
|||
} |
|||
|
|||
/** |
|||
* Gets the formatter for the Swift_Message subject. |
|||
* |
|||
* @param string $format The format of the subject |
|||
* @return FormatterInterface |
|||
*/ |
|||
protected function getSubjectFormatter($format) |
|||
{ |
|||
return new LineFormatter($format); |
|||
} |
|||
|
|||
/** |
|||
* Creates instance of Swift_Message to be sent |
|||
* |
|||
* @param string $content formatted email body to be sent |
|||
* @param array $records Log records that formed the content |
|||
* @return \Swift_Message |
|||
*/ |
|||
protected function buildMessage($content, array $records) |
|||
{ |
|||
$message = null; |
|||
if ($this->messageTemplate instanceof \Swift_Message) { |
|||
$message = clone $this->messageTemplate; |
|||
$message->generateId(); |
|||
} elseif (is_callable($this->messageTemplate)) { |
|||
$message = call_user_func($this->messageTemplate, $content, $records); |
|||
} |
|||
|
|||
if (!$message instanceof \Swift_Message) { |
|||
throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it'); |
|||
} |
|||
|
|||
if ($records) { |
|||
$subjectFormatter = $this->getSubjectFormatter($message->getSubject()); |
|||
$message->setSubject($subjectFormatter->format($this->getHighestRecord($records))); |
|||
} |
|||
|
|||
$message->setBody($content); |
|||
if (version_compare(Swift::VERSION, '6.0.0', '>=')) { |
|||
$message->setDate(new \DateTimeImmutable()); |
|||
} else { |
|||
$message->setDate(time()); |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
|
|||
/** |
|||
* BC getter, to be removed in 2.0 |
|||
*/ |
|||
public function __get($name) |
|||
{ |
|||
if ($name === 'message') { |
|||
trigger_error('SwiftMailerHandler->message is deprecated, use ->buildMessage() instead to retrieve the message', E_USER_DEPRECATED); |
|||
|
|||
return $this->buildMessage(null, array()); |
|||
} |
|||
|
|||
throw new \InvalidArgumentException('Invalid property '.$name); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Logs to syslog service. |
|||
* |
|||
* usage example: |
|||
* |
|||
* $log = new Logger('application'); |
|||
* $syslog = new SyslogHandler('myfacility', 'local6'); |
|||
* $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); |
|||
* $syslog->setFormatter($formatter); |
|||
* $log->pushHandler($syslog); |
|||
* |
|||
* @author Sven Paulus <sven@karlsruhe.org> |
|||
*/ |
|||
class SyslogHandler extends AbstractSyslogHandler |
|||
{ |
|||
protected $ident; |
|||
protected $logopts; |
|||
|
|||
/** |
|||
* @param string $ident |
|||
* @param mixed $facility |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param int $logopts Option flags for the openlog() call, defaults to LOG_PID |
|||
*/ |
|||
public function __construct($ident, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $logopts = LOG_PID) |
|||
{ |
|||
parent::__construct($facility, $level, $bubble); |
|||
|
|||
$this->ident = $ident; |
|||
$this->logopts = $logopts; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
closelog(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
if (!openlog($this->ident, $this->logopts, $this->facility)) { |
|||
throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"'); |
|||
} |
|||
syslog($this->logLevels[$record['level']], (string) $record['formatted']); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler\SyslogUdp; |
|||
|
|||
class UdpSocket |
|||
{ |
|||
const DATAGRAM_MAX_LENGTH = 65023; |
|||
|
|||
protected $ip; |
|||
protected $port; |
|||
protected $socket; |
|||
|
|||
public function __construct($ip, $port = 514) |
|||
{ |
|||
$this->ip = $ip; |
|||
$this->port = $port; |
|||
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); |
|||
} |
|||
|
|||
public function write($line, $header = "") |
|||
{ |
|||
$this->send($this->assembleMessage($line, $header)); |
|||
} |
|||
|
|||
public function close() |
|||
{ |
|||
if (is_resource($this->socket)) { |
|||
socket_close($this->socket); |
|||
$this->socket = null; |
|||
} |
|||
} |
|||
|
|||
protected function send($chunk) |
|||
{ |
|||
if (!is_resource($this->socket)) { |
|||
throw new \LogicException('The UdpSocket to '.$this->ip.':'.$this->port.' has been closed and can not be written to anymore'); |
|||
} |
|||
socket_sendto($this->socket, $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port); |
|||
} |
|||
|
|||
protected function assembleMessage($line, $header) |
|||
{ |
|||
$chunkSize = self::DATAGRAM_MAX_LENGTH - strlen($header); |
|||
|
|||
return $header . substr($line, 0, $chunkSize); |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Logger; |
|||
use Monolog\Handler\SyslogUdp\UdpSocket; |
|||
|
|||
/** |
|||
* A Handler for logging to a remote syslogd server. |
|||
* |
|||
* @author Jesper Skovgaard Nielsen <nulpunkt@gmail.com> |
|||
*/ |
|||
class SyslogUdpHandler extends AbstractSyslogHandler |
|||
{ |
|||
protected $socket; |
|||
protected $ident; |
|||
|
|||
/** |
|||
* @param string $host |
|||
* @param int $port |
|||
* @param mixed $facility |
|||
* @param int $level The minimum logging level at which this handler will be triggered |
|||
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not |
|||
* @param string $ident Program name or tag for each log message. |
|||
*/ |
|||
public function __construct($host, $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $ident = 'php') |
|||
{ |
|||
parent::__construct($facility, $level, $bubble); |
|||
|
|||
$this->ident = $ident; |
|||
|
|||
$this->socket = new UdpSocket($host, $port ?: 514); |
|||
} |
|||
|
|||
protected function write(array $record) |
|||
{ |
|||
$lines = $this->splitMessageIntoLines($record['formatted']); |
|||
|
|||
$header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']]); |
|||
|
|||
foreach ($lines as $line) { |
|||
$this->socket->write($line, $header); |
|||
} |
|||
} |
|||
|
|||
public function close() |
|||
{ |
|||
$this->socket->close(); |
|||
} |
|||
|
|||
private function splitMessageIntoLines($message) |
|||
{ |
|||
if (is_array($message)) { |
|||
$message = implode("\n", $message); |
|||
} |
|||
|
|||
return preg_split('/$\R?^/m', $message, -1, PREG_SPLIT_NO_EMPTY); |
|||
} |
|||
|
|||
/** |
|||
* Make common syslog header (see rfc5424) |
|||
*/ |
|||
protected function makeCommonSyslogHeader($severity) |
|||
{ |
|||
$priority = $severity + $this->facility; |
|||
|
|||
if (!$pid = getmypid()) { |
|||
$pid = '-'; |
|||
} |
|||
|
|||
if (!$hostname = gethostname()) { |
|||
$hostname = '-'; |
|||
} |
|||
|
|||
return "<$priority>1 " . |
|||
$this->getDateTime() . " " . |
|||
$hostname . " " . |
|||
$this->ident . " " . |
|||
$pid . " - - "; |
|||
} |
|||
|
|||
protected function getDateTime() |
|||
{ |
|||
return date(\DateTime::RFC3339); |
|||
} |
|||
|
|||
/** |
|||
* Inject your own socket, mainly used for testing |
|||
*/ |
|||
public function setSocket($socket) |
|||
{ |
|||
$this->socket = $socket; |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
/** |
|||
* Used for testing purposes. |
|||
* |
|||
* It records all records and gives you access to them for verification. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* @method bool hasEmergency($record) |
|||
* @method bool hasAlert($record) |
|||
* @method bool hasCritical($record) |
|||
* @method bool hasError($record) |
|||
* @method bool hasWarning($record) |
|||
* @method bool hasNotice($record) |
|||
* @method bool hasInfo($record) |
|||
* @method bool hasDebug($record) |
|||
* |
|||
* @method bool hasEmergencyRecords() |
|||
* @method bool hasAlertRecords() |
|||
* @method bool hasCriticalRecords() |
|||
* @method bool hasErrorRecords() |
|||
* @method bool hasWarningRecords() |
|||
* @method bool hasNoticeRecords() |
|||
* @method bool hasInfoRecords() |
|||
* @method bool hasDebugRecords() |
|||
* |
|||
* @method bool hasEmergencyThatContains($message) |
|||
* @method bool hasAlertThatContains($message) |
|||
* @method bool hasCriticalThatContains($message) |
|||
* @method bool hasErrorThatContains($message) |
|||
* @method bool hasWarningThatContains($message) |
|||
* @method bool hasNoticeThatContains($message) |
|||
* @method bool hasInfoThatContains($message) |
|||
* @method bool hasDebugThatContains($message) |
|||
* |
|||
* @method bool hasEmergencyThatMatches($message) |
|||
* @method bool hasAlertThatMatches($message) |
|||
* @method bool hasCriticalThatMatches($message) |
|||
* @method bool hasErrorThatMatches($message) |
|||
* @method bool hasWarningThatMatches($message) |
|||
* @method bool hasNoticeThatMatches($message) |
|||
* @method bool hasInfoThatMatches($message) |
|||
* @method bool hasDebugThatMatches($message) |
|||
* |
|||
* @method bool hasEmergencyThatPasses($message) |
|||
* @method bool hasAlertThatPasses($message) |
|||
* @method bool hasCriticalThatPasses($message) |
|||
* @method bool hasErrorThatPasses($message) |
|||
* @method bool hasWarningThatPasses($message) |
|||
* @method bool hasNoticeThatPasses($message) |
|||
* @method bool hasInfoThatPasses($message) |
|||
* @method bool hasDebugThatPasses($message) |
|||
*/ |
|||
class TestHandler extends AbstractProcessingHandler |
|||
{ |
|||
protected $records = array(); |
|||
protected $recordsByLevel = array(); |
|||
|
|||
public function getRecords() |
|||
{ |
|||
return $this->records; |
|||
} |
|||
|
|||
public function clear() |
|||
{ |
|||
$this->records = array(); |
|||
$this->recordsByLevel = array(); |
|||
} |
|||
|
|||
public function hasRecords($level) |
|||
{ |
|||
return isset($this->recordsByLevel[$level]); |
|||
} |
|||
|
|||
/** |
|||
* @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records |
|||
* @param int $level Logger::LEVEL constant value |
|||
*/ |
|||
public function hasRecord($record, $level) |
|||
{ |
|||
if (is_string($record)) { |
|||
$record = array('message' => $record); |
|||
} |
|||
|
|||
return $this->hasRecordThatPasses(function ($rec) use ($record) { |
|||
if ($rec['message'] !== $record['message']) { |
|||
return false; |
|||
} |
|||
if (isset($record['context']) && $rec['context'] !== $record['context']) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}, $level); |
|||
} |
|||
|
|||
public function hasRecordThatContains($message, $level) |
|||
{ |
|||
return $this->hasRecordThatPasses(function ($rec) use ($message) { |
|||
return strpos($rec['message'], $message) !== false; |
|||
}, $level); |
|||
} |
|||
|
|||
public function hasRecordThatMatches($regex, $level) |
|||
{ |
|||
return $this->hasRecordThatPasses(function ($rec) use ($regex) { |
|||
return preg_match($regex, $rec['message']) > 0; |
|||
}, $level); |
|||
} |
|||
|
|||
public function hasRecordThatPasses($predicate, $level) |
|||
{ |
|||
if (!is_callable($predicate)) { |
|||
throw new \InvalidArgumentException("Expected a callable for hasRecordThatSucceeds"); |
|||
} |
|||
|
|||
if (!isset($this->recordsByLevel[$level])) { |
|||
return false; |
|||
} |
|||
|
|||
foreach ($this->recordsByLevel[$level] as $i => $rec) { |
|||
if (call_user_func($predicate, $rec, $i)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->recordsByLevel[$record['level']][] = $record; |
|||
$this->records[] = $record; |
|||
} |
|||
|
|||
public function __call($method, $args) |
|||
{ |
|||
if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { |
|||
$genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; |
|||
$level = constant('Monolog\Logger::' . strtoupper($matches[2])); |
|||
if (method_exists($this, $genericMethod)) { |
|||
$args[] = $level; |
|||
|
|||
return call_user_func_array(array($this, $genericMethod), $args); |
|||
} |
|||
} |
|||
|
|||
throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
/** |
|||
* Forwards records to multiple handlers suppressing failures of each handler |
|||
* and continuing through to give every handler a chance to succeed. |
|||
* |
|||
* @author Craig D'Amelio <craig@damelio.ca> |
|||
*/ |
|||
class WhatFailureGroupHandler extends GroupHandler |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handle(array $record) |
|||
{ |
|||
if ($this->processors) { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
try { |
|||
$handler->handle($record); |
|||
} catch (\Exception $e) { |
|||
// What failure? |
|||
} catch (\Throwable $e) { |
|||
// What failure? |
|||
} |
|||
} |
|||
|
|||
return false === $this->bubble; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function handleBatch(array $records) |
|||
{ |
|||
if ($this->processors) { |
|||
$processed = array(); |
|||
foreach ($records as $record) { |
|||
foreach ($this->processors as $processor) { |
|||
$processed[] = call_user_func($processor, $record); |
|||
} |
|||
} |
|||
$records = $processed; |
|||
} |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
try { |
|||
$handler->handleBatch($records); |
|||
} catch (\Exception $e) { |
|||
// What failure? |
|||
} catch (\Throwable $e) { |
|||
// What failure? |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
<?php |
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Handler; |
|||
|
|||
use Monolog\Formatter\NormalizerFormatter; |
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Handler sending logs to Zend Monitor |
|||
* |
|||
* @author Christian Bergau <cbergau86@gmail.com> |
|||
*/ |
|||
class ZendMonitorHandler extends AbstractProcessingHandler |
|||
{ |
|||
/** |
|||
* Monolog level / ZendMonitor Custom Event priority map |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $levelMap = array( |
|||
Logger::DEBUG => 1, |
|||
Logger::INFO => 2, |
|||
Logger::NOTICE => 3, |
|||
Logger::WARNING => 4, |
|||
Logger::ERROR => 5, |
|||
Logger::CRITICAL => 6, |
|||
Logger::ALERT => 7, |
|||
Logger::EMERGENCY => 0, |
|||
); |
|||
|
|||
/** |
|||
* Construct |
|||
* |
|||
* @param int $level |
|||
* @param bool $bubble |
|||
* @throws MissingExtensionException |
|||
*/ |
|||
public function __construct($level = Logger::DEBUG, $bubble = true) |
|||
{ |
|||
if (!function_exists('zend_monitor_custom_event')) { |
|||
throw new MissingExtensionException('You must have Zend Server installed in order to use this handler'); |
|||
} |
|||
parent::__construct($level, $bubble); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function write(array $record) |
|||
{ |
|||
$this->writeZendMonitorCustomEvent( |
|||
$this->levelMap[$record['level']], |
|||
$record['message'], |
|||
$record['formatted'] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Write a record to Zend Monitor |
|||
* |
|||
* @param int $level |
|||
* @param string $message |
|||
* @param array $formatted |
|||
*/ |
|||
protected function writeZendMonitorCustomEvent($level, $message, $formatted) |
|||
{ |
|||
zend_monitor_custom_event($level, $message, $formatted); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getDefaultFormatter() |
|||
{ |
|||
return new NormalizerFormatter(); |
|||
} |
|||
|
|||
/** |
|||
* Get the level map |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getLevelMap() |
|||
{ |
|||
return $this->levelMap; |
|||
} |
|||
} |
|||
@ -0,0 +1,791 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog; |
|||
|
|||
use Monolog\Handler\HandlerInterface; |
|||
use Monolog\Handler\StreamHandler; |
|||
use Psr\Log\LoggerInterface; |
|||
use Psr\Log\InvalidArgumentException; |
|||
use Exception; |
|||
|
|||
/** |
|||
* Monolog log channel |
|||
* |
|||
* It contains a stack of Handlers and a stack of Processors, |
|||
* and uses them to store records that are added to it. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class Logger implements LoggerInterface, ResettableInterface |
|||
{ |
|||
/** |
|||
* Detailed debug information |
|||
*/ |
|||
const DEBUG = 100; |
|||
|
|||
/** |
|||
* Interesting events |
|||
* |
|||
* Examples: User logs in, SQL logs. |
|||
*/ |
|||
const INFO = 200; |
|||
|
|||
/** |
|||
* Uncommon events |
|||
*/ |
|||
const NOTICE = 250; |
|||
|
|||
/** |
|||
* Exceptional occurrences that are not errors |
|||
* |
|||
* Examples: Use of deprecated APIs, poor use of an API, |
|||
* undesirable things that are not necessarily wrong. |
|||
*/ |
|||
const WARNING = 300; |
|||
|
|||
/** |
|||
* Runtime errors |
|||
*/ |
|||
const ERROR = 400; |
|||
|
|||
/** |
|||
* Critical conditions |
|||
* |
|||
* Example: Application component unavailable, unexpected exception. |
|||
*/ |
|||
const CRITICAL = 500; |
|||
|
|||
/** |
|||
* Action must be taken immediately |
|||
* |
|||
* Example: Entire website down, database unavailable, etc. |
|||
* This should trigger the SMS alerts and wake you up. |
|||
*/ |
|||
const ALERT = 550; |
|||
|
|||
/** |
|||
* Urgent alert. |
|||
*/ |
|||
const EMERGENCY = 600; |
|||
|
|||
/** |
|||
* Monolog API version |
|||
* |
|||
* This is only bumped when API breaks are done and should |
|||
* follow the major version of the library |
|||
* |
|||
* @var int |
|||
*/ |
|||
const API = 1; |
|||
|
|||
/** |
|||
* Logging levels from syslog protocol defined in RFC 5424 |
|||
* |
|||
* @var array $levels Logging levels |
|||
*/ |
|||
protected static $levels = array( |
|||
self::DEBUG => 'DEBUG', |
|||
self::INFO => 'INFO', |
|||
self::NOTICE => 'NOTICE', |
|||
self::WARNING => 'WARNING', |
|||
self::ERROR => 'ERROR', |
|||
self::CRITICAL => 'CRITICAL', |
|||
self::ALERT => 'ALERT', |
|||
self::EMERGENCY => 'EMERGENCY', |
|||
); |
|||
|
|||
/** |
|||
* @var \DateTimeZone |
|||
*/ |
|||
protected static $timezone; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $name; |
|||
|
|||
/** |
|||
* The handler stack |
|||
* |
|||
* @var HandlerInterface[] |
|||
*/ |
|||
protected $handlers; |
|||
|
|||
/** |
|||
* Processors that will process all log records |
|||
* |
|||
* To process records of a single handler instead, add the processor on that specific handler |
|||
* |
|||
* @var callable[] |
|||
*/ |
|||
protected $processors; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $microsecondTimestamps = true; |
|||
|
|||
/** |
|||
* @var callable |
|||
*/ |
|||
protected $exceptionHandler; |
|||
|
|||
/** |
|||
* @param string $name The logging channel |
|||
* @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. |
|||
* @param callable[] $processors Optional array of processors |
|||
*/ |
|||
public function __construct($name, array $handlers = array(), array $processors = array()) |
|||
{ |
|||
$this->name = $name; |
|||
$this->setHandlers($handlers); |
|||
$this->processors = $processors; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
/** |
|||
* Return a new cloned instance with the name changed |
|||
* |
|||
* @return static |
|||
*/ |
|||
public function withName($name) |
|||
{ |
|||
$new = clone $this; |
|||
$new->name = $name; |
|||
|
|||
return $new; |
|||
} |
|||
|
|||
/** |
|||
* Pushes a handler on to the stack. |
|||
* |
|||
* @param HandlerInterface $handler |
|||
* @return $this |
|||
*/ |
|||
public function pushHandler(HandlerInterface $handler) |
|||
{ |
|||
array_unshift($this->handlers, $handler); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Pops a handler from the stack |
|||
* |
|||
* @return HandlerInterface |
|||
*/ |
|||
public function popHandler() |
|||
{ |
|||
if (!$this->handlers) { |
|||
throw new \LogicException('You tried to pop from an empty handler stack.'); |
|||
} |
|||
|
|||
return array_shift($this->handlers); |
|||
} |
|||
|
|||
/** |
|||
* Set handlers, replacing all existing ones. |
|||
* |
|||
* If a map is passed, keys will be ignored. |
|||
* |
|||
* @param HandlerInterface[] $handlers |
|||
* @return $this |
|||
*/ |
|||
public function setHandlers(array $handlers) |
|||
{ |
|||
$this->handlers = array(); |
|||
foreach (array_reverse($handlers) as $handler) { |
|||
$this->pushHandler($handler); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return HandlerInterface[] |
|||
*/ |
|||
public function getHandlers() |
|||
{ |
|||
return $this->handlers; |
|||
} |
|||
|
|||
/** |
|||
* Adds a processor on to the stack. |
|||
* |
|||
* @param callable $callback |
|||
* @return $this |
|||
*/ |
|||
public function pushProcessor($callback) |
|||
{ |
|||
if (!is_callable($callback)) { |
|||
throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given'); |
|||
} |
|||
array_unshift($this->processors, $callback); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Removes the processor on top of the stack and returns it. |
|||
* |
|||
* @return callable |
|||
*/ |
|||
public function popProcessor() |
|||
{ |
|||
if (!$this->processors) { |
|||
throw new \LogicException('You tried to pop from an empty processor stack.'); |
|||
} |
|||
|
|||
return array_shift($this->processors); |
|||
} |
|||
|
|||
/** |
|||
* @return callable[] |
|||
*/ |
|||
public function getProcessors() |
|||
{ |
|||
return $this->processors; |
|||
} |
|||
|
|||
/** |
|||
* Control the use of microsecond resolution timestamps in the 'datetime' |
|||
* member of new records. |
|||
* |
|||
* Generating microsecond resolution timestamps by calling |
|||
* microtime(true), formatting the result via sprintf() and then parsing |
|||
* the resulting string via \DateTime::createFromFormat() can incur |
|||
* a measurable runtime overhead vs simple usage of DateTime to capture |
|||
* a second resolution timestamp in systems which generate a large number |
|||
* of log events. |
|||
* |
|||
* @param bool $micro True to use microtime() to create timestamps |
|||
*/ |
|||
public function useMicrosecondTimestamps($micro) |
|||
{ |
|||
$this->microsecondTimestamps = (bool) $micro; |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record. |
|||
* |
|||
* @param int $level The logging level |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addRecord($level, $message, array $context = array()) |
|||
{ |
|||
if (!$this->handlers) { |
|||
$this->pushHandler(new StreamHandler('php://stderr', static::DEBUG)); |
|||
} |
|||
|
|||
$levelName = static::getLevelName($level); |
|||
|
|||
// check if any handler will handle this message so we can return early and save cycles |
|||
$handlerKey = null; |
|||
reset($this->handlers); |
|||
while ($handler = current($this->handlers)) { |
|||
if ($handler->isHandling(array('level' => $level))) { |
|||
$handlerKey = key($this->handlers); |
|||
break; |
|||
} |
|||
|
|||
next($this->handlers); |
|||
} |
|||
|
|||
if (null === $handlerKey) { |
|||
return false; |
|||
} |
|||
|
|||
if (!static::$timezone) { |
|||
static::$timezone = new \DateTimeZone(date_default_timezone_get() ?: 'UTC'); |
|||
} |
|||
|
|||
// php7.1+ always has microseconds enabled, so we do not need this hack |
|||
if ($this->microsecondTimestamps && PHP_VERSION_ID < 70100) { |
|||
$ts = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), static::$timezone); |
|||
} else { |
|||
$ts = new \DateTime(null, static::$timezone); |
|||
} |
|||
$ts->setTimezone(static::$timezone); |
|||
|
|||
$record = array( |
|||
'message' => (string) $message, |
|||
'context' => $context, |
|||
'level' => $level, |
|||
'level_name' => $levelName, |
|||
'channel' => $this->name, |
|||
'datetime' => $ts, |
|||
'extra' => array(), |
|||
); |
|||
|
|||
try { |
|||
foreach ($this->processors as $processor) { |
|||
$record = call_user_func($processor, $record); |
|||
} |
|||
|
|||
while ($handler = current($this->handlers)) { |
|||
if (true === $handler->handle($record)) { |
|||
break; |
|||
} |
|||
|
|||
next($this->handlers); |
|||
} |
|||
} catch (Exception $e) { |
|||
$this->handleException($e, $record); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Ends a log cycle and frees all resources used by handlers. |
|||
* |
|||
* Closing a Handler means flushing all buffers and freeing any open resources/handles. |
|||
* Handlers that have been closed should be able to accept log records again and re-open |
|||
* themselves on demand, but this may not always be possible depending on implementation. |
|||
* |
|||
* This is useful at the end of a request and will be called automatically on every handler |
|||
* when they get destructed. |
|||
*/ |
|||
public function close() |
|||
{ |
|||
foreach ($this->handlers as $handler) { |
|||
if (method_exists($handler, 'close')) { |
|||
$handler->close(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Ends a log cycle and resets all handlers and processors to their initial state. |
|||
* |
|||
* Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal |
|||
* state, and getting it back to a state in which it can receive log records again. |
|||
* |
|||
* This is useful in case you want to avoid logs leaking between two requests or jobs when you |
|||
* have a long running process like a worker or an application server serving multiple requests |
|||
* in one process. |
|||
*/ |
|||
public function reset() |
|||
{ |
|||
foreach ($this->handlers as $handler) { |
|||
if ($handler instanceof ResettableInterface) { |
|||
$handler->reset(); |
|||
} |
|||
} |
|||
|
|||
foreach ($this->processors as $processor) { |
|||
if ($processor instanceof ResettableInterface) { |
|||
$processor->reset(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the DEBUG level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addDebug($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::DEBUG, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the INFO level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addInfo($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::INFO, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the NOTICE level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addNotice($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::NOTICE, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the WARNING level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addWarning($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::WARNING, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the ERROR level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addError($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::ERROR, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the CRITICAL level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addCritical($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::CRITICAL, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the ALERT level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addAlert($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::ALERT, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the EMERGENCY level. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function addEmergency($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::EMERGENCY, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Gets all supported logging levels. |
|||
* |
|||
* @return array Assoc array with human-readable level names => level codes. |
|||
*/ |
|||
public static function getLevels() |
|||
{ |
|||
return array_flip(static::$levels); |
|||
} |
|||
|
|||
/** |
|||
* Gets the name of the logging level. |
|||
* |
|||
* @param int $level |
|||
* @return string |
|||
*/ |
|||
public static function getLevelName($level) |
|||
{ |
|||
if (!isset(static::$levels[$level])) { |
|||
throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); |
|||
} |
|||
|
|||
return static::$levels[$level]; |
|||
} |
|||
|
|||
/** |
|||
* Converts PSR-3 levels to Monolog ones if necessary |
|||
* |
|||
* @param string|int Level number (monolog) or name (PSR-3) |
|||
* @return int |
|||
*/ |
|||
public static function toMonologLevel($level) |
|||
{ |
|||
if (is_string($level) && defined(__CLASS__.'::'.strtoupper($level))) { |
|||
return constant(__CLASS__.'::'.strtoupper($level)); |
|||
} |
|||
|
|||
return $level; |
|||
} |
|||
|
|||
/** |
|||
* Checks whether the Logger has a handler that listens on the given level |
|||
* |
|||
* @param int $level |
|||
* @return bool |
|||
*/ |
|||
public function isHandling($level) |
|||
{ |
|||
$record = array( |
|||
'level' => $level, |
|||
); |
|||
|
|||
foreach ($this->handlers as $handler) { |
|||
if ($handler->isHandling($record)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Set a custom exception handler |
|||
* |
|||
* @param callable $callback |
|||
* @return $this |
|||
*/ |
|||
public function setExceptionHandler($callback) |
|||
{ |
|||
if (!is_callable($callback)) { |
|||
throw new \InvalidArgumentException('Exception handler must be valid callable (callback or object with an __invoke method), '.var_export($callback, true).' given'); |
|||
} |
|||
$this->exceptionHandler = $callback; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return callable |
|||
*/ |
|||
public function getExceptionHandler() |
|||
{ |
|||
return $this->exceptionHandler; |
|||
} |
|||
|
|||
/** |
|||
* Delegates exception management to the custom exception handler, |
|||
* or throws the exception if no custom handler is set. |
|||
*/ |
|||
protected function handleException(Exception $e, array $record) |
|||
{ |
|||
if (!$this->exceptionHandler) { |
|||
throw $e; |
|||
} |
|||
|
|||
call_user_func($this->exceptionHandler, $e, $record); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at an arbitrary level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param mixed $level The log level |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function log($level, $message, array $context = array()) |
|||
{ |
|||
$level = static::toMonologLevel($level); |
|||
|
|||
return $this->addRecord($level, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the DEBUG level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function debug($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::DEBUG, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the INFO level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function info($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::INFO, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the NOTICE level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function notice($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::NOTICE, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the WARNING level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function warn($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::WARNING, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the WARNING level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function warning($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::WARNING, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the ERROR level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function err($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::ERROR, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the ERROR level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function error($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::ERROR, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the CRITICAL level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function crit($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::CRITICAL, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the CRITICAL level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function critical($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::CRITICAL, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the ALERT level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function alert($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::ALERT, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the EMERGENCY level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function emerg($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::EMERGENCY, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Adds a log record at the EMERGENCY level. |
|||
* |
|||
* This method allows for compatibility with common interfaces. |
|||
* |
|||
* @param string $message The log message |
|||
* @param array $context The log context |
|||
* @return bool Whether the record has been processed |
|||
*/ |
|||
public function emergency($message, array $context = array()) |
|||
{ |
|||
return $this->addRecord(static::EMERGENCY, $message, $context); |
|||
} |
|||
|
|||
/** |
|||
* Set the timezone to be used for the timestamp of log records. |
|||
* |
|||
* This is stored globally for all Logger instances |
|||
* |
|||
* @param \DateTimeZone $tz Timezone object |
|||
*/ |
|||
public static function setTimezone(\DateTimeZone $tz) |
|||
{ |
|||
self::$timezone = $tz; |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Injects Git branch and Git commit SHA in all records |
|||
* |
|||
* @author Nick Otter |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class GitProcessor implements ProcessorInterface |
|||
{ |
|||
private $level; |
|||
private static $cache; |
|||
|
|||
public function __construct($level = Logger::DEBUG) |
|||
{ |
|||
$this->level = Logger::toMonologLevel($level); |
|||
} |
|||
|
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
// return if the level is not high enough |
|||
if ($record['level'] < $this->level) { |
|||
return $record; |
|||
} |
|||
|
|||
$record['extra']['git'] = self::getGitInfo(); |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
private static function getGitInfo() |
|||
{ |
|||
if (self::$cache) { |
|||
return self::$cache; |
|||
} |
|||
|
|||
$branches = `git branch -v --no-abbrev`; |
|||
if (preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) { |
|||
return self::$cache = array( |
|||
'branch' => $matches[1], |
|||
'commit' => $matches[2], |
|||
); |
|||
} |
|||
|
|||
return self::$cache = array(); |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Injects line/file:class/function where the log message came from |
|||
* |
|||
* Warning: This only works if the handler processes the logs directly. |
|||
* If you put the processor on a handler that is behind a FingersCrossedHandler |
|||
* for example, the processor will only be called once the trigger level is reached, |
|||
* and all the log records will have the same file/line/.. data from the call that |
|||
* triggered the FingersCrossedHandler. |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class IntrospectionProcessor implements ProcessorInterface |
|||
{ |
|||
private $level; |
|||
|
|||
private $skipClassesPartials; |
|||
|
|||
private $skipStackFramesCount; |
|||
|
|||
private $skipFunctions = array( |
|||
'call_user_func', |
|||
'call_user_func_array', |
|||
); |
|||
|
|||
public function __construct($level = Logger::DEBUG, array $skipClassesPartials = array(), $skipStackFramesCount = 0) |
|||
{ |
|||
$this->level = Logger::toMonologLevel($level); |
|||
$this->skipClassesPartials = array_merge(array('Monolog\\'), $skipClassesPartials); |
|||
$this->skipStackFramesCount = $skipStackFramesCount; |
|||
} |
|||
|
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
// return if the level is not high enough |
|||
if ($record['level'] < $this->level) { |
|||
return $record; |
|||
} |
|||
|
|||
/* |
|||
* http://php.net/manual/en/function.debug-backtrace.php |
|||
* As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added. |
|||
* Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'. |
|||
*/ |
|||
$trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS); |
|||
|
|||
// skip first since it's always the current method |
|||
array_shift($trace); |
|||
// the call_user_func call is also skipped |
|||
array_shift($trace); |
|||
|
|||
$i = 0; |
|||
|
|||
while ($this->isTraceClassOrSkippedFunction($trace, $i)) { |
|||
if (isset($trace[$i]['class'])) { |
|||
foreach ($this->skipClassesPartials as $part) { |
|||
if (strpos($trace[$i]['class'], $part) !== false) { |
|||
$i++; |
|||
continue 2; |
|||
} |
|||
} |
|||
} elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { |
|||
$i++; |
|||
continue; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
$i += $this->skipStackFramesCount; |
|||
|
|||
// we should have the call source now |
|||
$record['extra'] = array_merge( |
|||
$record['extra'], |
|||
array( |
|||
'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, |
|||
'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, |
|||
'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, |
|||
'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, |
|||
) |
|||
); |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
private function isTraceClassOrSkippedFunction(array $trace, $index) |
|||
{ |
|||
if (!isset($trace[$index])) { |
|||
return false; |
|||
} |
|||
|
|||
return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Injects memory_get_peak_usage in all records |
|||
* |
|||
* @see Monolog\Processor\MemoryProcessor::__construct() for options |
|||
* @author Rob Jensen |
|||
*/ |
|||
class MemoryPeakUsageProcessor extends MemoryProcessor |
|||
{ |
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
$bytes = memory_get_peak_usage($this->realUsage); |
|||
$formatted = $this->formatBytes($bytes); |
|||
|
|||
$record['extra']['memory_peak_usage'] = $formatted; |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Some methods that are common for all memory processors |
|||
* |
|||
* @author Rob Jensen |
|||
*/ |
|||
abstract class MemoryProcessor implements ProcessorInterface |
|||
{ |
|||
/** |
|||
* @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported. |
|||
*/ |
|||
protected $realUsage; |
|||
|
|||
/** |
|||
* @var bool If true, then format memory size to human readable string (MB, KB, B depending on size) |
|||
*/ |
|||
protected $useFormatting; |
|||
|
|||
/** |
|||
* @param bool $realUsage Set this to true to get the real size of memory allocated from system. |
|||
* @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size) |
|||
*/ |
|||
public function __construct($realUsage = true, $useFormatting = true) |
|||
{ |
|||
$this->realUsage = (bool) $realUsage; |
|||
$this->useFormatting = (bool) $useFormatting; |
|||
} |
|||
|
|||
/** |
|||
* Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is |
|||
* |
|||
* @param int $bytes |
|||
* @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as is |
|||
*/ |
|||
protected function formatBytes($bytes) |
|||
{ |
|||
$bytes = (int) $bytes; |
|||
|
|||
if (!$this->useFormatting) { |
|||
return $bytes; |
|||
} |
|||
|
|||
if ($bytes > 1024 * 1024) { |
|||
return round($bytes / 1024 / 1024, 2).' MB'; |
|||
} elseif ($bytes > 1024) { |
|||
return round($bytes / 1024, 2).' KB'; |
|||
} |
|||
|
|||
return $bytes . ' B'; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Injects memory_get_usage in all records |
|||
* |
|||
* @see Monolog\Processor\MemoryProcessor::__construct() for options |
|||
* @author Rob Jensen |
|||
*/ |
|||
class MemoryUsageProcessor extends MemoryProcessor |
|||
{ |
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
$bytes = memory_get_usage($this->realUsage); |
|||
$formatted = $this->formatBytes($bytes); |
|||
|
|||
$record['extra']['memory_usage'] = $formatted; |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jonathan A. Schweder <jonathanschweder@gmail.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
use Monolog\Logger; |
|||
|
|||
/** |
|||
* Injects Hg branch and Hg revision number in all records |
|||
* |
|||
* @author Jonathan A. Schweder <jonathanschweder@gmail.com> |
|||
*/ |
|||
class MercurialProcessor implements ProcessorInterface |
|||
{ |
|||
private $level; |
|||
private static $cache; |
|||
|
|||
public function __construct($level = Logger::DEBUG) |
|||
{ |
|||
$this->level = Logger::toMonologLevel($level); |
|||
} |
|||
|
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
// return if the level is not high enough |
|||
if ($record['level'] < $this->level) { |
|||
return $record; |
|||
} |
|||
|
|||
$record['extra']['hg'] = self::getMercurialInfo(); |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
private static function getMercurialInfo() |
|||
{ |
|||
if (self::$cache) { |
|||
return self::$cache; |
|||
} |
|||
|
|||
$result = explode(' ', trim(`hg id -nb`)); |
|||
if (count($result) >= 3) { |
|||
return self::$cache = array( |
|||
'branch' => $result[1], |
|||
'revision' => $result[2], |
|||
); |
|||
} |
|||
|
|||
return self::$cache = array(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Adds value of getmypid into records |
|||
* |
|||
* @author Andreas Hörnicke |
|||
*/ |
|||
class ProcessIdProcessor implements ProcessorInterface |
|||
{ |
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
$record['extra']['process_id'] = getmypid(); |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* An optional interface to allow labelling Monolog processors. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
interface ProcessorInterface |
|||
{ |
|||
/** |
|||
* @return array The processed records |
|||
*/ |
|||
public function __invoke(array $records); |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
use Monolog\Utils; |
|||
|
|||
/** |
|||
* Processes a record's message according to PSR-3 rules |
|||
* |
|||
* It replaces {foo} with the value from $context['foo'] |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class PsrLogMessageProcessor implements ProcessorInterface |
|||
{ |
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
if (false === strpos($record['message'], '{')) { |
|||
return $record; |
|||
} |
|||
|
|||
$replacements = array(); |
|||
foreach ($record['context'] as $key => $val) { |
|||
if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { |
|||
$replacements['{'.$key.'}'] = $val; |
|||
} elseif (is_object($val)) { |
|||
$replacements['{'.$key.'}'] = '[object '.Utils::getClass($val).']'; |
|||
} else { |
|||
$replacements['{'.$key.'}'] = '['.gettype($val).']'; |
|||
} |
|||
} |
|||
|
|||
$record['message'] = strtr($record['message'], $replacements); |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Adds a tags array into record |
|||
* |
|||
* @author Martijn Riemers |
|||
*/ |
|||
class TagProcessor implements ProcessorInterface |
|||
{ |
|||
private $tags; |
|||
|
|||
public function __construct(array $tags = array()) |
|||
{ |
|||
$this->setTags($tags); |
|||
} |
|||
|
|||
public function addTags(array $tags = array()) |
|||
{ |
|||
$this->tags = array_merge($this->tags, $tags); |
|||
} |
|||
|
|||
public function setTags(array $tags = array()) |
|||
{ |
|||
$this->tags = $tags; |
|||
} |
|||
|
|||
public function __invoke(array $record) |
|||
{ |
|||
$record['extra']['tags'] = $this->tags; |
|||
|
|||
return $record; |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
use Monolog\ResettableInterface; |
|||
|
|||
/** |
|||
* Adds a unique identifier into records |
|||
* |
|||
* @author Simon Mönch <sm@webfactory.de> |
|||
*/ |
|||
class UidProcessor implements ProcessorInterface, ResettableInterface |
|||
{ |
|||
private $uid; |
|||
|
|||
public function __construct($length = 7) |
|||
{ |
|||
if (!is_int($length) || $length > 32 || $length < 1) { |
|||
throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); |
|||
} |
|||
|
|||
|
|||
$this->uid = $this->generateUid($length); |
|||
} |
|||
|
|||
public function __invoke(array $record) |
|||
{ |
|||
$record['extra']['uid'] = $this->uid; |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getUid() |
|||
{ |
|||
return $this->uid; |
|||
} |
|||
|
|||
public function reset() |
|||
{ |
|||
$this->uid = $this->generateUid(strlen($this->uid)); |
|||
} |
|||
|
|||
private function generateUid($length) |
|||
{ |
|||
return substr(hash('md5', uniqid('', true)), 0, $length); |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Monolog package. |
|||
* |
|||
* (c) Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Monolog\Processor; |
|||
|
|||
/** |
|||
* Injects url/method and remote IP of the current web request in all records |
|||
* |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
*/ |
|||
class WebProcessor implements ProcessorInterface |
|||
{ |
|||
/** |
|||
* @var array|\ArrayAccess |
|||
*/ |
|||
protected $serverData; |
|||
|
|||
/** |
|||
* Default fields |
|||
* |
|||
* Array is structured as [key in record.extra => key in $serverData] |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $extraFields = array( |
|||
'url' => 'REQUEST_URI', |
|||
'ip' => 'REMOTE_ADDR', |
|||
'http_method' => 'REQUEST_METHOD', |
|||
'server' => 'SERVER_NAME', |
|||
'referrer' => 'HTTP_REFERER', |
|||
); |
|||
|
|||
/** |
|||
* @param array|\ArrayAccess $serverData Array or object w/ ArrayAccess that provides access to the $_SERVER data |
|||
* @param array|null $extraFields Field names and the related key inside $serverData to be added. If not provided it defaults to: url, ip, http_method, server, referrer |
|||
*/ |
|||
public function __construct($serverData = null, array $extraFields = null) |
|||
{ |
|||
if (null === $serverData) { |
|||
$this->serverData = &$_SERVER; |
|||
} elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) { |
|||
$this->serverData = $serverData; |
|||
} else { |
|||
throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.'); |
|||
} |
|||
|
|||
if (null !== $extraFields) { |
|||
if (isset($extraFields[0])) { |
|||
foreach (array_keys($this->extraFields) as $fieldName) { |
|||
if (!in_array($fieldName, $extraFields)) { |
|||
unset($this->extraFields[$fieldName]); |
|||
} |
|||
} |
|||
} else { |
|||
$this->extraFields = $extraFields; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param array $record |
|||
* @return array |
|||
*/ |
|||
public function __invoke(array $record) |
|||
{ |
|||
// skip processing if for some reason request data |
|||
// is not present (CLI or wonky SAPIs) |
|||
if (!isset($this->serverData['REQUEST_URI'])) { |
|||
return $record; |
|||
} |
|||
|
|||
$record['extra'] = $this->appendExtraFields($record['extra']); |
|||
|
|||
return $record; |
|||
} |
|||
|
|||
/** |
|||
* @param string $extraName |
|||
* @param string $serverName |
|||
* @return $this |
|||
*/ |
|||
public function addExtraField($extraName, $serverName) |
|||
{ |
|||
$this->extraFields[$extraName] = $serverName; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param array $extra |
|||
* @return array |
|||
*/ |
|||
private function appendExtraFields(array $extra) |
|||
{ |
|||
foreach ($this->extraFields as $extraName => $serverName) { |
|||
$extra[$extraName] = isset($this->serverData[$serverName]) ? $this->serverData[$serverName] : null; |
|||
} |
|||
|
|||
if (isset($this->serverData['UNIQUE_ID'])) { |
|||
$extra['unique_id'] = $this->serverData['UNIQUE_ID']; |
|||
} |
|||
|
|||
return $extra; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue