laravel-6 support

This commit is contained in:
RafficMohammed
2023-01-08 01:17:22 +05:30
parent 1a5c16ae4b
commit 774eed8b0e
4962 changed files with 279380 additions and 297961 deletions

View File

@@ -1,11 +1,187 @@
### 1.27.1 (2022-06-09)
### 2.8.0 (2022-07-24)
* Fixed MandrillHandler support for SwiftMailer 6 (#1676)
* Fixed StreamHandler chunk size (backport from #1552)
* Deprecated `CubeHandler` and `PHPConsoleHandler` as both projects are abandoned and those should not be used anymore (#1734)
* Added RFC 5424 level (`7` to `0`) support to `Logger::log` and `Logger::addRecord` to increase interoperability (#1723)
* Added support for `__toString` for objects which are not json serializable in `JsonFormatter` (#1733)
* Added `GoogleCloudLoggingFormatter` (#1719)
* Added support for Predis 2.x (#1732)
* Added `AmqpHandler->setExtraAttributes` to allow configuring attributes when using an AMQPExchange (#1724)
* Fixed serialization/unserialization of handlers to make sure private properties are included (#1727)
* Fixed allowInlineLineBreaks in LineFormatter causing issues with windows paths containing `\n` or `\r` sequences (#1720)
* Fixed max normalization depth not being taken into account when formatting exceptions with a deep chain of previous exceptions (#1726)
* Fixed PHP 8.2 deprecation warnings (#1722)
* Fixed rare race condition or filesystem issue where StreamHandler is unable to create the directory the log should go into yet it exists already (#1678)
### 1.27.0 (2022-03-13)
### 2.7.0 (2022-06-09)
* Added $maxDepth / setMaxDepth to NormalizerFormatter / JsonFormatter to configure the maximum depth if the default of 9 does not work for you (#1633)
* Added `$datetime` parameter to `Logger::addRecord` as low level API to allow logging into the past or future (#1682)
* Added `Logger::useLoggingLoopDetection` to allow disabling cyclic logging detection in concurrent frameworks (#1681)
* Fixed handling of fatal errors if callPrevious is disabled in ErrorHandler (#1670)
* Marked the reusable `Monolog\Test\TestCase` class as `@internal` to make sure PHPStorm does not show it above PHPUnit, you may still use it to test your own handlers/etc though (#1677)
* Fixed RotatingFileHandler issue when the date format contained slashes (#1671)
### 2.6.0 (2022-05-10)
* Deprecated `SwiftMailerHandler`, use `SymfonyMailerHandler` instead
* Added `SymfonyMailerHandler` (#1663)
* Added ElasticSearch 8.x support to the ElasticsearchHandler (#1662)
* Added a way to filter/modify stack traces in LineFormatter (#1665)
* Fixed UdpSocket not being able to reopen/reconnect after close()
* Fixed infinite loops if a Handler is triggering logging while handling log records
### 2.5.0 (2022-04-08)
* Added `callType` to IntrospectionProcessor (#1612)
* Fixed AsMonologProcessor syntax to be compatible with PHP 7.2 (#1651)
### 2.4.0 (2022-03-14)
* Added [`Monolog\LogRecord`](src/Monolog/LogRecord.php) interface that can be used to type-hint records like `array|\Monolog\LogRecord $record` to be forward compatible with the upcoming Monolog 3 changes
* Added `includeStacktraces` constructor params to LineFormatter & JsonFormatter (#1603)
* Added `persistent`, `timeout`, `writingTimeout`, `connectionTimeout`, `chunkSize` constructor params to SocketHandler and derivatives (#1600)
* Added `AsMonologProcessor` PHP attribute which can help autowiring / autoconfiguration of processors if frameworks / integrations decide to make use of it. This is useless when used purely with Monolog (#1637)
* Added support for keeping native BSON types as is in MongoDBFormatter (#1620)
* Added support for a `user_agent` key in WebProcessor, disabled by default but you can use it by configuring the $extraFields you want (#1613)
* Added support for username/userIcon in SlackWebhookHandler (#1617)
* Added extension points to BrowserConsoleHandler (#1593)
* Added record message/context/extra info to exceptions thrown when a StreamHandler cannot open its stream to avoid completely losing the data logged (#1630)
* Fixed error handler signature to accept a null $context which happens with internal PHP errors (#1614)
* Fixed a few setter methods not returning `self` (#1609)
* Fixed handling of records going over the max Telegram message length (#1616)
### 2.3.5 (2021-10-01)
* Fixed regression in StreamHandler since 2.3.3 on systems with the memory_limit set to >=20GB (#1592)
### 2.3.4 (2021-09-15)
* Fixed support for psr/log 3.x (#1589)
### 2.3.3 (2021-09-14)
* Fixed memory usage when using StreamHandler and calling stream_get_contents on the resource you passed to it (#1578, #1577)
* Fixed support for psr/log 2.x (#1587)
* Fixed some type annotations
### 2.3.2 (2021-07-23)
* Fixed compatibility with PHP 7.2 - 7.4 when experiencing PCRE errors (#1568)
### 2.3.1 (2021-07-14)
* Fixed Utils::getClass handling of anonymous classes not being fully compatible with PHP 8 (#1563)
* Fixed some `@inheritDoc` annotations having the wrong case
### 2.3.0 (2021-07-05)
* Added a ton of PHPStan type annotations as well as type aliases on Monolog\Logger for Record, Level and LevelName that you can import (#1557)
* Added ability to customize date format when using JsonFormatter (#1561)
* Fixed FilterHandler not calling reset on its internal handler when reset() is called on it (#1531)
* Fixed SyslogUdpHandler not setting the timezone correctly on DateTimeImmutable instances (#1540)
* Fixed StreamHandler thread safety - chunk size set to 2GB now to avoid interlacing when doing concurrent writes (#1553)
### 2.2.0 (2020-12-14)
* Added JSON_PARTIAL_OUTPUT_ON_ERROR to default json encoding flags, to avoid dropping entire context data or even records due to an invalid subset of it somewhere
* Added setDateFormat to NormalizerFormatter (and Line/Json formatters by extension) to allow changing this after object creation
* Added RedisPubSubHandler to log records to a Redis channel using PUBLISH
* Added support for Elastica 7, and deprecated the $type argument of ElasticaFormatter which is not in use anymore as of Elastica 7
* Added support for millisecond write timeouts in SocketHandler, you can now pass floats to setWritingTimeout, e.g. 0.2 is 200ms
* Added support for unix sockets in SyslogUdpHandler (set $port to 0 to make the $host a unix socket)
* Added handleBatch support for TelegramBotHandler
* Added RFC5424e extended date format including milliseconds to SyslogUdpHandler
* Added support for configuring handlers with numeric level values in strings (coming from e.g. env vars)
* Fixed Wildfire/FirePHP/ChromePHP handling of unicode characters
* Fixed PHP 8 issues in SyslogUdpHandler
* Fixed internal type error when mbstring is missing
### 2.1.1 (2020-07-23)
* Fixed removing of json encoding options
* Fixed type hint of $level not accepting strings in SendGridHandler and OverflowHandler
* Fixed SwiftMailerHandler not accepting email templates with an empty subject
* Fixed array access on null in RavenHandler
* Fixed unique_id in WebProcessor not being disableable
### 2.1.0 (2020-05-22)
* Added `JSON_INVALID_UTF8_SUBSTITUTE` to default json flags, so that invalid UTF8 characters now get converted to [<EFBFBD>](https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character) instead of being converted from ISO-8859-15 to UTF8 as it was before, which was hardly a comprehensive solution
* Added `$ignoreEmptyContextAndExtra` option to JsonFormatter to skip empty context/extra entirely from the output
* Added `$parseMode`, `$disableWebPagePreview` and `$disableNotification` options to TelegramBotHandler
* Added tentative support for PHP 8
* NormalizerFormatter::addJsonEncodeOption and removeJsonEncodeOption are now public to allow modifying default json flags
* Fixed GitProcessor type error when there is no git repo present
* Fixed normalization of SoapFault objects containing deeply nested objects as "detail"
* Fixed support for relative paths in RotatingFileHandler
### 2.0.2 (2019-12-20)
* Fixed ElasticsearchHandler swallowing exceptions details when failing to index log records
* Fixed normalization of SoapFault objects containing non-strings as "detail" in LineFormatter
* Fixed formatting of resources in JsonFormatter
* Fixed RedisHandler failing to use MULTI properly when passed a proxied Redis instance (e.g. in Symfony with lazy services)
* Fixed FilterHandler triggering a notice when handleBatch was filtering all records passed to it
* Fixed Turkish locale messing up the conversion of level names to their constant values
### 2.0.1 (2019-11-13)
* Fixed normalization of Traversables to avoid traversing them as not all of them are rewindable
* Fixed setFormatter/getFormatter to forward to the nested handler in FilterHandler, FingersCrossedHandler, BufferHandler, OverflowHandler and SamplingHandler
* Fixed BrowserConsoleHandler formatting when using multiple styles
* Fixed normalization of exception codes to be always integers even for PDOException which have them as numeric strings
* Fixed normalization of SoapFault objects containing non-strings as "detail"
* Fixed json encoding across all handlers to always attempt recovery of non-UTF-8 strings instead of failing the whole encoding
* Fixed ChromePHPHandler to avoid sending more data than latest Chrome versions allow in headers (4KB down from 256KB).
* Fixed type error in BrowserConsoleHandler when the context array of log records was not associative.
### 2.0.0 (2019-08-30)
* BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release
* BC Break: Logger methods log/debug/info/notice/warning/error/critical/alert/emergency now have explicit void return types
* Added FallbackGroupHandler which works like the WhatFailureGroupHandler but stops dispatching log records as soon as one handler accepted it
* Fixed support for UTF-8 when cutting strings to avoid cutting a multibyte-character in half
* Fixed normalizers handling of exception backtraces to avoid serializing arguments in some cases
* Fixed date timezone handling in SyslogUdpHandler
### 2.0.0-beta2 (2019-07-06)
* BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release
* BC Break: PHP 7.2 is now the minimum required PHP version.
* BC Break: Removed SlackbotHandler, RavenHandler and HipChatHandler, see [UPGRADE.md](UPGRADE.md) for details
* Added OverflowHandler which will only flush log records to its nested handler when reaching a certain amount of logs (i.e. only pass through when things go really bad)
* Added TelegramBotHandler to log records to a [Telegram](https://core.telegram.org/bots/api) bot account
* Added support for JsonSerializable when normalizing exceptions
* Added support for RFC3164 (outdated BSD syslog protocol) to SyslogUdpHandler
* Added SoapFault details to formatted exceptions
* Fixed DeduplicationHandler silently failing to start when file could not be opened
* Fixed issue in GroupHandler and WhatFailureGroupHandler where setting multiple processors would duplicate records
* Fixed GelfFormatter losing some data when one attachment was too long
* Fixed issue in SignalHandler restarting syscalls functionality
* Improved performance of LogglyHandler when sending multiple logs in a single request
### 2.0.0-beta1 (2018-12-08)
* BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release
* BC Break: PHP 7.1 is now the minimum required PHP version.
* BC Break: Quite a few interface changes, only relevant if you implemented your own handlers/processors/formatters
* BC Break: Removed non-PSR-3 methods to add records, all the `add*` (e.g. `addWarning`) methods as well as `emerg`, `crit`, `err` and `warn`
* BC Break: The record timezone is now set per Logger instance and not statically anymore
* BC Break: There is no more default handler configured on empty Logger instances
* BC Break: ElasticSearchHandler renamed to ElasticaHandler
* BC Break: Various handler-specific breaks, see [UPGRADE.md](UPGRADE.md) for details
* Added scalar type hints and return hints in all the places it was possible. Switched strict_types on for more reliability.
* Added DateTimeImmutable support, all record datetime are now immutable, and will toString/json serialize with the correct date format, including microseconds (unless disabled)
* Added timezone and microseconds to the default date format
* Added SendGridHandler to use the SendGrid API to send emails
* Added LogmaticHandler to use the Logmatic.io API to store log records
* Added SqsHandler to send log records to an AWS SQS queue
* Added ElasticsearchHandler to send records via the official ES library. Elastica users should now use ElasticaHandler instead of ElasticSearchHandler
* Added NoopHandler which is similar to the NullHandle but does not prevent the bubbling of log records to handlers further down the configuration, useful for temporarily disabling a handler in configuration files
* Added ProcessHandler to write log output to the STDIN of a given process
* Added HostnameProcessor that adds the machine's hostname to log records
* Added a `$dateFormat` option to the PsrLogMessageProcessor which lets you format DateTime instances nicely
* Added support for the PHP 7.x `mongodb` extension in the MongoDBHandler
* Fixed many minor issues in various handlers, and probably added a few regressions too
### 1.26.1 (2021-05-28)
@@ -67,7 +243,7 @@
* 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 protection to NormalizerFormatter 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
@@ -107,7 +283,7 @@
* 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 fatal errors occurring 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
@@ -117,7 +293,7 @@
* 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 ability to format the main line of text the SlackHandler sends by explicitly 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
@@ -239,7 +415,7 @@
* 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 MandrillHandler to avoid outputting API call responses
* Fixed some non-standard behaviors in SyslogUdpHandler
### 1.11.0 (2014-09-30)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2011-2016 Jordi Boggiano
Copyright (c) 2011-2020 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

View File

@@ -1,4 +1,4 @@
# Monolog - Logging for PHP [![Build Status](https://img.shields.io/travis/Seldaek/monolog.svg)](https://travis-ci.org/Seldaek/monolog)
# Monolog - Logging for PHP [![Continuous Integration](https://github.com/Seldaek/monolog/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/Seldaek/monolog/actions)
[![Total Downloads](https://img.shields.io/packagist/dt/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)
[![Latest Stable Version](https://img.shields.io/packagist/v/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)
@@ -36,16 +36,23 @@ $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');
$log->warning('Foo');
$log->error('Bar');
```
## Documentation
- [Usage Instructions](doc/01-usage.md)
- [Handlers, Formatters and Processors](doc/02-handlers-formatters-processors.md)
- [Utility classes](doc/03-utilities.md)
- [Utility Classes](doc/03-utilities.md)
- [Extending Monolog](doc/04-extending.md)
- [Log Record Structure](doc/message-structure.md)
## Support Monolog Financially
Get supported Monolog and help fund the project with the [Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-monolog-monolog?utm_source=packagist-monolog-monolog&utm_medium=referral&utm_campaign=enterprise) or via [GitHub sponsorship](https://github.com/sponsors/Seldaek).
Tidelift delivers commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use.
## Third Party Packages
@@ -57,7 +64,11 @@ can also add your own there if you publish one.
### Requirements
- Monolog works with PHP 5.3 or above, and is also tested to work with HHVM.
- Monolog `^2.0` works with PHP 7.2 or above, use Monolog `^1.25` for PHP 5.3+ support.
### Support
Monolog 1.x support is somewhat limited at this point and only important fixes will be done. You should migrate to Monolog 2 where possible to benefit from all the latest features and fixes.
### Submitting bugs and feature requests
@@ -67,26 +78,33 @@ Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/mono
- 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.
- [Symfony](http://symfony.com) comes out of the box with Monolog.
- [Laravel](http://laravel.com/) comes 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.
- [PPI](https://github.com/ppi/framework) 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.
- [Nette Framework](http://nette.org/en/) is usable with Monolog via the [contributte/monolog](https://github.com/contributte/monolog) or [orisai/nette-monolog](https://github.com/orisai/nette-monolog) extensions.
- [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog.
- [FuelPHP](http://fuelphp.com/) comes out of the box with Monolog.
- [Equip Framework](https://github.com/equip/framework) comes out of the box with Monolog.
- [Yii 2](http://www.yiiframework.com/) is usable with Monolog via the [yii2-monolog](https://github.com/merorafael/yii2-monolog) or [yii2-psr-log-target](https://github.com/samdark/yii2-psr-log-target) plugins.
- [Hawkbit Micro Framework](https://github.com/HawkBitPhp/hawkbit) comes out of the box with Monolog.
- [SilverStripe 4](https://www.silverstripe.org/) comes out of the box with Monolog.
- [Drupal](https://www.drupal.org/) is usable with Monolog via the [monolog](https://www.drupal.org/project/monolog) module.
- [Aimeos ecommerce framework](https://aimeos.org/) is usable with Monolog via the [ai-monolog](https://github.com/aimeos/ai-monolog) extension.
- [Magento](https://magento.com/) 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.
See also the list of [contributors](https://github.com/Seldaek/monolog/contributors) who participated in this project.
### License
Monolog is licensed under the MIT License - see the `LICENSE` file for details
Monolog is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
### Acknowledgements

72
vendor/monolog/monolog/UPGRADE.md vendored Normal file
View File

@@ -0,0 +1,72 @@
### 2.0.0
- `Monolog\Logger::API` can be used to distinguish between a Monolog `1` and `2`
install of Monolog when writing integration code.
- Removed non-PSR-3 methods to add records, all the `add*` (e.g. `addWarning`)
methods as well as `emerg`, `crit`, `err` and `warn`.
- DateTime are now formatted with a timezone and microseconds (unless disabled).
Various formatters and log output might be affected, which may mess with log parsing
in some cases.
- The `datetime` in every record array is now a DateTimeImmutable, not that you
should have been modifying these anyway.
- The timezone is now set per Logger instance and not statically, either
via ->setTimezone or passed in the constructor. Calls to Logger::setTimezone
should be converted.
- `HandlerInterface` has been split off and two new interfaces now exist for
more granular controls: `ProcessableHandlerInterface` and
`FormattableHandlerInterface`. Handlers not extending `AbstractHandler`
should make sure to implement the relevant interfaces.
- `HandlerInterface` now requires the `close` method to be implemented. This
only impacts you if you implement the interface yourself, but you can extend
the new `Monolog\Handler\Handler` base class too.
- There is no more default handler configured on empty Logger instances, if
you were relying on that you will not get any output anymore, make sure to
configure the handler you need.
#### LogglyFormatter
- The records' `datetime` is not sent anymore. Only `timestamp` is sent to Loggly.
#### AmqpHandler
- Log levels are not shortened to 4 characters anymore. e.g. a warning record
will be sent using the `warning.channel` routing key instead of `warn.channel`
as in 1.x.
- The exchange name does not default to 'log' anymore, and it is completely ignored
now for the AMQP extension users. Only PHPAmqpLib uses it if provided.
#### RotatingFileHandler
- The file name format must now contain `{date}` and the date format must be set
to one of the predefined FILE_PER_* constants to avoid issues with file rotation.
See `setFilenameFormat`.
#### LogstashFormatter
- Removed Logstash V0 support
- Context/extra prefix has been removed in favor of letting users configure the exact key being sent
- Context/extra data are now sent as an object instead of single keys
#### HipChatHandler
- Removed deprecated HipChat handler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead
#### SlackbotHandler
- Removed deprecated SlackbotHandler handler, use SlackWebhookHandler or SlackHandler instead
#### RavenHandler
- Removed deprecated RavenHandler handler, use sentry/sentry 2.x and their Sentry\Monolog\Handler instead
#### ElasticSearchHandler
- As support for the official Elasticsearch library was added, the former ElasticSearchHandler has been
renamed to ElasticaHandler and the new one added as ElasticsearchHandler.

View File

@@ -2,44 +2,55 @@
"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",
"homepage": "https://github.com/Seldaek/monolog",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
"homepage": "https://seld.be"
}
],
"require": {
"php": ">=5.3.0",
"psr/log": "~1.0"
"php": ">=7.2",
"psr/log": "^1.0.1 || ^2.0 || ^3.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",
"ext-json": "*",
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"php-amqplib/php-amqplib": "~2.4",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"graylog2/gelf-php": "^1.4.2",
"guzzlehttp/guzzle": "^7.4",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"phpspec/prophecy": "^1.15",
"phpstan/phpstan": "^0.12.91",
"phpunit/phpunit": "^8.5.14",
"predis/predis": "^1.1 || ^2.0",
"rollbar/rollbar": "^1.3 || ^2 || ^3",
"ruflin/elastica": "^7",
"swiftmailer/swiftmailer": "^5.3|^6.0",
"php-console/php-console": "^3.1.3",
"phpstan/phpstan": "^0.12.59"
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"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",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"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",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"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"
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-openssl": "Required to send log messages using SSL"
},
"autoload": {
"psr-4": {"Monolog\\": "src/Monolog"}
@@ -48,13 +59,23 @@
"psr-4": {"Monolog\\": "tests/Monolog"}
},
"provide": {
"psr/log-implementation": "1.0.0"
"psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0"
},
"extra": {
"branch-alias": {
"dev-main": "2.x-dev"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"phpstan": "vendor/bin/phpstan analyse"
"test": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan analyse"
},
"config": {
"lock": false
"lock": false,
"sort-packages": true,
"platform-check": false,
"allow-plugins": {
"composer/package-versions-deprecated": true
}
}
}

View File

@@ -1,16 +0,0 @@
parameters:
level: 3
paths:
- src/
# - tests/
ignoreErrors:
- '#zend_monitor_|ZEND_MONITOR_#'
- '#RollbarNotifier#'
- '#Predis\\Client#'
- '#^Cannot call method ltrim\(\) on int\|false.$#'
- '#^Access to an undefined property Raven_Client::\$context.$#'
- '#MongoDB\\(Client|Collection)#'
- '#Gelf\\IMessagePublisher#'

View File

@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/*
* 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\Attribute;
/**
* A reusable attribute to help configure a class or a method as a processor.
*
* Using it offers no guarantee: it needs to be leveraged by a Monolog third-party consumer.
*
* Using it with the Monolog library only has no effect at all: processors should still be turned into a callable if
* needed and manually pushed to the loggers and to the processable handlers.
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsMonologProcessor
{
/** @var string|null */
public $channel = null;
/** @var string|null */
public $handler = null;
/** @var string|null */
public $method = null;
/**
* @param string|null $channel The logging channel the processor should be pushed to.
* @param string|null $handler The handler the processor should be pushed to.
* @param string|null $method The method that processes the records (if the attribute is used at the class level).
*/
public function __construct(
?string $channel = null,
?string $handler = null,
?string $method = null
) {
$this->channel = $channel;
$this->handler = $handler;
$this->method = $method;
}
}

View File

@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/*
* 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 DateTimeZone;
/**
* Overrides default json encoding of date time objects
*
* @author Menno Holtkamp
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class DateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable
{
/**
* @var bool
*/
private $useMicroseconds;
public function __construct(bool $useMicroseconds, ?DateTimeZone $timezone = null)
{
$this->useMicroseconds = $useMicroseconds;
parent::__construct('now', $timezone);
}
public function jsonSerialize(): string
{
if ($this->useMicroseconds) {
return $this->format('Y-m-d\TH:i:s.uP');
}
return $this->format('Y-m-d\TH:i:sP');
}
public function __toString(): string
{
return $this->jsonSerialize();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -13,7 +13,6 @@ namespace Monolog;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Monolog\Handler\AbstractHandler;
/**
* Monolog error handler
@@ -26,20 +25,31 @@ use Monolog\Handler\AbstractHandler;
*/
class ErrorHandler
{
/** @var LoggerInterface */
private $logger;
private $previousExceptionHandler;
private $uncaughtExceptionLevel;
/** @var ?callable */
private $previousExceptionHandler = null;
/** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
private $uncaughtExceptionLevelMap = [];
private $previousErrorHandler;
private $errorLevelMap;
private $handleOnlyReportedErrors;
/** @var callable|true|null */
private $previousErrorHandler = null;
/** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
private $errorLevelMap = [];
/** @var bool */
private $handleOnlyReportedErrors = true;
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);
/** @var bool */
private $hasFatalErrorHandler = false;
/** @var LogLevel::* */
private $fatalLevel = LogLevel::ALERT;
/** @var ?string */
private $reservedMemory = null;
/** @var ?array{type: int, message: string, file: string, line: int, trace: mixed} */
private $lastFatalData = null;
/** @var int[] */
private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
public function __construct(LoggerInterface $logger)
{
@@ -51,24 +61,21 @@ class ErrorHandler
*
* 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
* @param LoggerInterface $logger
* @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
* @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
* @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
* @return ErrorHandler
*/
public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null)
public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
{
//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);
/** @phpstan-ignore-next-line */
$handler = new static($logger);
if ($errorLevelMap !== false) {
$handler->registerErrorHandler($errorLevelMap);
}
if ($exceptionLevel !== false) {
$handler->registerExceptionHandler($exceptionLevel);
if ($exceptionLevelMap !== false) {
$handler->registerExceptionHandler($exceptionLevelMap);
}
if ($fatalLevel !== false) {
$handler->registerFatalHandler($fatalLevel);
@@ -77,38 +84,79 @@ class ErrorHandler
return $handler;
}
public function registerExceptionHandler($level = null, $callPrevious = true)
/**
* @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
* @return $this
*/
public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
{
$prev = set_exception_handler(array($this, 'handleException'));
$this->uncaughtExceptionLevel = $level;
$prev = set_exception_handler(function (\Throwable $e): void {
$this->handleException($e);
});
$this->uncaughtExceptionLevelMap = $levelMap;
foreach ($this->defaultExceptionLevelMap() as $class => $level) {
if (!isset($this->uncaughtExceptionLevelMap[$class])) {
$this->uncaughtExceptionLevelMap[$class] = $level;
}
}
if ($callPrevious && $prev) {
$this->previousExceptionHandler = $prev;
}
return $this;
}
public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true)
/**
* @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
* @return $this
*/
public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
{
$prev = set_error_handler(array($this, 'handleError'), $errorTypes);
$prev = set_error_handler([$this, 'handleError'], $errorTypes);
$this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
if ($callPrevious) {
$this->previousErrorHandler = $prev ?: true;
} else {
$this->previousErrorHandler = null;
}
$this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
return $this;
}
public function registerFatalHandler($level = null, $reservedMemorySize = 20)
/**
* @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT
* @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done
*/
public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
{
register_shutdown_function(array($this, 'handleFatalError'));
register_shutdown_function([$this, 'handleFatalError']);
$this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
$this->fatalLevel = $level;
$this->fatalLevel = null === $level ? LogLevel::ALERT : $level;
$this->hasFatalErrorHandler = true;
return $this;
}
protected function defaultErrorLevelMap()
/**
* @return array<class-string, LogLevel::*>
*/
protected function defaultExceptionLevelMap(): array
{
return array(
return [
'ParseError' => LogLevel::CRITICAL,
'Throwable' => LogLevel::ERROR,
];
}
/**
* @return array<int, LogLevel::*>
*/
protected function defaultErrorLevelMap(): array
{
return [
E_ERROR => LogLevel::CRITICAL,
E_WARNING => LogLevel::WARNING,
E_PARSE => LogLevel::ALERT,
@@ -124,22 +172,34 @@ class ErrorHandler
E_RECOVERABLE_ERROR => LogLevel::ERROR,
E_DEPRECATED => LogLevel::NOTICE,
E_USER_DEPRECATED => LogLevel::NOTICE,
);
];
}
/**
* @private
* @phpstan-return never
*/
public function handleException($e)
private function handleException(\Throwable $e): void
{
$level = LogLevel::ERROR;
foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
if ($e instanceof $class) {
$level = $candidate;
break;
}
}
$this->logger->log(
$this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel,
$level,
sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
array('exception' => $e)
['exception' => $e]
);
if ($this->previousExceptionHandler) {
call_user_func($this->previousExceptionHandler, $e);
($this->previousExceptionHandler)($e);
}
if (!headers_sent() && !ini_get('display_errors')) {
http_response_code(500);
}
exit(255);
@@ -147,59 +207,67 @@ class ErrorHandler
/**
* @private
*
* @param mixed[] $context
*/
public function handleError($code, $message, $file = '', $line = 0, $context = array())
public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool
{
if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
return;
return false;
}
// 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));
$level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
$this->logger->log($level, self::codeToString($code).': '.$message, ['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);
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace); // Exclude handleError from trace
$this->lastFatalTrace = $trace;
$this->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $trace];
}
if ($this->previousErrorHandler === true) {
return false;
} elseif ($this->previousErrorHandler) {
return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context);
return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context);
}
return true;
}
/**
* @private
*/
public function handleFatalError()
public function handleFatalError(): void
{
$this->reservedMemory = null;
$this->reservedMemory = '';
if (is_array($this->lastFatalData)) {
$lastError = $this->lastFatalData;
} else {
$lastError = error_get_last();
}
$lastError = error_get_last();
if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
$trace = $lastError['trace'] ?? null;
$this->logger->log(
$this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel,
$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)
['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace]
);
if ($this->logger instanceof Logger) {
foreach ($this->logger->getHandlers() as $handler) {
if ($handler instanceof AbstractHandler) {
$handler->close();
}
$handler->close();
}
}
}
}
private static function codeToString($code)
/**
* @param int $code
*/
private static function codeToString($code): string
{
switch ($code) {
case E_ERROR:

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -22,8 +22,10 @@ class ChromePHPFormatter implements FormatterInterface
{
/**
* Translates Monolog log levels to Wildfire levels.
*
* @var array<int, 'log'|'info'|'warn'|'error'>
*/
private $logLevels = array(
private $logLevels = [
Logger::DEBUG => 'log',
Logger::INFO => 'info',
Logger::NOTICE => 'info',
@@ -32,10 +34,10 @@ class ChromePHPFormatter implements FormatterInterface
Logger::CRITICAL => 'error',
Logger::ALERT => 'error',
Logger::EMERGENCY => 'error',
);
];
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
{
@@ -46,7 +48,7 @@ class ChromePHPFormatter implements FormatterInterface
unset($record['extra']['file'], $record['extra']['line']);
}
$message = array('message' => $record['message']);
$message = ['message' => $record['message']];
if ($record['context']) {
$message['context'] = $record['context'];
}
@@ -57,17 +59,20 @@ class ChromePHPFormatter implements FormatterInterface
$message = reset($message);
}
return array(
return [
$record['channel'],
$message,
$backtrace,
$this->logLevels[$record['level']],
);
];
}
/**
* {@inheritDoc}
*/
public function formatBatch(array $records)
{
$formatted = array();
$formatted = [];
foreach ($records as $record) {
$formatted[] = $this->format($record);

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -17,6 +17,8 @@ use Elastica\Document;
* Format a log message into an Elastica Document
*
* @author Jelle Vink <jelle.vink@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class ElasticaFormatter extends NormalizerFormatter
{
@@ -26,15 +28,15 @@ class ElasticaFormatter extends NormalizerFormatter
protected $index;
/**
* @var string Elastic search document type
* @var ?string Elastic search document type
*/
protected $type;
/**
* @param string $index Elastic Search index name
* @param string $type Elastic Search document type
* @param string $index Elastic Search index name
* @param ?string $type Elastic Search document type, deprecated as of Elastica 7
*/
public function __construct($index, $type)
public function __construct(string $index, ?string $type)
{
// elasticsearch requires a ISO 8601 format date with optional millisecond precision.
parent::__construct('Y-m-d\TH:i:s.uP');
@@ -44,7 +46,7 @@ class ElasticaFormatter extends NormalizerFormatter
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
{
@@ -53,35 +55,33 @@ class ElasticaFormatter extends NormalizerFormatter
return $this->getDocument($record);
}
/**
* Getter index
* @return string
*/
public function getIndex()
public function getIndex(): string
{
return $this->index;
}
/**
* Getter type
* @return string
* @deprecated since Elastica 7 type has no effect
*/
public function getType()
public function getType(): string
{
/** @phpstan-ignore-next-line */
return $this->type;
}
/**
* Convert a log message into an Elastica Document
*
* @param array $record Log message
* @return Document
* @phpstan-param Record $record
*/
protected function getDocument($record)
protected function getDocument(array $record): Document
{
$document = new Document();
$document->setData($record);
$document->setType($this->type);
if (method_exists($document, 'setType')) {
/** @phpstan-ignore-next-line */
$document->setType($this->type);
}
$document->setIndex($this->index);
return $document;

View File

@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
/*
* 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 DateTimeInterface;
/**
* Format a log message into an Elasticsearch record
*
* @author Avtandil Kikabidze <akalongman@gmail.com>
*/
class ElasticsearchFormatter extends NormalizerFormatter
{
/**
* @var string Elasticsearch index name
*/
protected $index;
/**
* @var string Elasticsearch record type
*/
protected $type;
/**
* @param string $index Elasticsearch index name
* @param string $type Elasticsearch record type
*/
public function __construct(string $index, string $type)
{
// Elasticsearch requires an ISO 8601 format date with optional millisecond precision.
parent::__construct(DateTimeInterface::ISO8601);
$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(): string
{
return $this->index;
}
/**
* Getter type
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Convert a log message into an Elasticsearch record
*
* @param mixed[] $record Log message
* @return mixed[]
*/
protected function getDocument(array $record): array
{
$record['_index'] = $this->index;
$record['_type'] = $this->type;
return $record;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -28,26 +28,24 @@ class FlowdockFormatter implements FormatterInterface
*/
private $sourceEmail;
/**
* @param string $source
* @param string $sourceEmail
*/
public function __construct($source, $sourceEmail)
public function __construct(string $source, string $sourceEmail)
{
$this->source = $source;
$this->sourceEmail = $sourceEmail;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @return mixed[]
*/
public function format(array $record)
public function format(array $record): array
{
$tags = array(
$tags = [
'#logs',
'#' . strtolower($record['level_name']),
'#' . $record['channel'],
);
];
foreach ($record['extra'] as $value) {
$tags[] = '#' . $value;
@@ -60,24 +58,26 @@ class FlowdockFormatter implements FormatterInterface
$this->getShortMessage($record['message'])
);
$record['flowdock'] = array(
$record['flowdock'] = [
'source' => $this->source,
'from_address' => $this->sourceEmail,
'subject' => $subject,
'content' => $record['message'],
'tags' => $tags,
'project' => $this->source,
);
];
return $record;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @return mixed[][]
*/
public function formatBatch(array $records)
public function formatBatch(array $records): array
{
$formatted = array();
$formatted = [];
foreach ($records as $record) {
$formatted[] = $this->format($record);
@@ -86,12 +86,7 @@ class FlowdockFormatter implements FormatterInterface
return $formatted;
}
/**
* @param string $message
*
* @return string
*/
public function getShortMessage($message)
public function getShortMessage(string $message): string
{
static $hasMbString;

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -41,42 +41,42 @@ class FluentdFormatter implements FormatterInterface
*/
protected $levelTag = false;
public function __construct($levelTag = false)
public function __construct(bool $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;
$this->levelTag = $levelTag;
}
public function isUsingLevelsInTag()
public function isUsingLevelsInTag(): bool
{
return $this->levelTag;
}
public function format(array $record)
public function format(array $record): string
{
$tag = $record['channel'];
if ($this->levelTag) {
$tag .= '.' . strtolower($record['level_name']);
}
$message = array(
$message = [
'message' => $record['message'],
'context' => $record['context'],
'extra' => $record['extra'],
);
];
if (!$this->levelTag) {
$message['level'] = $record['level'];
$message['level_name'] = $record['level_name'];
}
return Utils::jsonEncode(array($tag, $record['datetime']->getTimestamp(), $message));
return Utils::jsonEncode([$tag, $record['datetime']->getTimestamp(), $message]);
}
public function formatBatch(array $records)
public function formatBatch(array $records): string
{
$message = '';
foreach ($records as $record) {

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -15,6 +15,8 @@ namespace Monolog\Formatter;
* Interface for formatters
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
interface FormatterInterface
{
@@ -23,6 +25,8 @@ interface FormatterInterface
*
* @param array $record A record to format
* @return mixed The formatted record
*
* @phpstan-param Record $record
*/
public function format(array $record);
@@ -31,6 +35,8 @@ interface FormatterInterface
*
* @param array $records A set of records to format
* @return mixed The formatted set of records
*
* @phpstan-param Record[] $records
*/
public function formatBatch(array $records);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -13,16 +13,19 @@ namespace Monolog\Formatter;
use Monolog\Logger;
use Gelf\Message;
use Monolog\Utils;
/**
* Serializes a log message to GELF
* @see http://www.graylog2.org/about/gelf
* @see http://docs.graylog.org/en/latest/pages/gelf.html
*
* @author Matt Lehner <mlehner@gmail.com>
*
* @phpstan-import-type Level from \Monolog\Logger
*/
class GelfMessageFormatter extends NormalizerFormatter
{
const DEFAULT_MAX_LENGTH = 32766;
protected const DEFAULT_MAX_LENGTH = 32766;
/**
* @var string the name of the system for the Gelf log message
@@ -46,8 +49,12 @@ class GelfMessageFormatter extends NormalizerFormatter
/**
* Translates Monolog log levels to Graylog2 log priorities.
*
* @var array<int, int>
*
* @phpstan-var array<Level, int>
*/
private $logLevels = array(
private $logLevels = [
Logger::DEBUG => 7,
Logger::INFO => 6,
Logger::NOTICE => 5,
@@ -56,25 +63,37 @@ class GelfMessageFormatter extends NormalizerFormatter
Logger::CRITICAL => 2,
Logger::ALERT => 1,
Logger::EMERGENCY => 0,
);
];
public function __construct($systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $maxLength = null)
public function __construct(?string $systemName = null, ?string $extraPrefix = null, string $contextPrefix = 'ctxt_', ?int $maxLength = null)
{
if (!class_exists(Message::class)) {
throw new \RuntimeException('Composer package graylog2/gelf-php is required to use Monolog\'s GelfMessageFormatter');
}
parent::__construct('U.u');
$this->systemName = $systemName ?: gethostname();
$this->systemName = (is_null($systemName) || $systemName === '') ? (string) gethostname() : $systemName;
$this->extraPrefix = $extraPrefix;
$this->extraPrefix = is_null($extraPrefix) ? '' : $extraPrefix;
$this->contextPrefix = $contextPrefix;
$this->maxLength = is_null($maxLength) ? self::DEFAULT_MAX_LENGTH : $maxLength;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
public function format(array $record): Message
{
$record = parent::format($record);
$context = $extra = [];
if (isset($record['context'])) {
/** @var mixed[] $context */
$context = parent::normalize($record['context']);
}
if (isset($record['extra'])) {
/** @var mixed[] $extra */
$extra = parent::normalize($record['extra']);
}
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');
@@ -87,47 +106,50 @@ class GelfMessageFormatter extends NormalizerFormatter
->setHost($this->systemName)
->setLevel($this->logLevels[$record['level']]);
// message length + system name length + 200 for padding / metadata
// 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));
$message->setShortMessage(Utils::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($extra['line'])) {
$message->setLine($extra['line']);
unset($extra['line']);
}
if (isset($record['extra']['file'])) {
$message->setFile($record['extra']['file']);
unset($record['extra']['file']);
if (isset($extra['file'])) {
$message->setFile($extra['file']);
unset($extra['file']);
}
foreach ($record['extra'] as $key => $val) {
foreach ($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, Utils::substr((string) $val, 0, $this->maxLength));
continue;
}
$message->setAdditional($this->extraPrefix . $key, $val);
}
foreach ($record['context'] as $key => $val) {
foreach ($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, Utils::substr((string) $val, 0, $this->maxLength));
continue;
}
$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)) {
/** @phpstan-ignore-next-line */
if (null === $message->getFile() && isset($context['exception']['file'])) {
if (preg_match("/^(.+):([0-9]+)$/", $context['exception']['file'], $matches)) {
$message->setFile($matches[1]);
$message->setLine($matches[2]);
}

View File

@@ -0,0 +1,39 @@
<?php declare(strict_types=1);
/*
* 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 DateTimeInterface;
use Monolog\LogRecord;
/**
* Encodes message information into JSON in a format compatible with Cloud logging.
*
* @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
*
* @author Luís Cobucci <lcobucci@gmail.com>
*/
final class GoogleCloudLoggingFormatter extends JsonFormatter
{
/** {@inheritdoc} **/
public function format(array $record): string
{
// Re-key level for GCP logging
$record['severity'] = $record['level_name'];
$record['timestamp'] = $record['datetime']->format(DateTimeInterface::RFC3339_EXTENDED);
// Remove keys that are not used by GCP
unset($record['level'], $record['level_name'], $record['datetime']);
return parent::format($record);
}
}

View File

@@ -1,4 +1,5 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
*
@@ -24,22 +25,24 @@ class HtmlFormatter extends NormalizerFormatter
{
/**
* Translates Monolog log levels to html color priorities.
*
* @var array<int, string>
*/
protected $logLevels = array(
Logger::DEBUG => '#cccccc',
Logger::INFO => '#468847',
Logger::NOTICE => '#3a87ad',
Logger::WARNING => '#c09853',
Logger::ERROR => '#f0ad4e',
Logger::CRITICAL => '#FF7708',
Logger::ALERT => '#C12A19',
protected $logLevels = [
Logger::DEBUG => '#CCCCCC',
Logger::INFO => '#28A745',
Logger::NOTICE => '#17A2B8',
Logger::WARNING => '#FFC107',
Logger::ERROR => '#FD7E14',
Logger::CRITICAL => '#DC3545',
Logger::ALERT => '#821722',
Logger::EMERGENCY => '#000000',
);
];
/**
* @param string $dateFormat The format of the timestamp: one supported by DateTime::format
* @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
*/
public function __construct($dateFormat = null)
public function __construct(?string $dateFormat = null)
{
parent::__construct($dateFormat);
}
@@ -47,12 +50,11 @@ class HtmlFormatter extends NormalizerFormatter
/**
* 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
* @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
*/
protected function addRow($th, $td = ' ', $escapeTd = true)
protected function addRow(string $th, string $td = ' ', bool $escapeTd = true): string
{
$th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8');
if ($escapeTd) {
@@ -69,7 +71,7 @@ class HtmlFormatter extends NormalizerFormatter
* @param int $level Error level
* @return string
*/
protected function addTitle($title, $level)
protected function addTitle(string $title, int $level): string
{
$title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8');
@@ -79,21 +81,20 @@ class HtmlFormatter extends NormalizerFormatter
/**
* Formats a log record.
*
* @param array $record A record to format
* @return mixed The formatted record
* @return string The formatted record
*/
public function format(array $record)
public function format(array $record): string
{
$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('Time', $this->formatDate($record['datetime']));
$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 .= $this->addRow((string) $key, $this->convertToString($value));
}
$embeddedTable .= '</table>';
$output .= $this->addRow('Context', $embeddedTable, false);
@@ -101,7 +102,7 @@ class HtmlFormatter extends NormalizerFormatter
if ($record['extra']) {
$embeddedTable = '<table cellspacing="1" width="100%">';
foreach ($record['extra'] as $key => $value) {
$embeddedTable .= $this->addRow($key, $this->convertToString($value));
$embeddedTable .= $this->addRow((string) $key, $this->convertToString($value));
}
$embeddedTable .= '</table>';
$output .= $this->addRow('Extra', $embeddedTable, false);
@@ -113,10 +114,9 @@ class HtmlFormatter extends NormalizerFormatter
/**
* Formats a set of log records.
*
* @param array $records A set of records to format
* @return mixed The formatted set of records
* @return string The formatted set of records
*/
public function formatBatch(array $records)
public function formatBatch(array $records): string
{
$message = '';
foreach ($records as $record) {
@@ -126,17 +126,17 @@ class HtmlFormatter extends NormalizerFormatter
return $message;
}
protected function convertToString($data)
/**
* @param mixed $data
*/
protected function convertToString($data): string
{
if (null === $data || is_scalar($data)) {
return (string) $data;
}
$data = $this->normalize($data);
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
return Utils::jsonEncode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, true);
}
return str_replace('\\/', '/', Utils::jsonEncode($data, null, true));
return Utils::jsonEncode($data, JSON_PRETTY_PRINT | Utils::DEFAULT_JSON_FLAGS, true);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,8 +11,6 @@
namespace Monolog\Formatter;
use Exception;
use Monolog\Utils;
use Throwable;
/**
@@ -21,30 +19,34 @@ use Throwable;
* This can be useful to log to databases or remote APIs
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class JsonFormatter extends NormalizerFormatter
{
const BATCH_MODE_JSON = 1;
const BATCH_MODE_NEWLINES = 2;
public const BATCH_MODE_JSON = 1;
public const BATCH_MODE_NEWLINES = 2;
/** @var self::BATCH_MODE_* */
protected $batchMode;
/** @var bool */
protected $appendNewline;
/**
* @var bool
*/
/** @var bool */
protected $ignoreEmptyContextAndExtra;
/** @var bool */
protected $includeStacktraces = false;
/**
* @param int $batchMode
* @param bool $appendNewline
* @param int $maxDepth
* @param self::BATCH_MODE_* $batchMode
*/
public function __construct($batchMode = self::BATCH_MODE_JSON, $appendNewline = true, $maxDepth = 9)
public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
{
parent::__construct(null, $maxDepth);
$this->batchMode = $batchMode;
$this->appendNewline = $appendNewline;
$this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
$this->includeStacktraces = $includeStacktraces;
parent::__construct();
}
/**
@@ -53,36 +55,49 @@ class JsonFormatter extends NormalizerFormatter
* formatted as a JSON-encoded array. However, for
* compatibility with some API endpoints, alternative styles
* are available.
*
* @return int
*/
public function getBatchMode()
public function getBatchMode(): int
{
return $this->batchMode;
}
/**
* True if newlines are appended to every formatted record
*
* @return bool
*/
public function isAppendingNewlines()
public function isAppendingNewlines(): bool
{
return $this->appendNewline;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
public function format(array $record): string
{
return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : '');
$normalized = $this->normalize($record);
if (isset($normalized['context']) && $normalized['context'] === []) {
if ($this->ignoreEmptyContextAndExtra) {
unset($normalized['context']);
} else {
$normalized['context'] = new \stdClass;
}
}
if (isset($normalized['extra']) && $normalized['extra'] === []) {
if ($this->ignoreEmptyContextAndExtra) {
unset($normalized['extra']);
} else {
$normalized['extra'] = new \stdClass;
}
}
return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : '');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function formatBatch(array $records)
public function formatBatch(array $records): string
{
switch ($this->batchMode) {
case static::BATCH_MODE_NEWLINES:
@@ -95,20 +110,21 @@ class JsonFormatter extends NormalizerFormatter
}
/**
* @param bool $include
* @return self
*/
public function includeStacktraces($include = true)
public function includeStacktraces(bool $include = true): self
{
$this->includeStacktraces = $include;
return $this;
}
/**
* Return a JSON-encoded array of records.
*
* @param array $records
* @return string
* @phpstan-param Record[] $records
*/
protected function formatBatchJson(array $records)
protected function formatBatchJson(array $records): string
{
return $this->toJson($this->normalize($records), true);
}
@@ -117,10 +133,9 @@ class JsonFormatter extends NormalizerFormatter
* Use new lines to separate records instead of a
* JSON-encoded array.
*
* @param array $records
* @return string
* @phpstan-param Record[] $records
*/
protected function formatBatchNewlines(array $records)
protected function formatBatchNewlines(array $records): string
{
$instance = $this;
@@ -141,30 +156,47 @@ class JsonFormatter extends NormalizerFormatter
*
* @return mixed
*/
protected function normalize($data, $depth = 0)
protected function normalize($data, int $depth = 0)
{
if ($depth > $this->maxDepth) {
return 'Over '.$this->maxDepth.' levels deep, aborting normalization';
if ($depth > $this->maxNormalizeDepth) {
return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization';
}
if (is_array($data)) {
$normalized = array();
$normalized = [];
$count = 1;
foreach ($data as $key => $value) {
if ($count++ > 1000) {
$normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
if ($count++ > $this->maxNormalizeItemCount) {
$normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization';
break;
}
$normalized[$key] = $this->normalize($value, $depth+1);
$normalized[$key] = $this->normalize($value, $depth + 1);
}
return $normalized;
}
if ($data instanceof Exception || $data instanceof Throwable) {
return $this->normalizeException($data);
if (is_object($data)) {
if ($data instanceof \DateTimeInterface) {
return $this->formatDate($data);
}
if ($data instanceof Throwable) {
return $this->normalizeException($data, $depth);
}
// if the object has specific json serializability we want to make sure we skip the __toString treatment below
if ($data instanceof \JsonSerializable) {
return $data;
}
if (method_exists($data, '__toString')) {
return $data->__toString();
}
return $data;
}
if (is_resource($data)) {
@@ -178,35 +210,13 @@ class JsonFormatter extends NormalizerFormatter
* Normalizes given exception with or without its own stack trace based on
* `includeStacktraces` property.
*
* @param Exception|Throwable $e
*
* @return array
* {@inheritDoc}
*/
protected function normalizeException($e)
protected function normalizeException(Throwable $e, int $depth = 0): array
{
// 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' => (int) $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'];
}
}
}
if ($previous = $e->getPrevious()) {
$data['previous'] = $this->normalizeException($previous);
$data = parent::normalizeException($e, $depth);
if (!$this->includeStacktraces) {
unset($data['trace']);
}
return $data;

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -23,49 +23,63 @@ use Monolog\Utils;
*/
class LineFormatter extends NormalizerFormatter
{
const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
/** @var string */
protected $format;
/** @var bool */
protected $allowInlineLineBreaks;
/** @var bool */
protected $ignoreEmptyContextAndExtra;
/** @var bool */
protected $includeStacktraces;
/** @var ?callable */
protected $stacktracesParser;
/**
* @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
* @param string|null $format The format of the message
* @param string|null $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)
public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
{
$this->format = $format ?: static::SIMPLE_FORMAT;
$this->format = $format === null ? static::SIMPLE_FORMAT : $format;
$this->allowInlineLineBreaks = $allowInlineLineBreaks;
$this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
$this->includeStacktraces($includeStacktraces);
parent::__construct($dateFormat);
}
public function includeStacktraces($include = true)
public function includeStacktraces(bool $include = true, ?callable $parser = null): self
{
$this->includeStacktraces = $include;
if ($this->includeStacktraces) {
$this->allowInlineLineBreaks = true;
$this->stacktracesParser = $parser;
}
return $this;
}
public function allowInlineLineBreaks($allow = true)
public function allowInlineLineBreaks(bool $allow = true): self
{
$this->allowInlineLineBreaks = $allow;
return $this;
}
public function ignoreEmptyContextAndExtra($ignore = true)
public function ignoreEmptyContextAndExtra(bool $ignore = true): self
{
$this->ignoreEmptyContextAndExtra = $ignore;
return $this;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
public function format(array $record): string
{
$vars = parent::format($record);
@@ -78,7 +92,6 @@ class LineFormatter extends NormalizerFormatter
}
}
foreach ($vars['context'] as $var => $val) {
if (false !== strpos($output, '%context.'.$var.'%')) {
$output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
@@ -107,12 +120,16 @@ class LineFormatter extends NormalizerFormatter
// remove leftover %extra.xxx% and %context.xxx% if any
if (false !== strpos($output, '%')) {
$output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
if (null === $output) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
}
return $output;
}
public function formatBatch(array $records)
public function formatBatch(array $records): string
{
$message = '';
foreach ($records as $record) {
@@ -122,34 +139,37 @@ class LineFormatter extends NormalizerFormatter
return $message;
}
public function stringify($value)
/**
* @param mixed $value
*/
public function stringify($value): string
{
return $this->replaceNewlines($this->convertToString($value));
}
protected function normalizeException($e)
protected function normalizeException(\Throwable $e, int $depth = 0): string
{
// 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));
}
$str = $this->formatException($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());
}
$depth++;
if ($depth > $this->maxNormalizeDepth) {
$str .= '\n[previous exception] Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
break;
}
$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";
$str .= "\n[previous exception] " . $this->formatException($previous);
} while ($previous = $previous->getPrevious());
}
return $str;
}
protected function convertToString($data)
/**
* @param mixed $data
*/
protected function convertToString($data): string
{
if (null === $data || is_bool($data)) {
return var_export($data, true);
@@ -159,23 +179,68 @@ class LineFormatter extends NormalizerFormatter
return (string) $data;
}
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
return $this->toJson($data, true);
}
return str_replace('\\/', '/', $this->toJson($data, true));
return $this->toJson($data, true);
}
protected function replaceNewlines($str)
protected function replaceNewlines(string $str): string
{
if ($this->allowInlineLineBreaks) {
if (0 === strpos($str, '{')) {
return str_replace(array('\r', '\n'), array("\r", "\n"), $str);
$str = preg_replace('/(?<!\\\\)\\\\[rn]/', "\n", $str);
if (null === $str) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
}
return $str;
}
return str_replace(array("\r\n", "\r", "\n"), ' ', $str);
return str_replace(["\r\n", "\r", "\n"], ' ', $str);
}
private function formatException(\Throwable $e): string
{
$str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode();
if ($e instanceof \SoapFault) {
if (isset($e->faultcode)) {
$str .= ' faultcode: ' . $e->faultcode;
}
if (isset($e->faultactor)) {
$str .= ' faultactor: ' . $e->faultactor;
}
if (isset($e->detail)) {
if (is_string($e->detail)) {
$str .= ' detail: ' . $e->detail;
} elseif (is_object($e->detail) || is_array($e->detail)) {
$str .= ' detail: ' . $this->toJson($e->detail, true);
}
}
}
$str .= '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')';
if ($this->includeStacktraces) {
$str .= $this->stacktracesParser($e);
}
return $str;
}
private function stacktracesParser(\Throwable $e): string
{
$trace = $e->getTraceAsString();
if ($this->stacktracesParser) {
$trace = $this->stacktracesParserCustom($trace);
}
return "\n[stacktrace]\n" . $trace . "\n";
}
private function stacktracesParserCustom(string $trace): string
{
return implode("\n", array_filter(array_map($this->stacktracesParser, explode("\n", $trace))));
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -21,10 +21,8 @@ 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)
public function __construct(int $batchMode = self::BATCH_MODE_NEWLINES, bool $appendNewline = false)
{
parent::__construct($batchMode, $appendNewline);
}
@@ -35,11 +33,11 @@ class LogglyFormatter extends JsonFormatter
* @see https://www.loggly.com/docs/automated-parsing/#json
* @see \Monolog\Formatter\JsonFormatter::format()
*/
public function format(array $record)
public function format(array $record): string
{
if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTime)) {
if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTimeInterface)) {
$record["timestamp"] = $record["datetime"]->format("Y-m-d\TH:i:s.uO");
// TODO 2.0 unset the 'datetime' parameter, retained for BC
unset($record["datetime"]);
}
return parent::format($record);

View File

@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
/*
* 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 Logmatic.
*
* @author Julien Breux <julien.breux@gmail.com>
*/
class LogmaticFormatter extends JsonFormatter
{
protected const MARKERS = ["sourcecode", "php"];
/**
* @var string
*/
protected $hostname = '';
/**
* @var string
*/
protected $appname = '';
public function setHostname(string $hostname): self
{
$this->hostname = $hostname;
return $this;
}
public function setAppname(string $appname): self
{
$this->appname = $appname;
return $this;
}
/**
* Appends the 'hostname' and 'appname' parameter for indexing by Logmatic.
*
* @see http://doc.logmatic.io/docs/basics-to-send-data
* @see \Monolog\Formatter\JsonFormatter::format()
*/
public function format(array $record): string
{
if (!empty($this->hostname)) {
$record["hostname"] = $this->hostname;
}
if (!empty($this->appname)) {
$record["appname"] = $this->appname;
}
$record["@marker"] = static::MARKERS;
return parent::format($record);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -14,16 +14,13 @@ 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
* @see https://www.elastic.co/products/logstash
* @see https://github.com/elastic/logstash/blob/master/logstash-core/src/main/java/org/logstash/Event.java
*
* @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
*/
@@ -35,108 +32,47 @@ class LogstashFormatter extends NormalizerFormatter
protected $applicationName;
/**
* @var string a prefix for 'extra' fields from the Monolog record (optional)
* @var string the key for 'extra' fields from the Monolog record
*/
protected $extraPrefix;
protected $extraKey;
/**
* @var string a prefix for 'context' fields from the Monolog record (optional)
* @var string the key for 'context' fields from the Monolog record
*/
protected $contextPrefix;
protected $contextKey;
/**
* @var int logstash format version to use
* @param string $applicationName The application that sends the data, used as the "type" field of logstash
* @param string|null $systemName The system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine
* @param string $extraKey The key for extra keys inside logstash "fields", defaults to extra
* @param string $contextKey The key for context keys inside logstash "fields", defaults to context
*/
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)
public function __construct(string $applicationName, ?string $systemName = null, string $extraKey = 'extra', string $contextKey = 'context')
{
// 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->systemName = $systemName === null ? (string) gethostname() : $systemName;
$this->applicationName = $applicationName;
$this->extraPrefix = $extraPrefix;
$this->contextPrefix = $contextPrefix;
$this->version = $version;
$this->extraKey = $extraKey;
$this->contextKey = $contextKey;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function format(array $record)
public function format(array $record): string
{
$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(
$message = [
'@timestamp' => $record['datetime'],
'@version' => 1,
'host' => $this->systemName,
);
];
if (isset($record['message'])) {
$message['message'] = $record['message'];
}
@@ -147,20 +83,19 @@ class LogstashFormatter extends NormalizerFormatter
if (isset($record['level_name'])) {
$message['level'] = $record['level_name'];
}
if (isset($record['level'])) {
$message['monolog_level'] = $record['level'];
}
if ($this->applicationName) {
$message['type'] = $this->applicationName;
}
if (!empty($record['extra'])) {
foreach ($record['extra'] as $key => $val) {
$message[$this->extraPrefix . $key] = $val;
}
$message[$this->extraKey] = $record['extra'];
}
if (!empty($record['context'])) {
foreach ($record['context'] as $key => $val) {
$message[$this->contextPrefix . $key] = $val;
}
$message[$this->contextKey] = $record['context'];
}
return $message;
return $this->toJson($message) . "\n";
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,6 +11,8 @@
namespace Monolog\Formatter;
use MongoDB\BSON\Type;
use MongoDB\BSON\UTCDateTime;
use Monolog\Utils;
/**
@@ -20,61 +22,83 @@ use Monolog\Utils;
*/
class MongoDBFormatter implements FormatterInterface
{
/** @var bool */
private $exceptionTraceAsString;
/** @var int */
private $maxNestingLevel;
/** @var bool */
private $isLegacyMongoExt;
/**
* @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)
public function __construct(int $maxNestingLevel = 3, bool $exceptionTraceAsString = true)
{
$this->maxNestingLevel = max($maxNestingLevel, 0);
$this->exceptionTraceAsString = (bool) $exceptionTraceAsString;
$this->exceptionTraceAsString = $exceptionTraceAsString;
$this->isLegacyMongoExt = extension_loaded('mongodb') && version_compare((string) phpversion('mongodb'), '1.1.9', '<=');
}
/**
* {@inheritDoc}
*
* @return mixed[]
*/
public function format(array $record)
public function format(array $record): array
{
return $this->formatArray($record);
/** @var mixed[] $res */
$res = $this->formatArray($record);
return $res;
}
/**
* {@inheritDoc}
*
* @return array<mixed[]>
*/
public function formatBatch(array $records)
public function formatBatch(array $records): array
{
$formatted = [];
foreach ($records as $key => $record) {
$records[$key] = $this->format($record);
$formatted[$key] = $this->format($record);
}
return $records;
return $formatted;
}
protected function formatArray(array $record, $nestingLevel = 0)
/**
* @param mixed[] $array
* @return mixed[]|string Array except when max nesting level is reached then a string "[...]"
*/
protected function formatArray(array $array, int $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 = '[...]';
if ($this->maxNestingLevel > 0 && $nestingLevel > $this->maxNestingLevel) {
return '[...]';
}
return $record;
foreach ($array as $name => $value) {
if ($value instanceof \DateTimeInterface) {
$array[$name] = $this->formatDate($value, $nestingLevel + 1);
} elseif ($value instanceof \Throwable) {
$array[$name] = $this->formatException($value, $nestingLevel + 1);
} elseif (is_array($value)) {
$array[$name] = $this->formatArray($value, $nestingLevel + 1);
} elseif (is_object($value) && !$value instanceof Type) {
$array[$name] = $this->formatObject($value, $nestingLevel + 1);
}
}
return $array;
}
protected function formatObject($value, $nestingLevel)
/**
* @param mixed $value
* @return mixed[]|string
*/
protected function formatObject($value, int $nestingLevel)
{
$objectVars = get_object_vars($value);
$objectVars['class'] = Utils::getClass($value);
@@ -82,14 +106,17 @@ class MongoDBFormatter implements FormatterInterface
return $this->formatArray($objectVars, $nestingLevel);
}
protected function formatException(\Exception $exception, $nestingLevel)
/**
* @return mixed[]|string
*/
protected function formatException(\Throwable $exception, int $nestingLevel)
{
$formattedException = array(
$formattedException = [
'class' => Utils::getClass($exception),
'message' => $exception->getMessage(),
'code' => (int) $exception->getCode(),
'file' => $exception->getFile() . ':' . $exception->getLine(),
);
];
if ($this->exceptionTraceAsString === true) {
$formattedException['trace'] = $exception->getTraceAsString();
@@ -100,8 +127,36 @@ class MongoDBFormatter implements FormatterInterface
return $this->formatArray($formattedException, $nestingLevel);
}
protected function formatDate(\DateTime $value, $nestingLevel)
protected function formatDate(\DateTimeInterface $value, int $nestingLevel): UTCDateTime
{
return new \MongoDate($value->getTimestamp());
if ($this->isLegacyMongoExt) {
return $this->legacyGetMongoDbDateTime($value);
}
return $this->getMongoDbDateTime($value);
}
private function getMongoDbDateTime(\DateTimeInterface $value): UTCDateTime
{
return new UTCDateTime((int) floor(((float) $value->format('U.u')) * 1000));
}
/**
* This is needed to support MongoDB Driver v1.19 and below
*
* See https://github.com/mongodb/mongo-php-driver/issues/426
*
* It can probably be removed in 2.1 or later once MongoDB's 1.2 is released and widely adopted
*/
private function legacyGetMongoDbDateTime(\DateTimeInterface $value): UTCDateTime
{
$milliseconds = floor(((float) $value->format('U.u')) * 1000);
$milliseconds = (PHP_INT_SIZE == 8) //64-bit OS?
? (int) $milliseconds
: (string) $milliseconds;
// @phpstan-ignore-next-line
return new UTCDateTime($milliseconds);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,8 +11,9 @@
namespace Monolog\Formatter;
use Exception;
use Monolog\DateTimeImmutable;
use Monolog\Utils;
use Throwable;
/**
* Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
@@ -21,26 +22,33 @@ use Monolog\Utils;
*/
class NormalizerFormatter implements FormatterInterface
{
const SIMPLE_DATE = "Y-m-d H:i:s";
public const SIMPLE_DATE = "Y-m-d\TH:i:sP";
/** @var string */
protected $dateFormat;
protected $maxDepth;
/** @var int */
protected $maxNormalizeDepth = 9;
/** @var int */
protected $maxNormalizeItemCount = 1000;
/** @var int */
private $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS;
/**
* @param string $dateFormat The format of the timestamp: one supported by DateTime::format
* @param int $maxDepth
* @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
*/
public function __construct($dateFormat = null, $maxDepth = 9)
public function __construct(?string $dateFormat = null)
{
$this->dateFormat = $dateFormat ?: static::SIMPLE_DATE;
$this->maxDepth = $maxDepth;
$this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat;
if (!function_exists('json_encode')) {
throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
}
}
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @param mixed[] $record
*/
public function format(array $record)
{
@@ -48,7 +56,7 @@ class NormalizerFormatter implements FormatterInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function formatBatch(array $records)
{
@@ -59,26 +67,70 @@ class NormalizerFormatter implements FormatterInterface
return $records;
}
/**
* @return int
*/
public function getMaxDepth()
public function getDateFormat(): string
{
return $this->maxDepth;
return $this->dateFormat;
}
public function setDateFormat(string $dateFormat): self
{
$this->dateFormat = $dateFormat;
return $this;
}
/**
* @param int $maxDepth
* The maximum number of normalization levels to go through
*/
public function setMaxDepth($maxDepth)
public function getMaxNormalizeDepth(): int
{
$this->maxDepth = $maxDepth;
return $this->maxNormalizeDepth;
}
protected function normalize($data, $depth = 0)
public function setMaxNormalizeDepth(int $maxNormalizeDepth): self
{
if ($depth > $this->maxDepth) {
return 'Over '.$this->maxDepth.' levels deep, aborting normalization';
$this->maxNormalizeDepth = $maxNormalizeDepth;
return $this;
}
/**
* The maximum number of items to normalize per level
*/
public function getMaxNormalizeItemCount(): int
{
return $this->maxNormalizeItemCount;
}
public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): self
{
$this->maxNormalizeItemCount = $maxNormalizeItemCount;
return $this;
}
/**
* Enables `json_encode` pretty print.
*/
public function setJsonPrettyPrint(bool $enable): self
{
if ($enable) {
$this->jsonEncodeOptions |= JSON_PRETTY_PRINT;
} else {
$this->jsonEncodeOptions &= ~JSON_PRETTY_PRINT;
}
return $this;
}
/**
* @param mixed $data
* @return null|scalar|array<array|scalar|null>
*/
protected function normalize($data, int $depth = 0)
{
if ($depth > $this->maxNormalizeDepth) {
return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
}
if (null === $data || is_scalar($data)) {
@@ -95,62 +147,71 @@ class NormalizerFormatter implements FormatterInterface
}
if (is_array($data)) {
$normalized = array();
$normalized = [];
$count = 1;
foreach ($data as $key => $value) {
if ($count++ > 1000) {
$normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
if ($count++ > $this->maxNormalizeItemCount) {
$normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization';
break;
}
$normalized[$key] = $this->normalize($value, $depth+1);
$normalized[$key] = $this->normalize($value, $depth + 1);
}
return $normalized;
}
if ($data instanceof \DateTime) {
return $data->format($this->dateFormat);
if ($data instanceof \DateTimeInterface) {
return $this->formatDate($data);
}
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);
if ($data instanceof Throwable) {
return $this->normalizeException($data, $depth);
}
// non-serializable objects that implement __toString stringified
if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) {
if ($data instanceof \JsonSerializable) {
/** @var null|scalar|array<array|scalar|null> $value */
$value = $data->jsonSerialize();
} elseif (method_exists($data, '__toString')) {
/** @var string $value */
$value = $data->__toString();
} else {
// the rest is json-serialized in some way
$value = $this->toJson($data, true);
// the rest is normalized by json encoding and decoding it
/** @var null|scalar|array<array|scalar|null> $value */
$value = json_decode($this->toJson($data, true), true);
}
return sprintf("[object] (%s: %s)", Utils::getClass($data), $value);
return [Utils::getClass($data) => $value];
}
if (is_resource($data)) {
return sprintf('[resource] (%s)', get_resource_type($data));
return sprintf('[resource(%s)]', get_resource_type($data));
}
return '[unknown('.gettype($data).')]';
}
protected function normalizeException($e)
/**
* @return mixed[]
*/
protected function normalizeException(Throwable $e, int $depth = 0)
{
// 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));
if ($depth > $this->maxNormalizeDepth) {
return ['Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'];
}
$data = array(
if ($e instanceof \JsonSerializable) {
return (array) $e->jsonSerialize();
}
$data = [
'class' => Utils::getClass($e),
'message' => $e->getMessage(),
'code' => (int) $e->getCode(),
'file' => $e->getFile().':'.$e->getLine(),
);
];
if ($e instanceof \SoapFault) {
if (isset($e->faultcode)) {
@@ -162,7 +223,7 @@ class NormalizerFormatter implements FormatterInterface
}
if (isset($e->detail)) {
if (is_string($e->detail)) {
if (is_string($e->detail)) {
$data['detail'] = $e->detail;
} elseif (is_object($e->detail) || is_array($e->detail)) {
$data['detail'] = $this->toJson($e->detail, true);
@@ -178,7 +239,7 @@ class NormalizerFormatter implements FormatterInterface
}
if ($previous = $e->getPrevious()) {
$data['previous'] = $this->normalizeException($previous);
$data['previous'] = $this->normalizeException($previous, $depth + 1);
}
return $data;
@@ -188,12 +249,39 @@ class NormalizerFormatter implements FormatterInterface
* 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 if encoding fails and ignoreErrors is true 'null' is returned
*/
protected function toJson($data, bool $ignoreErrors = false): string
{
return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors);
}
/**
* @return string
*/
protected function toJson($data, $ignoreErrors = false)
protected function formatDate(\DateTimeInterface $date)
{
return Utils::jsonEncode($data, null, $ignoreErrors);
// in case the date format isn't custom then we defer to the custom DateTimeImmutable
// formatting logic, which will pick the right format based on whether useMicroseconds is on
if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) {
return (string) $date;
}
return $date->format($this->dateFormat);
}
}
public function addJsonEncodeOption(int $option): self
{
$this->jsonEncodeOptions |= $option;
return $this;
}
public function removeJsonEncodeOption(int $option): self
{
$this->jsonEncodeOptions &= ~$option;
return $this;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -20,26 +20,29 @@ namespace Monolog\Formatter;
class ScalarFormatter extends NormalizerFormatter
{
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @phpstan-return array<string, scalar|null> $record
*/
public function format(array $record)
public function format(array $record): array
{
$result = [];
foreach ($record as $key => $value) {
$record[$key] = $this->normalizeValue($value);
$result[$key] = $this->normalizeValue($value);
}
return $record;
return $result;
}
/**
* @param mixed $value
* @return mixed
* @param mixed $value
* @return scalar|null
*/
protected function normalizeValue($value)
{
$normalized = $this->normalize($value);
if (is_array($normalized) || is_object($normalized)) {
if (is_array($normalized)) {
return $this->toJson($normalized, true);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -19,15 +19,17 @@ use Monolog\Logger;
* @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
* @author Christophe Coevoet <stof@notk.org>
* @author Kirill chEbba Chebunin <iam@chebba.org>
*
* @phpstan-import-type Level from \Monolog\Logger
*/
class WildfireFormatter extends NormalizerFormatter
{
const TABLE = 'table';
/**
* Translates Monolog log levels to Wildfire levels.
*
* @var array<Level, string>
*/
private $logLevels = array(
private $logLevels = [
Logger::DEBUG => 'LOG',
Logger::INFO => 'INFO',
Logger::NOTICE => 'INFO',
@@ -36,12 +38,25 @@ class WildfireFormatter extends NormalizerFormatter
Logger::CRITICAL => 'ERROR',
Logger::ALERT => 'ERROR',
Logger::EMERGENCY => 'ERROR',
);
];
/**
* {@inheritdoc}
* @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
*/
public function format(array $record)
public function __construct(?string $dateFormat = null)
{
parent::__construct($dateFormat);
// http headers do not like non-ISO-8559-1 characters
$this->removeJsonEncodeOption(JSON_UNESCAPED_UNICODE);
}
/**
* {@inheritDoc}
*
* @return string
*/
public function format(array $record): string
{
// Retrieve the line and file if set and remove them from the formatted extra
$file = $line = '';
@@ -54,8 +69,9 @@ class WildfireFormatter extends NormalizerFormatter
unset($record['extra']['line']);
}
/** @var mixed[] $record */
$record = $this->normalize($record);
$message = array('message' => $record['message']);
$message = ['message' => $record['message']];
$handleError = false;
if ($record['context']) {
$message['context'] = $record['context'];
@@ -69,42 +85,52 @@ class WildfireFormatter extends NormalizerFormatter
$message = reset($message);
}
if (isset($record['context'][self::TABLE])) {
if (isset($record['context']['table'])) {
$type = 'TABLE';
$label = $record['channel'] .': '. $record['message'];
$message = $record['context'][self::TABLE];
$message = $record['context']['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(
$json = $this->toJson([
[
'Type' => $type,
'File' => $file,
'Line' => $line,
'Label' => $label,
),
],
$message,
), $handleError);
], $handleError);
// The message itself is a serialization of the above JSON object + it's length
return sprintf(
'%s|%s|',
'%d|%s|',
strlen($json),
$json
);
}
/**
* {@inheritDoc}
*
* @phpstan-return never
*/
public function formatBatch(array $records)
{
throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter');
}
protected function normalize($data, $depth = 0)
/**
* {@inheritDoc}
*
* @return null|scalar|array<array|scalar|null>|object
*/
protected function normalize($data, int $depth = 0)
{
if (is_object($data) && !$data instanceof \DateTime) {
if (is_object($data) && !$data instanceof \DateTimeInterface) {
return $data;
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,118 +11,55 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\ResettableInterface;
use Psr\Log\LogLevel;
/**
* Base Handler class providing the Handler structure
* Base Handler class providing basic level/bubble support
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
abstract class AbstractHandler implements HandlerInterface, ResettableInterface
abstract class AbstractHandler extends Handler implements ResettableInterface
{
protected $level = Logger::DEBUG;
protected $bubble = true;
/**
* @var FormatterInterface
* @var int
* @phpstan-var Level
*/
protected $formatter;
protected $processors = array();
protected $level = Logger::DEBUG;
/** @var bool */
protected $bubble = true;
/**
* @param int|string $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
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function __construct($level = Logger::DEBUG, $bubble = true)
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
$this->setLevel($level);
$this->bubble = $bubble;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function isHandling(array $record)
public function isHandling(array $record): bool
{
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
* @param Level|LevelName|LogLevel::* $level Level or level name
* @return self
*/
public function setLevel($level)
public function setLevel($level): self
{
$this->level = Logger::toMonologLevel($level);
@@ -133,8 +70,10 @@ abstract class AbstractHandler implements HandlerInterface, ResettableInterface
* Gets minimum logging level at which this handler will be triggered.
*
* @return int
*
* @phpstan-return Level
*/
public function getLevel()
public function getLevel(): int
{
return $this->level;
}
@@ -146,7 +85,7 @@ abstract class AbstractHandler implements HandlerInterface, ResettableInterface
* false means that bubbling is not permitted.
* @return self
*/
public function setBubble($bubble)
public function setBubble(bool $bubble): self
{
$this->bubble = $bubble;
@@ -159,38 +98,15 @@ abstract class AbstractHandler implements HandlerInterface, ResettableInterface
* @return bool true means that this handler allows bubbling.
* false means that bubbling is not permitted.
*/
public function getBubble()
public function getBubble(): bool
{
return $this->bubble;
}
public function __destruct()
{
try {
$this->close();
} catch (\Exception $e) {
// do nothing
} catch (\Throwable $e) {
// do nothing
}
}
/**
* {@inheritDoc}
*/
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();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,28 +11,37 @@
namespace Monolog\Handler;
use Monolog\ResettableInterface;
/**
* Base Handler class providing the Handler structure
* Base Handler class providing the Handler structure, including processors and formatters
*
* Classes extending it should (in most cases) only implement write($record)
*
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type LevelName from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-type FormattedRecord array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[], formatted: mixed}
*/
abstract class AbstractProcessingHandler extends AbstractHandler
abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
use FormattableHandlerTrait;
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
$record = $this->processRecord($record);
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$record['formatted'] = $this->getFormatter()->format($record);
@@ -44,25 +53,17 @@ abstract class AbstractProcessingHandler extends AbstractHandler
/**
* Writes the record down to the log of the implementing handler
*
* @param array $record
* @return void
* @phpstan-param FormattedRecord $record
*/
abstract protected function write(array $record);
abstract protected function write(array $record): void;
/**
* Processes a record.
*
* @param array $record
* @return array
* @return void
*/
protected function processRecord(array $record)
public function reset()
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
}
parent::reset();
return $record;
$this->resetProcessors();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,19 +12,25 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
/**
* Common syslog functionality
*
* @phpstan-import-type Level from \Monolog\Logger
*/
abstract class AbstractSyslogHandler extends AbstractProcessingHandler
{
/** @var int */
protected $facility;
/**
* Translates Monolog log levels to syslog log priorities.
* @var array
* @phpstan-var array<Level, int>
*/
protected $logLevels = array(
protected $logLevels = [
Logger::DEBUG => LOG_DEBUG,
Logger::INFO => LOG_INFO,
Logger::NOTICE => LOG_NOTICE,
@@ -33,12 +39,13 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler
Logger::CRITICAL => LOG_CRIT,
Logger::ALERT => LOG_ALERT,
Logger::EMERGENCY => LOG_EMERG,
);
];
/**
* List of valid log facility names.
* @var array<string, int>
*/
protected $facilities = array(
protected $facilities = [
'auth' => LOG_AUTH,
'authpriv' => LOG_AUTHPRIV,
'cron' => LOG_CRON,
@@ -50,14 +57,12 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler
'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
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
*/
public function __construct($facility = LOG_USER, $level = Logger::DEBUG, $bubble = true)
public function __construct($facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
@@ -82,7 +87,7 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler
}
// convert textual description of facility to syslog constant
if (array_key_exists(strtolower($facility), $this->facilities)) {
if (is_string($facility) && 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');
@@ -92,9 +97,9 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%');
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,17 +12,46 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\JsonFormatter;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Channel\AMQPChannel;
use AMQPExchange;
/**
* @phpstan-import-type Record from \Monolog\Logger
*/
class AmqpHandler extends AbstractProcessingHandler
{
/**
* @var AMQPExchange|AMQPChannel $exchange
*/
protected $exchange;
/** @var array<string, mixed> */
private $extraAttributes = [];
/**
* @return array<string, mixed>
*/
public function getExtraAttributes(): array
{
return $this->extraAttributes;
}
/**
* Configure extra attributes to pass to the AMQPExchange (if you are using the amqp extension)
*
* @param array<string, mixed> $extraAttributes One of content_type, content_encoding,
* message_id, user_id, app_id, delivery_mode,
* priority, timestamp, expiration, type
* or reply_to, headers.
* @return AmqpHandler
*/
public function setExtraAttributes(array $extraAttributes): self
{
$this->extraAttributes = $extraAttributes;
return $this;
}
/**
* @var string
@@ -31,18 +60,16 @@ class AmqpHandler extends AbstractProcessingHandler
/**
* @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
* @param string|null $exchangeName Optional exchange name, for AMQPChannel (PhpAmqpLib) only
*/
public function __construct($exchange, $exchangeName = 'log', $level = Logger::DEBUG, $bubble = true)
public function __construct($exchange, ?string $exchangeName = null, $level = Logger::DEBUG, bool $bubble = true)
{
if ($exchange instanceof AMQPExchange) {
$exchange->setName($exchangeName);
} elseif ($exchange instanceof AMQPChannel) {
$this->exchangeName = $exchangeName;
} else {
if ($exchange instanceof AMQPChannel) {
$this->exchangeName = (string) $exchangeName;
} elseif (!$exchange instanceof AMQPExchange) {
throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required');
} elseif ($exchangeName) {
@trigger_error('The $exchangeName parameter can only be passed when using PhpAmqpLib, if using an AMQPExchange instance configure it beforehand', E_USER_DEPRECATED);
}
$this->exchange = $exchange;
@@ -52,20 +79,24 @@ class AmqpHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$data = $record["formatted"];
$routingKey = $this->getRoutingKey($record);
if ($this->exchange instanceof AMQPExchange) {
$attributes = [
'delivery_mode' => 2,
'content_type' => 'application/json',
];
if ($this->extraAttributes) {
$attributes = array_merge($attributes, $this->extraAttributes);
}
$this->exchange->publish(
$data,
$routingKey,
0,
array(
'delivery_mode' => 2,
'content_type' => 'application/json',
)
$attributes
);
} else {
$this->exchange->basic_publish(
@@ -79,7 +110,7 @@ class AmqpHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
if ($this->exchange instanceof AMQPExchange) {
parent::handleBatch($records);
@@ -92,6 +123,7 @@ class AmqpHandler extends AbstractProcessingHandler
continue;
}
/** @var Record $record */
$record = $this->processRecord($record);
$data = $this->getFormatter()->format($record);
@@ -108,40 +140,30 @@ class AmqpHandler extends AbstractProcessingHandler
/**
* Gets the routing key for the AMQP exchange
*
* @param array $record
* @return string
* @phpstan-param Record $record
*/
protected function getRoutingKey(array $record)
protected function getRoutingKey(array $record): string
{
$routingKey = sprintf(
'%s.%s',
// TODO 2.0 remove substr call
substr($record['level_name'], 0, 4),
$record['channel']
);
$routingKey = sprintf('%s.%s', $record['level_name'], $record['channel']);
return strtolower($routingKey);
}
/**
* @param string $data
* @return AMQPMessage
*/
private function createAmqpMessage($data)
private function createAmqpMessage(string $data): AMQPMessage
{
return new AMQPMessage(
(string) $data,
array(
$data,
[
'delivery_mode' => 2,
'content_type' => 'application/json',
)
]
);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,17 +11,34 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Utils;
use function count;
use function headers_list;
use function stripos;
use function trigger_error;
use const E_USER_DEPRECATED;
/**
* Handler sending logs to browser's javascript console with no browser extension required
*
* @author Olivier Poitrey <rs@dailymotion.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class BrowserConsoleHandler extends AbstractProcessingHandler
{
/** @var bool */
protected static $initialized = false;
protected static $records = array();
/** @var FormattedRecord[] */
protected static $records = [];
protected const FORMAT_HTML = 'html';
protected const FORMAT_JS = 'js';
protected const FORMAT_UNKNOWN = 'unknown';
/**
* {@inheritDoc}
@@ -32,7 +49,7 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
*
* You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
}
@@ -40,7 +57,7 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
// Accumulate records
static::$records[] = $record;
@@ -56,57 +73,57 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
* 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()
public static function send(): void
{
$format = static::getResponseFormat();
if ($format === 'unknown') {
if ($format === self::FORMAT_UNKNOWN) {
return;
}
if (count(static::$records)) {
if ($format === 'html') {
if ($format === self::FORMAT_HTML) {
static::writeOutput('<script>' . static::generateScript() . '</script>');
} elseif ($format === 'js') {
} elseif ($format === self::FORMAT_JS) {
static::writeOutput(static::generateScript());
}
static::resetStatic();
}
}
public function close()
public function close(): void
{
self::resetStatic();
}
public function reset()
{
parent::reset();
self::resetStatic();
}
/**
* Forget all logged records
*/
public static function resetStatic()
public static function resetStatic(): void
{
static::$records = array();
static::$records = [];
}
/**
* Wrapper for register_shutdown_function to allow overriding
*/
protected function registerShutdownFunction()
protected function registerShutdownFunction(): void
{
if (PHP_SAPI !== 'cli') {
register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']);
}
}
/**
* Wrapper for echo to allow overriding
*
* @param string $str
*/
protected static function writeOutput($str)
protected static function writeOutput(string $str): void
{
echo $str;
}
@@ -119,30 +136,42 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
* If Content-Type is anything else -> unknown
*
* @return string One of 'js', 'html' or 'unknown'
* @phpstan-return self::FORMAT_*
*/
protected static function getResponseFormat()
protected static function getResponseFormat(): string
{
// 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 static::getResponseFormatFromContentType($header);
}
}
return 'html';
return self::FORMAT_HTML;
}
private static function generateScript()
/**
* @return string One of 'js', 'html' or 'unknown'
* @phpstan-return self::FORMAT_*
*/
protected static function getResponseFormatFromContentType(string $contentType): string
{
$script = array();
// This handler only works with HTML and javascript outputs
// text/javascript is obsolete in favour of application/javascript, but still used
if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) {
return self::FORMAT_JS;
}
if (stripos($contentType, 'text/html') !== false) {
return self::FORMAT_HTML;
}
return self::FORMAT_UNKNOWN;
}
private static function generateScript(): string
{
$script = [];
foreach (static::$records as $record) {
$context = static::dump('Context', $record['context']);
$extra = static::dump('Extra', $record['extra']);
@@ -150,11 +179,12 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
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']))),
$script = array_merge(
$script,
[static::call_array('groupCollapsed', static::handleStyles($record['formatted']))],
$context,
$extra,
array(static::call('groupEnd'))
[static::call('groupEnd')]
);
}
}
@@ -162,9 +192,12 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
}
private static function handleStyles($formatted)
/**
* @return string[]
*/
private static function handleStyles(string $formatted): array
{
$args = array();
$args = [];
$format = '%c' . $formatted;
preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
@@ -173,7 +206,7 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
$args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));
$pos = $match[0][1];
$format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0]));
$format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0]));
}
$args[] = static::quote('font-weight: normal');
@@ -182,12 +215,12 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
return array_reverse($args);
}
private static function handleCustomStyles($style, $string)
private static function handleCustomStyles(string $style, string $string): string
{
static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
static $labels = array();
static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey'];
static $labels = [];
return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) {
$style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $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])) {
@@ -200,11 +233,22 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
return $m[1];
}, $style);
if (null === $style) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
return $style;
}
private static function dump($title, array $dict)
/**
* @param mixed[] $dict
* @return mixed[]
*/
private static function dump(string $title, array $dict): array
{
$script = array();
$script = [];
$dict = array_filter($dict);
if (empty($dict)) {
return $script;
@@ -215,26 +259,34 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
if (empty($value)) {
$value = static::quote('');
}
$script[] = static::call('log', static::quote('%s: %o'), static::quote($key), $value);
$script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value);
}
return $script;
}
private static function quote($arg)
private static function quote(string $arg): string
{
return '"' . addcslashes($arg, "\"\n\\") . '"';
}
private static function call()
/**
* @param mixed $args
*/
private static function call(...$args): string
{
$args = func_get_args();
$method = array_shift($args);
if (!is_string($method)) {
throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true));
}
return static::call_array($method, $args);
}
private static function call_array($method, array $args)
/**
* @param mixed[] $args
*/
private static function call_array(string $method, array $args): string
{
return 'c.' . $method . '(' . implode(', ', $args) . ');';
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -22,35 +22,43 @@ use Monolog\Formatter\FormatterInterface;
* sending one per log message.
*
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class BufferHandler extends AbstractHandler
class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/** @var HandlerInterface */
protected $handler;
/** @var int */
protected $bufferSize = 0;
/** @var int */
protected $bufferLimit;
/** @var bool */
protected $flushOnOverflow;
protected $buffer = array();
/** @var Record[] */
protected $buffer = [];
/** @var bool */
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)
public function __construct(HandlerInterface $handler, int $bufferLimit = 0, $level = Logger::DEBUG, bool $bubble = true, bool $flushOnOverflow = false)
{
parent::__construct($level, $bubble);
$this->handler = $handler;
$this->bufferLimit = (int) $bufferLimit;
$this->bufferLimit = $bufferLimit;
$this->flushOnOverflow = $flushOnOverflow;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if ($record['level'] < $this->level) {
return false;
@@ -58,7 +66,7 @@ class BufferHandler extends AbstractHandler
if (!$this->initialized) {
// __destructor() doesn't get called on Fatal errors
register_shutdown_function(array($this, 'close'));
register_shutdown_function([$this, 'close']);
$this->initialized = true;
}
@@ -72,9 +80,8 @@ class BufferHandler extends AbstractHandler
}
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->buffer[] = $record;
@@ -83,7 +90,7 @@ class BufferHandler extends AbstractHandler
return false === $this->bubble;
}
public function flush()
public function flush(): void
{
if ($this->bufferSize === 0) {
return;
@@ -101,20 +108,22 @@ class BufferHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
$this->flush();
$this->handler->close();
}
/**
* Clears the buffer without flushing any messages down to the wrapped handler.
*/
public function clear()
public function clear(): void
{
$this->bufferSize = 0;
$this->buffer = array();
$this->buffer = [];
}
public function reset()
@@ -123,26 +132,36 @@ class BufferHandler extends AbstractHandler
parent::reset();
$this->resetProcessors();
if ($this->handler instanceof ResettableInterface) {
$this->handler->reset();
}
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->handler->setFormatter($formatter);
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter()
public function getFormatter(): FormatterInterface
{
return $this->handler->getFormatter();
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler;
use Monolog\Formatter\ChromePHPFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
@@ -21,24 +22,29 @@ use Monolog\Utils;
* This also works out of the box with Firefox 43+
*
* @author Christophe Coevoet <stof@notk.org>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class ChromePHPHandler extends AbstractProcessingHandler
{
use WebRequestRecognizerTrait;
/**
* Version of the extension
*/
const VERSION = '4.0';
protected const VERSION = '4.0';
/**
* Header name
*/
const HEADER_NAME = 'X-ChromeLogger-Data';
protected 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 const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}';
/** @var bool */
protected static $initialized = false;
/**
@@ -50,19 +56,17 @@ class ChromePHPHandler extends AbstractProcessingHandler
*/
protected static $overflowed = false;
protected static $json = array(
/** @var mixed[] */
protected static $json = [
'version' => self::VERSION,
'columns' => array('label', 'log', 'backtrace', 'type'),
'rows' => array(),
);
'columns' => ['label', 'log', 'backtrace', 'type'],
'rows' => [],
];
/** @var bool */
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)
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
if (!function_exists('json_encode')) {
@@ -71,17 +75,23 @@ class ChromePHPHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
$messages = array();
if (!$this->isWebRequest()) {
return;
}
$messages = [];
foreach ($records as $record) {
if ($record['level'] < $this->level) {
continue;
}
$messages[] = $this->processRecord($record);
/** @var Record $message */
$message = $this->processRecord($record);
$messages[] = $message;
}
if (!empty($messages)) {
@@ -94,7 +104,7 @@ class ChromePHPHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new ChromePHPFormatter();
}
@@ -104,10 +114,13 @@ class ChromePHPHandler extends AbstractProcessingHandler
*
* @see sendHeader()
* @see send()
* @param array $record
*/
protected function write(array $record)
protected function write(array $record): void
{
if (!$this->isWebRequest()) {
return;
}
self::$json['rows'][] = $record['formatted'];
$this->send();
@@ -118,7 +131,7 @@ class ChromePHPHandler extends AbstractProcessingHandler
*
* @see sendHeader()
*/
protected function send()
protected function send(): void
{
if (self::$overflowed || !self::$sendHeaders) {
return;
@@ -132,40 +145,37 @@ class ChromePHPHandler extends AbstractProcessingHandler
return;
}
self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? '';
}
$json = Utils::jsonEncode(self::$json, null, true);
$data = base64_encode(utf8_encode($json));
$json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
$data = base64_encode($json);
if (strlen($data) > 3 * 1024) {
self::$overflowed = true;
$record = array(
$record = [
'message' => 'Incomplete logs, chrome header size limit reached',
'context' => array(),
'context' => [],
'level' => Logger::WARNING,
'level_name' => Logger::getLevelName(Logger::WARNING),
'channel' => 'monolog',
'datetime' => new \DateTime(),
'extra' => array(),
);
'datetime' => new \DateTimeImmutable(),
'extra' => [],
];
self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
$json = Utils::jsonEncode(self::$json, null, true);
$data = base64_encode(utf8_encode($json));
$json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
$data = base64_encode($json);
}
if (trim($data) !== '') {
$this->sendHeader(self::HEADER_NAME, $data);
$this->sendHeader(static::HEADER_NAME, $data);
}
}
/**
* Send header string to the client
*
* @param string $header
* @param string $content
*/
protected function sendHeader($header, $content)
protected function sendHeader(string $header, string $content): void
{
if (!headers_sent() && self::$sendHeaders) {
header(sprintf('%s: %s', $header, $content));
@@ -174,39 +184,13 @@ class ChromePHPHandler extends AbstractProcessingHandler
/**
* Verifies if the headers are accepted by the current user agent
*
* @return bool
*/
protected function headersAccepted()
protected function headersAccepted(): bool
{
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;
return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,6 +11,7 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;
@@ -21,17 +22,21 @@ use Monolog\Logger;
*/
class CouchDBHandler extends AbstractProcessingHandler
{
/** @var mixed[] */
private $options;
public function __construct(array $options = array(), $level = Logger::DEBUG, $bubble = true)
/**
* @param mixed[] $options
*/
public function __construct(array $options = [], $level = Logger::DEBUG, bool $bubble = true)
{
$this->options = array_merge(array(
$this->options = array_merge([
'host' => 'localhost',
'port' => 5984,
'dbname' => 'logger',
'username' => null,
'password' => null,
), $options);
], $options);
parent::__construct($level, $bubble);
}
@@ -39,7 +44,7 @@ class CouchDBHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$basicAuth = null;
if ($this->options['username']) {
@@ -47,17 +52,17 @@ class CouchDBHandler extends AbstractProcessingHandler
}
$url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname'];
$context = stream_context_create(array(
'http' => array(
$context = stream_context_create([
'http' => [
'method' => 'POST',
'content' => $record['formatted'],
'ignore_errors' => true,
'max_redirects' => 0,
'header' => 'Content-type: application/json',
),
));
],
]);
if (false === @file_get_contents($url, null, $context)) {
if (false === @file_get_contents($url, false, $context)) {
throw new \RuntimeException(sprintf('Could not connect to %s', $url));
}
}
@@ -65,7 +70,7 @@ class CouchDBHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -17,17 +17,24 @@ use Monolog\Utils;
/**
* Logs to Cube.
*
* @link http://square.github.com/cube/
* @link https://github.com/square/cube/wiki
* @author Wan Chen <kami@kamisama.me>
* @deprecated Since 2.8.0 and 3.2.0, Cube appears abandoned and thus we will drop this handler in Monolog 4
*/
class CubeHandler extends AbstractProcessingHandler
{
private $udpConnection;
private $httpConnection;
/** @var resource|\Socket|null */
private $udpConnection = null;
/** @var resource|\CurlHandle|null */
private $httpConnection = null;
/** @var string */
private $scheme;
/** @var string */
private $host;
/** @var int */
private $port;
private $acceptedSchemes = array('http', 'udp');
/** @var string[] */
private $acceptedSchemes = ['http', 'udp'];
/**
* Create a Cube handler
@@ -36,23 +43,24 @@ class CubeHandler extends AbstractProcessingHandler
* 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)
public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true)
{
$urlInfo = parse_url($url);
if (!isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) {
if ($urlInfo === false || !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));
. ' Valid options are ' . implode(', ', $this->acceptedSchemes)
);
}
$this->scheme = $urlInfo['scheme'];
$this->host = $urlInfo['host'];
$this->port = $urlInfo['port'];
$this->port = (int) $urlInfo['port'];
parent::__construct($level, $bubble);
}
@@ -63,50 +71,53 @@ class CubeHandler extends AbstractProcessingHandler
* @throws \LogicException when unable to connect to the socket
* @throws MissingExtensionException when there is no socket extension
*/
protected function connectUdp()
protected function connectUdp(): void
{
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) {
$udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0);
if (false === $udpConnection) {
throw new \LogicException('Unable to create a socket');
}
$this->udpConnection = $udpConnection;
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
* Establish a connection to an http server
*
* @throws \LogicException when unable to connect to the socket
* @throws MissingExtensionException when no curl extension
*/
protected function connectHttp()
protected function connectHttp(): void
{
if (!extension_loaded('curl')) {
throw new \LogicException('The curl extension is needed to use http URLs with the CubeHandler');
throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler');
}
$this->httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');
if (!$this->httpConnection) {
$httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');
if (false === $httpConnection) {
throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port);
}
$this->httpConnection = $httpConnection;
curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$date = $record['datetime'];
$data = array('time' => $date->format('Y-m-d\TH:i:s.uO'));
$data = ['time' => $date->format('Y-m-d\TH:i:s.uO')];
unset($record['datetime']);
if (isset($record['context']['type'])) {
@@ -126,7 +137,7 @@ class CubeHandler extends AbstractProcessingHandler
}
}
private function writeUdp($data)
private function writeUdp(string $data): void
{
if (!$this->udpConnection) {
$this->connectUdp();
@@ -135,17 +146,21 @@ class CubeHandler extends AbstractProcessingHandler
socket_send($this->udpConnection, $data, strlen($data), 0);
}
private function writeHttp($data)
private function writeHttp(string $data): void
{
if (!$this->httpConnection) {
$this->connectHttp();
}
if (null === $this->httpConnection) {
throw new \LogicException('No connection could be established');
}
curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']');
curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, array(
curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen('['.$data.']'),
));
]);
Curl\Util::execute($this->httpConnection, 5, false);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,9 +11,17 @@
namespace Monolog\Handler\Curl;
class Util
use CurlHandle;
/**
* This class is marked as internal and it is not under the BC promise of the package.
*
* @internal
*/
final class Util
{
private static $retriableErrorCodes = array(
/** @var array<int> */
private static $retriableErrorCodes = [
CURLE_COULDNT_RESOLVE_HOST,
CURLE_COULDNT_CONNECT,
CURLE_HTTP_NOT_FOUND,
@@ -21,18 +29,21 @@ class Util
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
* @param resource|CurlHandle $ch curl handler
* @param int $retries
* @param bool $closeAfterDone
* @return bool|string @see curl_exec
*/
public static function execute($ch, $retries = 5, $closeAfterDone = true)
public static function execute($ch, int $retries = 5, bool $closeAfterDone = true)
{
while ($retries--) {
if (curl_exec($ch) === false) {
$curlResponse = curl_exec($ch);
if ($curlResponse === false) {
$curlErrno = curl_errno($ch);
if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) {
@@ -42,7 +53,7 @@ class Util
curl_close($ch);
}
throw new \RuntimeException(sprintf('Curl error (code %s): %s', $curlErrno, $curlError));
throw new \RuntimeException(sprintf('Curl error (code %d): %s', $curlErrno, $curlError));
}
continue;
@@ -51,7 +62,10 @@ class Util
if ($closeAfterDone) {
curl_close($ch);
}
break;
return $curlResponse;
}
return false;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Simple handler wrapper that deduplicates log records across multiple requests
@@ -32,6 +33,10 @@ use Monolog\Logger;
* same way.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
class DeduplicationHandler extends BufferHandler
{
@@ -41,7 +46,7 @@ class DeduplicationHandler extends BufferHandler
protected $deduplicationStore;
/**
* @var int
* @var Level
*/
protected $deduplicationLevel;
@@ -58,11 +63,13 @@ class DeduplicationHandler extends BufferHandler
/**
* @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 string|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
*
* @phpstan-param Level|LevelName|LogLevel::* $deduplicationLevel
*/
public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true)
public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, int $time = 60, bool $bubble = true)
{
parent::__construct($handler, 0, Logger::DEBUG, $bubble, false);
@@ -71,7 +78,7 @@ class DeduplicationHandler extends BufferHandler
$this->time = $time;
}
public function flush()
public function flush(): void
{
if ($this->bufferSize === 0) {
return;
@@ -81,7 +88,6 @@ class DeduplicationHandler extends BufferHandler
foreach ($this->buffer as $record) {
if ($record['level'] >= $this->deduplicationLevel) {
$passthru = $passthru || !$this->isDuplicate($record);
if ($passthru) {
$this->appendRecord($record);
@@ -101,7 +107,10 @@ class DeduplicationHandler extends BufferHandler
}
}
private function isDuplicate(array $record)
/**
* @phpstan-param Record $record
*/
private function isDuplicate(array $record): bool
{
if (!file_exists($this->deduplicationStore)) {
return false;
@@ -131,21 +140,26 @@ class DeduplicationHandler extends BufferHandler
return false;
}
private function collectLogs()
private function collectLogs(): void
{
if (!file_exists($this->deduplicationStore)) {
return false;
return;
}
$handle = fopen($this->deduplicationStore, 'rw+');
if (!$handle) {
throw new \RuntimeException('Failed to open file for reading and writing: ' . $this->deduplicationStore);
}
flock($handle, LOCK_EX);
$validLogs = array();
$validLogs = [];
$timestampValidity = time() - $this->time;
while (!feof($handle)) {
$log = fgets($handle);
if (substr($log, 0, 10) >= $timestampValidity) {
if ($log && substr($log, 0, 10) >= $timestampValidity) {
$validLogs[] = $log;
}
}
@@ -162,7 +176,10 @@ class DeduplicationHandler extends BufferHandler
$this->gc = false;
}
private function appendRecord(array $record)
/**
* @phpstan-param Record $record
*/
private function appendRecord(array $record): void
{
file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -13,6 +13,7 @@ namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
use Doctrine\CouchDB\CouchDBClient;
/**
@@ -22,9 +23,10 @@ use Doctrine\CouchDB\CouchDBClient;
*/
class DoctrineCouchDBHandler extends AbstractProcessingHandler
{
/** @var CouchDBClient */
private $client;
public function __construct(CouchDBClient $client, $level = Logger::DEBUG, $bubble = true)
public function __construct(CouchDBClient $client, $level = Logger::DEBUG, bool $bubble = true)
{
$this->client = $client;
parent::__construct($level, $bubble);
@@ -33,12 +35,12 @@ class DoctrineCouchDBHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$this->client->postDocument($record['formatted']);
}
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter;
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -13,6 +13,7 @@ namespace Monolog\Handler;
use Aws\Sdk;
use Aws\DynamoDb\DynamoDbClient;
use Monolog\Formatter\FormatterInterface;
use Aws\DynamoDb\Marshaler;
use Monolog\Formatter\ScalarFormatter;
use Monolog\Logger;
@@ -25,7 +26,7 @@ use Monolog\Logger;
*/
class DynamoDbHandler extends AbstractProcessingHandler
{
const DATE_FORMAT = 'Y-m-d\TH:i:s.uO';
public const DATE_FORMAT = 'Y-m-d\TH:i:s.uO';
/**
* @var DynamoDbClient
@@ -47,14 +48,9 @@ class DynamoDbHandler extends AbstractProcessingHandler
*/
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)
public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true)
{
/** @phpstan-ignore-next-line */
if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) {
$this->version = 3;
$this->marshaler = new Marshaler;
@@ -69,9 +65,9 @@ class DynamoDbHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$filtered = $this->filterEmptyFields($record['formatted']);
if ($this->version === 3) {
@@ -81,17 +77,17 @@ class DynamoDbHandler extends AbstractProcessingHandler
$formatted = $this->client->formatAttributes($filtered);
}
$this->client->putItem(array(
$this->client->putItem([
'TableName' => $this->table,
'Item' => $formatted,
));
]);
}
/**
* @param array $record
* @return array
* @param mixed[] $record
* @return mixed[]
*/
protected function filterEmptyFields(array $record)
protected function filterEmptyFields(array $record): array
{
return array_filter($record, function ($value) {
return !empty($value) || false === $value || 0 === $value;
@@ -99,9 +95,9 @@ class DynamoDbHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new ScalarFormatter(self::DATE_FORMAT);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,6 +11,7 @@
namespace Monolog\Handler;
use Elastica\Document;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticaFormatter;
use Monolog\Logger;
@@ -25,15 +26,15 @@ use Elastica\Exception\ExceptionInterface;
* $client = new \Elastica\Client();
* $options = array(
* 'index' => 'elastic_index_name',
* 'type' => 'elastic_doc_type',
* 'type' => 'elastic_doc_type', Types have been removed in Elastica 7
* );
* $handler = new ElasticSearchHandler($client, $options);
* $handler = new ElasticaHandler($client, $options);
* $log = new Logger('application');
* $log->pushHandler($handler);
*
* @author Jelle Vink <jelle.vink@gmail.com>
*/
class ElasticSearchHandler extends AbstractProcessingHandler
class ElasticaHandler extends AbstractProcessingHandler
{
/**
* @var Client
@@ -41,26 +42,24 @@ class ElasticSearchHandler extends AbstractProcessingHandler
protected $client;
/**
* @var array Handler config options
* @var mixed[] Handler config options
*/
protected $options = array();
protected $options = [];
/**
* @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
* @param Client $client Elastica Client object
* @param mixed[] $options Handler configuration
*/
public function __construct(Client $client, array $options = array(), $level = Logger::DEBUG, $bubble = true)
public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $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
);
}
@@ -68,27 +67,27 @@ class ElasticSearchHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$this->bulkSend(array($record['formatted']));
$this->bulkSend([$record['formatted']]);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($formatter instanceof ElasticaFormatter) {
return parent::setFormatter($formatter);
}
throw new \InvalidArgumentException('ElasticSearchHandler is only compatible with ElasticaFormatter');
throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter');
}
/**
* Getter options
* @return array
* @return mixed[]
*/
public function getOptions()
public function getOptions(): array
{
return $this->options;
}
@@ -96,15 +95,15 @@ class ElasticSearchHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new ElasticaFormatter($this->options['index'], $this->options['type']);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
$documents = $this->getFormatter()->formatBatch($records);
$this->bulkSend($documents);
@@ -112,10 +111,12 @@ class ElasticSearchHandler extends AbstractProcessingHandler
/**
* Use Elasticsearch bulk API to send list of documents
* @param array $documents
*
* @param Document[] $documents
*
* @throws \RuntimeException
*/
protected function bulkSend(array $documents)
protected function bulkSend(array $documents): void
{
try {
$this->client->addDocuments($documents);

View File

@@ -0,0 +1,218 @@
<?php declare(strict_types=1);
/*
* 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 Elastic\Elasticsearch\Response\Elasticsearch;
use Throwable;
use RuntimeException;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticsearchFormatter;
use InvalidArgumentException;
use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException;
use Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException;
use Elastic\Elasticsearch\Client as Client8;
/**
* Elasticsearch handler
*
* @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html
*
* Simple usage example:
*
* $client = \Elasticsearch\ClientBuilder::create()
* ->setHosts($hosts)
* ->build();
*
* $options = array(
* 'index' => 'elastic_index_name',
* 'type' => 'elastic_doc_type',
* );
* $handler = new ElasticsearchHandler($client, $options);
* $log = new Logger('application');
* $log->pushHandler($handler);
*
* @author Avtandil Kikabidze <akalongman@gmail.com>
*/
class ElasticsearchHandler extends AbstractProcessingHandler
{
/**
* @var Client|Client8
*/
protected $client;
/**
* @var mixed[] Handler config options
*/
protected $options = [];
/**
* @var bool
*/
private $needsType;
/**
* @param Client|Client8 $client Elasticsearch Client object
* @param mixed[] $options Handler configuration
*/
public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
{
if (!$client instanceof Client && !$client instanceof Client8) {
throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required');
}
parent::__construct($level, $bubble);
$this->client = $client;
$this->options = array_merge(
[
'index' => 'monolog', // Elastic index name
'type' => '_doc', // Elastic document type
'ignore_error' => false, // Suppress Elasticsearch exceptions
],
$options
);
if ($client instanceof Client8 || $client::VERSION[0] === '7') {
$this->needsType = false;
// force the type to _doc for ES8/ES7
$this->options['type'] = '_doc';
} else {
$this->needsType = true;
}
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->bulkSend([$record['formatted']]);
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($formatter instanceof ElasticsearchFormatter) {
return parent::setFormatter($formatter);
}
throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter');
}
/**
* Getter options
*
* @return mixed[]
*/
public function getOptions(): array
{
return $this->options;
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new ElasticsearchFormatter($this->options['index'], $this->options['type']);
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
$documents = $this->getFormatter()->formatBatch($records);
$this->bulkSend($documents);
}
/**
* Use Elasticsearch bulk API to send list of documents
*
* @param array[] $records Records + _index/_type keys
* @throws \RuntimeException
*/
protected function bulkSend(array $records): void
{
try {
$params = [
'body' => [],
];
foreach ($records as $record) {
$params['body'][] = [
'index' => $this->needsType ? [
'_index' => $record['_index'],
'_type' => $record['_type'],
] : [
'_index' => $record['_index'],
],
];
unset($record['_index'], $record['_type']);
$params['body'][] = $record;
}
/** @var Elasticsearch */
$responses = $this->client->bulk($params);
if ($responses['errors'] === true) {
throw $this->createExceptionFromResponses($responses);
}
} catch (Throwable $e) {
if (! $this->options['ignore_error']) {
throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e);
}
}
}
/**
* Creates elasticsearch exception from responses array
*
* Only the first error is converted into an exception.
*
* @param mixed[]|Elasticsearch $responses returned by $this->client->bulk()
*/
protected function createExceptionFromResponses($responses): Throwable
{
foreach ($responses['items'] ?? [] as $item) {
if (isset($item['index']['error'])) {
return $this->createExceptionFromError($item['index']['error']);
}
}
if (class_exists(ElasticInvalidArgumentException::class)) {
return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.');
}
return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.');
}
/**
* Creates elasticsearch exception from error array
*
* @param mixed[] $error
*/
protected function createExceptionFromError(array $error): Throwable
{
$previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null;
if (class_exists(ElasticInvalidArgumentException::class)) {
return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous);
}
return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,7 +12,9 @@
namespace Monolog\Handler;
use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
/**
* Stores to PHP error_log() handler.
@@ -21,24 +23,25 @@ use Monolog\Logger;
*/
class ErrorLogHandler extends AbstractProcessingHandler
{
const OPERATING_SYSTEM = 0;
const SAPI = 4;
public const OPERATING_SYSTEM = 0;
public const SAPI = 4;
/** @var int */
protected $messageType;
/** @var bool */
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)
public function __construct(int $messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, bool $bubble = true, bool $expandNewlines = false)
{
parent::__construct($level, $bubble);
if (false === in_array($messageType, self::getAvailableTypes())) {
if (false === in_array($messageType, self::getAvailableTypes(), true)) {
$message = sprintf('The given message type "%s" is not supported', print_r($messageType, true));
throw new \InvalidArgumentException($message);
}
@@ -47,36 +50,42 @@ class ErrorLogHandler extends AbstractProcessingHandler
}
/**
* @return array With all available types
* @return int[] With all available types
*/
public static function getAvailableTypes()
public static function getAvailableTypes(): array
{
return array(
return [
self::OPERATING_SYSTEM,
self::SAPI,
);
];
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
if ($this->expandNewlines) {
$lines = preg_split('{[\r\n]+}', (string) $record['formatted']);
foreach ($lines as $line) {
error_log($line, $this->messageType);
}
} else {
if (!$this->expandNewlines) {
error_log((string) $record['formatted'], $this->messageType);
return;
}
$lines = preg_split('{[\r\n]+}', (string) $record['formatted']);
if ($lines === false) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Failed to preg_split formatted string: ' . $pcreErrorCode . ' / '. Utils::pcreLastErrorMessage($pcreErrorCode));
}
foreach ($lines as $line) {
error_log($line, $this->messageType);
}
}
}

View File

@@ -0,0 +1,71 @@
<?php declare(strict_types=1);
/*
* 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 Throwable;
/**
* Forwards records to at most one handler
*
* If a handler fails, the exception is suppressed and the record is forwarded to the next handler.
*
* As soon as one handler handles a record successfully, the handling stops there.
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class FallbackGroupHandler extends GroupHandler
{
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
foreach ($this->handlers as $handler) {
try {
$handler->handle($record);
break;
} catch (Throwable $e) {
// What throwable?
}
}
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
if ($this->processors) {
$processed = [];
foreach ($records as $record) {
$processed[] = $this->processRecord($record);
}
/** @var Record[] $records */
$records = $processed;
}
foreach ($this->handlers as $handler) {
try {
$handler->handleBatch($records);
break;
} catch (Throwable $e) {
// What throwable?
}
}
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,7 +12,9 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
use Psr\Log\LogLevel;
/**
* Simple handler wrapper that filters records based on a list of levels
@@ -21,13 +23,20 @@ use Monolog\Formatter\FormatterInterface;
*
* @author Hennadiy Verkh
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class FilterHandler extends AbstractHandler
class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* Handler or factory callable($record, $this)
*
* @var callable|\Monolog\Handler\HandlerInterface
* @var callable|HandlerInterface
* @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface
*/
protected $handler;
@@ -35,6 +44,7 @@ class FilterHandler extends AbstractHandler
* Minimum level for logs that are passed to handler
*
* @var int[]
* @phpstan-var array<Level, int>
*/
protected $acceptedLevels;
@@ -46,12 +56,17 @@ class FilterHandler extends AbstractHandler
protected $bubble;
/**
* @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $filterHandler).
* @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 int|string $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
*
* @phpstan-param Level|LevelName|LogLevel::*|array<Level|LevelName|LogLevel::*> $minLevelOrList
* @phpstan-param Level|LevelName|LogLevel::* $maxLevel
*/
public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, $bubble = true)
public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, bool $bubble = true)
{
$this->handler = $handler;
$this->bubble = $bubble;
@@ -63,9 +78,9 @@ class FilterHandler extends AbstractHandler
}
/**
* @return array
* @phpstan-return array<int, Level>
*/
public function getAcceptedLevels()
public function getAcceptedLevels(): array
{
return array_flip($this->acceptedLevels);
}
@@ -73,8 +88,11 @@ class FilterHandler extends AbstractHandler
/**
* @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
*
* @phpstan-param Level|LevelName|LogLevel::*|array<Level|LevelName|LogLevel::*> $minLevelOrList
* @phpstan-param Level|LevelName|LogLevel::* $maxLevel
*/
public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY)
public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY): self
{
if (is_array($minLevelOrList)) {
$acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList);
@@ -86,29 +104,30 @@ class FilterHandler extends AbstractHandler
}));
}
$this->acceptedLevels = array_flip($acceptedLevels);
return $this;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function isHandling(array $record)
public function isHandling(array $record): bool
{
return isset($this->acceptedLevels[$record['level']]);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->getHandler($record)->handle($record);
@@ -117,11 +136,11 @@ class FilterHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
$filtered = array();
$filtered = [];
foreach ($records as $record) {
if ($this->isHandling($record)) {
$filtered[] = $record;
@@ -139,11 +158,13 @@ class FilterHandler extends AbstractHandler
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @return HandlerInterface
*
* @phpstan-param Record $record
*/
public function getHandler(array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = call_user_func($this->handler, $record, $this);
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
@@ -153,20 +174,39 @@ class FilterHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->getHandler()->setFormatter($formatter);
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter()
public function getFormatter(): FormatterInterface
{
return $this->getHandler()->getFormatter();
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
public function reset()
{
$this->resetProcessors();
if ($this->getHandler() instanceof ResettableInterface) {
$this->getHandler()->reset();
}
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -15,14 +15,15 @@ namespace Monolog\Handler\FingersCrossed;
* Interface for activation strategies for the FingersCrossedHandler.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
interface ActivationStrategyInterface
{
/**
* Returns whether the given record activates the handler.
*
* @param array $record
* @return bool
* @phpstan-param Record $record
*/
public function isHandlerActivated(array $record);
public function isHandlerActivated(array $record): bool;
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler\FingersCrossed;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Channel and Error level based monolog activation strategy. Allows to trigger activation
@@ -32,23 +33,40 @@ use Monolog\Logger;
* </code>
*
* @author Mike Meessen <netmikey@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class ChannelLevelActivationStrategy implements ActivationStrategyInterface
{
/**
* @var Level
*/
private $defaultActionLevel;
/**
* @var array<string, Level>
*/
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.
* @param int|string $defaultActionLevel The default action level to be used if the record's category doesn't match any
* @param array<string, int> $channelToActionLevel An array that maps channel names to action levels.
*
* @phpstan-param array<string, Level> $channelToActionLevel
* @phpstan-param Level|LevelName|LogLevel::* $defaultActionLevel
*/
public function __construct($defaultActionLevel, $channelToActionLevel = array())
public function __construct($defaultActionLevel, array $channelToActionLevel = [])
{
$this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel);
$this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel);
}
public function isHandlerActivated(array $record)
/**
* @phpstan-param Record $record
*/
public function isHandlerActivated(array $record): bool
{
if (isset($this->channelToActionLevel[$record['channel']])) {
return $record['level'] >= $this->channelToActionLevel[$record['channel']];

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,22 +12,34 @@
namespace Monolog\Handler\FingersCrossed;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Error level based activation strategy.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class ErrorLevelActivationStrategy implements ActivationStrategyInterface
{
/**
* @var Level
*/
private $actionLevel;
/**
* @param int|string $actionLevel Level or name or value
*
* @phpstan-param Level|LevelName|LogLevel::* $actionLevel
*/
public function __construct($actionLevel)
{
$this->actionLevel = Logger::toMonologLevel($actionLevel);
}
public function isHandlerActivated(array $record)
public function isHandlerActivated(array $record): bool
{
return $record['level'] >= $this->actionLevel;
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -16,6 +16,7 @@ use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
use Monolog\Logger;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;
use Psr\Log\LogLevel;
/**
* Buffers all records until a certain level is reached
@@ -24,30 +25,60 @@ use Monolog\Formatter\FormatterInterface;
* 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 then have a passthruLevel as well which means that at the end of the request,
* even if it did not get activated, it will still send through log records of e.g. at least a
* warning level.
*
* You can find the various activation strategies in the
* Monolog\Handler\FingersCrossed\ namespace.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class FingersCrossedHandler extends AbstractHandler
class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface
{
protected $handler;
protected $activationStrategy;
protected $buffering = true;
protected $bufferSize;
protected $buffer = array();
protected $stopBuffering;
protected $passthruLevel;
use ProcessableHandlerTrait;
/**
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $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
* @var callable|HandlerInterface
* @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface
*/
public function __construct($handler, $activationStrategy = null, $bufferSize = 0, $bubble = true, $stopBuffering = true, $passthruLevel = null)
protected $handler;
/** @var ActivationStrategyInterface */
protected $activationStrategy;
/** @var bool */
protected $buffering = true;
/** @var int */
protected $bufferSize;
/** @var Record[] */
protected $buffer = [];
/** @var bool */
protected $stopBuffering;
/**
* @var ?int
* @phpstan-var ?Level
*/
protected $passthruLevel;
/** @var bool */
protected $bubble;
/**
* @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $fingersCrossedHandler).
* @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated
* @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|string $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered
*
* @phpstan-param Level|LevelName|LogLevel::* $passthruLevel
* @phpstan-param Level|LevelName|LogLevel::*|ActivationStrategyInterface $activationStrategy
*/
public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null)
{
if (null === $activationStrategy) {
$activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING);
@@ -74,9 +105,9 @@ class FingersCrossedHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function isHandling(array $record)
public function isHandling(array $record): bool
{
return true;
}
@@ -84,24 +115,24 @@ class FingersCrossedHandler extends AbstractHandler
/**
* Manually activate this logger regardless of the activation strategy
*/
public function activate()
public function activate(): void
{
if ($this->stopBuffering) {
$this->buffering = false;
}
$this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
$this->buffer = array();
$this->buffer = [];
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
/** @var Record $record */
$record = $this->processRecord($record);
}
if ($this->buffering) {
@@ -120,18 +151,20 @@ class FingersCrossedHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
$this->flushBuffer();
$this->getHandler()->close();
}
public function reset()
{
$this->flushBuffer();
parent::reset();
$this->resetProcessors();
if ($this->getHandler() instanceof ResettableInterface) {
$this->getHandler()->reset();
@@ -143,16 +176,16 @@ class FingersCrossedHandler extends AbstractHandler
*
* It also resets the handler to its initial buffering state.
*/
public function clear()
public function clear(): void
{
$this->buffer = array();
$this->buffer = [];
$this->reset();
}
/**
* Resets the state of the handler. Stops forwarding records to the wrapped handler.
*/
private function flushBuffer()
private function flushBuffer(): void
{
if (null !== $this->passthruLevel) {
$level = $this->passthruLevel;
@@ -160,11 +193,11 @@ class FingersCrossedHandler extends AbstractHandler
return $record['level'] >= $level;
});
if (count($this->buffer) > 0) {
$this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
$this->getHandler(end($this->buffer))->handleBatch($this->buffer);
}
}
$this->buffer = array();
$this->buffer = [];
$this->buffering = true;
}
@@ -174,11 +207,13 @@ class FingersCrossedHandler extends AbstractHandler
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @return HandlerInterface
*
* @phpstan-param Record $record
*/
public function getHandler(array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = call_user_func($this->handler, $record, $this);
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
@@ -188,20 +223,30 @@ class FingersCrossedHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->getHandler()->setFormatter($formatter);
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter()
public function getFormatter(): FormatterInterface
{
return $this->getHandler()->getFormatter();
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,36 +12,42 @@
namespace Monolog\Handler;
use Monolog\Formatter\WildfireFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol.
*
* @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class FirePHPHandler extends AbstractProcessingHandler
{
use WebRequestRecognizerTrait;
/**
* WildFire JSON header message format
*/
const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2';
protected 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';
protected 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';
protected 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';
protected const HEADER_PREFIX = 'X-Wf';
/**
* Whether or not Wildfire vendor-specific headers have been generated & sent yet
* @var bool
*/
protected static $initialized = false;
@@ -51,35 +57,43 @@ class FirePHPHandler extends AbstractProcessingHandler
*/
protected static $messageIndex = 1;
/** @var bool */
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
* @param array<int|string> $meta Wildfire Plugin, Protocol & Structure Indexes
* @param string $message Log message
*
* @return array<string, string> Complete header string ready for the client as key and message as value
*
* @phpstan-return non-empty-array<string, string>
*/
protected function createHeader(array $meta, $message)
protected function createHeader(array $meta, string $message): array
{
$header = sprintf('%s-%s', self::HEADER_PREFIX, join('-', $meta));
$header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta));
return array($header => $message);
return [$header => $message];
}
/**
* Creates message header from record
*
* @return array<string, string>
*
* @phpstan-return non-empty-array<string, string>
*
* @see createHeader()
* @param array $record
* @return array
*
* @phpstan-param FormattedRecord $record
*/
protected function createRecordHeader(array $record)
protected function createRecordHeader(array $record): array
{
// 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++),
[1, 1, 1, self::$messageIndex++],
$record['formatted']
);
}
@@ -87,7 +101,7 @@ class FirePHPHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new WildfireFormatter();
}
@@ -97,25 +111,23 @@ class FirePHPHandler extends AbstractProcessingHandler
*
* @see createHeader()
* @see sendHeader()
* @return array
*
* @return array<string, string>
*/
protected function getInitHeaders()
protected function getInitHeaders(): array
{
// 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)
$this->createHeader(['Protocol', 1], static::PROTOCOL_URI),
$this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI),
$this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI)
);
}
/**
* Send header string to the client
*
* @param string $header
* @param string $content
*/
protected function sendHeader($header, $content)
protected function sendHeader(string $header, string $content): void
{
if (!headers_sent() && self::$sendHeaders) {
header(sprintf('%s: %s', $header, $content));
@@ -127,11 +139,10 @@ class FirePHPHandler extends AbstractProcessingHandler
*
* @see sendHeader()
* @see sendInitHeaders()
* @param array $record
*/
protected function write(array $record)
protected function write(array $record): void
{
if (!self::$sendHeaders) {
if (!self::$sendHeaders || !$this->isWebRequest()) {
return;
}
@@ -157,10 +168,8 @@ class FirePHPHandler extends AbstractProcessingHandler
/**
* Verifies if the headers are accepted by the current user agent
*
* @return bool
*/
protected function headersAccepted()
protected function headersAccepted(): bool
{
if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) {
return true;
@@ -168,28 +177,4 @@ class FirePHPHandler extends AbstractProcessingHandler
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;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,6 +11,7 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
@@ -21,12 +22,14 @@ use Monolog\Logger;
*
* @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation
* @author Ando Roots <ando@sqroot.eu>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class FleepHookHandler extends SocketHandler
{
const FLEEP_HOST = 'fleep.io';
protected const FLEEP_HOST = 'fleep.io';
const FLEEP_HOOK_URI = '/hook/';
protected const FLEEP_HOOK_URI = '/hook/';
/**
* @var string Webhook token (specifies the conversation where logs are sent)
@@ -40,20 +43,35 @@ class FleepHookHandler extends SocketHandler
* 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)
{
public function __construct(
string $token,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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);
$connectionString = 'ssl://' . static::FLEEP_HOST . ':443';
parent::__construct(
$connectionString,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
}
/**
@@ -63,29 +81,24 @@ class FleepHookHandler extends SocketHandler
*
* @return LineFormatter
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter(null, null, true, true);
}
/**
* Handles a log record
*
* @param array $record
*/
public function write(array $record)
public function write(array $record): void
{
parent::write($record);
$this->closeSocket();
}
/**
* {@inheritdoc}
*
* @param array $record
* @return string
* {@inheritDoc}
*/
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
@@ -94,14 +107,11 @@ class FleepHookHandler extends SocketHandler
/**
* Builds the header of the API Call
*
* @param string $content
* @return string
*/
private function buildHeader($content)
private function buildHeader(string $content): string
{
$header = "POST " . self::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n";
$header .= "Host: " . self::FLEEP_HOST . "\r\n";
$header = "POST " . static::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n";
$header .= "Host: " . static::FLEEP_HOST . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($content) . "\r\n";
$header .= "\r\n";
@@ -112,14 +122,13 @@ class FleepHookHandler extends SocketHandler
/**
* Builds the body of API call
*
* @param array $record
* @return string
* @phpstan-param FormattedRecord $record
*/
private function buildContent($record)
private function buildContent(array $record): string
{
$dataArray = array(
$dataArray = [
'message' => $record['formatted'],
);
];
return http_build_query($dataArray);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -26,6 +26,8 @@ use Monolog\Formatter\FormatterInterface;
*
* @author Dominik Liebler <liebler.dominik@gmail.com>
* @see https://www.flowdock.com/api/push
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class FlowdockHandler extends SocketHandler
{
@@ -35,26 +37,39 @@ class FlowdockHandler extends SocketHandler
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)
{
public function __construct(
string $apiToken,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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);
parent::__construct(
'ssl://api.flowdock.com:443',
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->apiToken = $apiToken;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if (!$formatter instanceof FlowdockFormatter) {
throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
@@ -65,20 +80,16 @@ class FlowdockHandler extends SocketHandler
/**
* Gets the default formatter.
*
* @return FormatterInterface
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
}
/**
* {@inheritdoc}
*
* @param array $record
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
parent::write($record);
@@ -86,12 +97,9 @@ class FlowdockHandler extends SocketHandler
}
/**
* {@inheritdoc}
*
* @param array $record
* @return string
* {@inheritDoc}
*/
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
@@ -101,21 +109,17 @@ class FlowdockHandler extends SocketHandler
/**
* Builds the body of API call
*
* @param array $record
* @return string
* @phpstan-param FormattedRecord $record
*/
private function buildContent($record)
private function buildContent(array $record): string
{
return Utils::jsonEncode($record['formatted']['flowdock']);
}
/**
* Builds the header of the API Call
*
* @param string $content
* @return string
*/
private function buildHeader($content)
private function buildHeader(string $content): string
{
$header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n";
$header .= "Host: api.flowdock.com\r\n";

View File

@@ -16,8 +16,6 @@ use Monolog\Formatter\FormatterInterface;
/**
* Interface to describe loggers that have a formatter
*
* This interface is present in monolog 1.x to ease forward compatibility.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface FormattableHandlerInterface

View File

@@ -17,20 +17,17 @@ use Monolog\Formatter\LineFormatter;
/**
* Helper trait for implementing FormattableInterface
*
* This trait is present in monolog 1.x to ease forward compatibility.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
trait FormattableHandlerTrait
{
/**
* @var FormatterInterface
* @var ?FormatterInterface
*/
protected $formatter;
/**
* {@inheritdoc}
* @suppress PhanTypeMismatchReturn
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
@@ -40,7 +37,7 @@ trait FormattableHandlerTrait
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,12 +11,10 @@
namespace Monolog\Handler;
use Gelf\IMessagePublisher;
use Gelf\PublisherInterface;
use Gelf\Publisher;
use InvalidArgumentException;
use Monolog\Logger;
use Monolog\Formatter\GelfMessageFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Handler to send messages to a Graylog2 (http://www.graylog2.org) server
@@ -27,30 +25,24 @@ use Monolog\Formatter\GelfMessageFormatter;
class GelfHandler extends AbstractProcessingHandler
{
/**
* @var Publisher|PublisherInterface|IMessagePublisher the publisher object that sends the message to the server
* @var PublisherInterface 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
* @param PublisherInterface $publisher a gelf publisher object
*/
public function __construct($publisher, $level = Logger::DEBUG, $bubble = true)
public function __construct(PublisherInterface $publisher, $level = Logger::DEBUG, bool $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}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$this->publisher->publish($record['formatted']);
}
@@ -58,7 +50,7 @@ class GelfHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new GelfMessageFormatter();
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -18,16 +18,23 @@ use Monolog\ResettableInterface;
* Forwards records to multiple handlers
*
* @author Lenar Lõhmus <lenar@city.ee>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class GroupHandler extends AbstractHandler
class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface
{
use ProcessableHandlerTrait;
/** @var HandlerInterface[] */
protected $handlers;
/** @var bool */
protected $bubble;
/**
* @param array $handlers Array of Handlers.
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
* @param HandlerInterface[] $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)
public function __construct(array $handlers, bool $bubble = true)
{
foreach ($handlers as $handler) {
if (!$handler instanceof HandlerInterface) {
@@ -40,9 +47,9 @@ class GroupHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function isHandling(array $record)
public function isHandling(array $record): bool
{
foreach ($this->handlers as $handler) {
if ($handler->isHandling($record)) {
@@ -54,14 +61,13 @@ class GroupHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
/** @var Record $record */
$record = $this->processRecord($record);
}
foreach ($this->handlers as $handler) {
@@ -72,18 +78,16 @@ class GroupHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
if ($this->processors) {
$processed = array();
$processed = [];
foreach ($records as $record) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
$processed[] = $record;
$processed[] = $this->processRecord($record);
}
/** @var Record[] $records */
$records = $processed;
}
@@ -94,7 +98,7 @@ class GroupHandler extends AbstractHandler
public function reset()
{
parent::reset();
$this->resetProcessors();
foreach ($this->handlers as $handler) {
if ($handler instanceof ResettableInterface) {
@@ -103,13 +107,24 @@ class GroupHandler extends AbstractHandler
}
}
public function close(): void
{
parent::close();
foreach ($this->handlers as $handler) {
$handler->close();
}
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
foreach ($this->handlers as $handler) {
$handler->setFormatter($formatter);
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
}
}
return $this;

View File

@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
/*
* 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 Handler class providing basic close() support as well as handleBatch
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
abstract class Handler implements HandlerInterface
{
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
foreach ($records as $record) {
$this->handle($record);
}
}
/**
* {@inheritDoc}
*/
public function close(): void
{
}
public function __destruct()
{
try {
$this->close();
} catch (\Throwable $e) {
// do nothing
}
}
public function __sleep()
{
$this->close();
$reflClass = new \ReflectionClass($this);
$keys = [];
foreach ($reflClass->getProperties() as $reflProp) {
if (!$reflProp->isStatic()) {
$keys[] = $reflProp->getName();
}
}
return $keys;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,12 +11,13 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
/**
* Interface that all Monolog Handlers must implement
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
interface HandlerInterface
{
@@ -32,8 +33,10 @@ interface HandlerInterface
* @param array $record Partial log record containing only a level key
*
* @return bool
*
* @phpstan-param array{level: Level} $record
*/
public function isHandling(array $record);
public function isHandling(array $record): bool;
/**
* Handles a record.
@@ -45,46 +48,38 @@ interface HandlerInterface
* 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.
* @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.
*
* @phpstan-param Record $record
*/
public function handle(array $record);
public function handle(array $record): bool;
/**
* Handles a set of records at once.
*
* @param array $records The records to handle (an array of record arrays)
*
* @phpstan-param Record[] $records
*/
public function handleBatch(array $records);
public function handleBatch(array $records): void;
/**
* Adds a processor in the stack.
* Closes the handler.
*
* @param callable $callback
* @return self
*/
public function pushProcessor($callback);
/**
* Removes the processor on top of the stack and returns it.
* Ends a log cycle and frees all resources used by the handler.
*
* @return callable
*/
public function popProcessor();
/**
* Sets the formatter.
* Closing a Handler means flushing all buffers and freeing any open resources/handles.
*
* @param FormatterInterface $formatter
* @return self
*/
public function setFormatter(FormatterInterface $formatter);
/**
* Gets the formatter.
* Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage)
* and ideally handlers should be able to reopen themselves on handle() after they have been closed.
*
* @return FormatterInterface
* This is useful at the end of a request and will be called automatically when the object
* is destroyed if you extend Monolog\Handler\Handler.
*
* If you are thinking of calling this method yourself, most likely you should be
* calling ResettableInterface::reset instead. Have a look.
*/
public function getFormatter();
public function close(): void;
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -31,86 +31,106 @@ use Monolog\Formatter\FormatterInterface;
*
* @author Alexey Karapetov <alexey@karapetov.com>
*/
class HandlerWrapper implements HandlerInterface, ResettableInterface
class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface
{
/**
* @var HandlerInterface
*/
protected $handler;
/**
* HandlerWrapper constructor.
* @param HandlerInterface $handler
*/
public function __construct(HandlerInterface $handler)
{
$this->handler = $handler;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function isHandling(array $record)
public function isHandling(array $record): bool
{
return $this->handler->isHandling($record);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
return $this->handler->handle($record);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
return $this->handler->handleBatch($records);
$this->handler->handleBatch($records);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function pushProcessor($callback)
public function close(): void
{
$this->handler->pushProcessor($callback);
return $this;
$this->handler->close();
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function popProcessor()
public function pushProcessor(callable $callback): HandlerInterface
{
return $this->handler->popProcessor();
if ($this->handler instanceof ProcessableHandlerInterface) {
$this->handler->pushProcessor($callback);
return $this;
}
throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function popProcessor(): callable
{
$this->handler->setFormatter($formatter);
if ($this->handler instanceof ProcessableHandlerInterface) {
return $this->handler->popProcessor();
}
return $this;
throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter()
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
return $this->handler->getFormatter();
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
}
throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class);
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class);
}
public function reset()
{
if ($this->handler instanceof ResettableInterface) {
return $this->handler->reset();
$this->handler->reset();
}
}
}

View File

@@ -1,367 +0,0 @@
<?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)
{
@trigger_error('The Monolog\Handler\HipChatHandler class is deprecated. You should migrate to Slack and the SlackWebhookHandler / SlackbotHandler, see https://www.atlassian.com/partnerships/slack', E_USER_DEPRECATED);
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 array $records
* @return array
*/
private function combineRecords(array $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);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -27,17 +27,21 @@ use Monolog\Utils;
*/
class IFTTTHandler extends AbstractProcessingHandler
{
/** @var string */
private $eventName;
/** @var string */
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)
public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the IFTTTHandler');
}
$this->eventName = $eventName;
$this->secretKey = $secretKey;
@@ -45,15 +49,15 @@ class IFTTTHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function write(array $record)
public function write(array $record): void
{
$postData = array(
$postData = [
"value1" => $record["channel"],
"value2" => $record["level_name"],
"value3" => $record["message"],
);
];
$postString = Utils::jsonEncode($postData);
$ch = curl_init();
@@ -61,9 +65,9 @@ class IFTTTHandler extends AbstractProcessingHandler
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
));
]);
Curl\Util::execute($ch);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Monolog\Handler;
use Monolog\Logger;
namespace Monolog\Handler;
use Monolog\Logger;
/**
* Inspired on LogEntriesHandler.
@@ -27,16 +27,24 @@ class InsightOpsHandler extends SocketHandler
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.
* @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
*
* @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)
{
public function __construct(
string $token,
string $region = 'us',
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if ($useSSL && !extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler');
}
@@ -45,17 +53,23 @@ class InsightOpsHandler extends SocketHandler
? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443'
: $region . '.data.logs.insight.rapid7.com:80';
parent::__construct($endpoint, $level, $bubble);
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
}
/**
* {@inheritdoc}
*
* @param array $record
* @return string
* {@inheritDoc}
*/
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -24,31 +24,46 @@ class LogEntriesHandler extends SocketHandler
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.
* @param string $token Log token supplied by LogEntries
* @param bool $useSSL Whether or not SSL encryption should be used.
* @param string $host Custom hostname to send the data to if needed
*
* @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')
{
public function __construct(
string $token,
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
string $host = 'data.logentries.com',
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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);
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
}
/**
* {@inheritdoc}
*
* @param array $record
* @return string
* {@inheritDoc}
*/
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,7 +12,10 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LogglyFormatter;
use function array_key_exists;
use CurlHandle;
/**
* Sends errors to Loggly.
@@ -23,18 +26,32 @@ use Monolog\Formatter\LogglyFormatter;
*/
class LogglyHandler extends AbstractProcessingHandler
{
const HOST = 'logs-01.loggly.com';
const ENDPOINT_SINGLE = 'inputs';
const ENDPOINT_BATCH = 'bulk';
protected const HOST = 'logs-01.loggly.com';
protected const ENDPOINT_SINGLE = 'inputs';
protected const ENDPOINT_BATCH = 'bulk';
/**
* Caches the curl handlers for every given endpoint.
*
* @var resource[]|CurlHandle[]
*/
protected $curlHandlers = [];
/** @var string */
protected $token;
protected $tag = array();
/** @var string[] */
protected $tag = [];
public function __construct($token, $level = Logger::DEBUG, $bubble = true)
/**
* @param string $token API token supplied by Loggly
*
* @throws MissingExtensionException If the curl extension is missing
*/
public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new \LogicException('The curl extension is needed to use the LogglyHandler');
throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler');
}
$this->token = $token;
@@ -42,26 +59,72 @@ class LogglyHandler extends AbstractProcessingHandler
parent::__construct($level, $bubble);
}
public function setTag($tag)
/**
* Loads and returns the shared curl handler for the given endpoint.
*
* @param string $endpoint
*
* @return resource|CurlHandle
*/
protected function getCurlHandler(string $endpoint)
{
$tag = !empty($tag) ? $tag : array();
$this->tag = is_array($tag) ? $tag : array($tag);
if (!array_key_exists($endpoint, $this->curlHandlers)) {
$this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint);
}
return $this->curlHandlers[$endpoint];
}
public function addTag($tag)
/**
* Starts a fresh curl session for the given endpoint and returns its handler.
*
* @param string $endpoint
*
* @return resource|CurlHandle
*/
private function loadCurlHandle(string $endpoint)
{
$url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
return $ch;
}
/**
* @param string[]|string $tag
*/
public function setTag($tag): self
{
$tag = !empty($tag) ? $tag : [];
$this->tag = is_array($tag) ? $tag : [$tag];
return $this;
}
/**
* @param string[]|string $tag
*/
public function addTag($tag): self
{
if (!empty($tag)) {
$tag = is_array($tag) ? $tag : array($tag);
$tag = is_array($tag) ? $tag : [$tag];
$this->tag = array_unique(array_merge($this->tag, $tag));
}
return $this;
}
protected function write(array $record)
protected function write(array $record): void
{
$this->send($record["formatted"], self::ENDPOINT_SINGLE);
$this->send($record["formatted"], static::ENDPOINT_SINGLE);
}
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
$level = $this->level;
@@ -70,32 +133,27 @@ class LogglyHandler extends AbstractProcessingHandler
});
if ($records) {
$this->send($this->getFormatter()->formatBatch($records), self::ENDPOINT_BATCH);
$this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH);
}
}
protected function send($data, $endpoint)
protected function send(string $data, string $endpoint): void
{
$url = sprintf("https://%s/%s/%s/", self::HOST, $endpoint, $this->token);
$ch = $this->getCurlHandler($endpoint);
$headers = array('Content-Type: application/json');
$headers = ['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);
Curl\Util::execute($ch, 5, false);
}
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LogglyFormatter();
}

View File

@@ -0,0 +1,106 @@
<?php declare(strict_types=1);
/*
* 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\LogmaticFormatter;
/**
* @author Julien Breux <julien.breux@gmail.com>
*/
class LogmaticHandler extends SocketHandler
{
/**
* @var string
*/
private $logToken;
/**
* @var string
*/
private $hostname;
/**
* @var string
*/
private $appname;
/**
* @param string $token Log token supplied by Logmatic.
* @param string $hostname Host name supplied by Logmatic.
* @param string $appname Application name supplied by Logmatic.
* @param bool $useSSL Whether or not SSL encryption should be used.
*
* @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
*/
public function __construct(
string $token,
string $hostname = '',
string $appname = '',
bool $useSSL = true,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
if ($useSSL && !extension_loaded('openssl')) {
throw new MissingExtensionException('The OpenSSL PHP extension is required to use SSL encrypted connection for LogmaticHandler');
}
$endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514';
$endpoint .= '/v1/';
parent::__construct(
$endpoint,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->logToken = $token;
$this->hostname = $hostname;
$this->appname = $appname;
}
/**
* {@inheritDoc}
*/
protected function generateDataStream(array $record): string
{
return $this->logToken . ' ' . $record['formatted'];
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
$formatter = new LogmaticFormatter();
if (!empty($this->hostname)) {
$formatter->setHostname($this->hostname);
}
if (!empty($this->appname)) {
$formatter->setAppname($this->appname);
}
return $formatter;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,25 +11,32 @@
namespace Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\HtmlFormatter;
/**
* Base class for all mail handlers
*
* @author Gyula Sallai
*
* @phpstan-import-type Record from \Monolog\Logger
*/
abstract class MailHandler extends AbstractProcessingHandler
{
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handleBatch(array $records)
public function handleBatch(array $records): void
{
$messages = array();
$messages = [];
foreach ($records as $record) {
if ($record['level'] < $this->level) {
continue;
}
$messages[] = $this->processRecord($record);
/** @var Record $message */
$message = $this->processRecord($record);
$messages[] = $message;
}
if (!empty($messages)) {
@@ -42,18 +49,24 @@ abstract class MailHandler extends AbstractProcessingHandler
*
* @param string $content formatted email body to be sent
* @param array $records the array of log records that formed this content
*
* @phpstan-param Record[] $records
*/
abstract protected function send($content, array $records);
abstract protected function send(string $content, array $records): void;
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$this->send((string) $record['formatted'], array($record));
$this->send((string) $record['formatted'], [$record]);
}
protected function getHighestRecord(array $records)
/**
* @phpstan-param non-empty-array<Record> $records
* @phpstan-return Record
*/
protected function getHighestRecord(array $records): array
{
$highestRecord = null;
foreach ($records as $record) {
@@ -64,4 +77,19 @@ abstract class MailHandler extends AbstractProcessingHandler
return $highestRecord;
}
protected function isHtmlBody(string $body): bool
{
return ($body[0] ?? null) === '<';
}
/**
* Gets the default formatter.
*
* @return FormatterInterface
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new HtmlFormatter();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,8 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Swift;
use Swift_Message;
/**
* MandrillHandler uses cURL to send the emails to the Mandrill API
@@ -20,23 +22,25 @@ use Monolog\Logger;
*/
class MandrillHandler extends MailHandler
{
/** @var Swift_Message */
protected $message;
/** @var string */
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
* @psalm-param Swift_Message|callable(): Swift_Message $message
*
* @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
*/
public function __construct($apiKey, $message, $level = Logger::ERROR, $bubble = true)
public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
if (!$message instanceof \Swift_Message && is_callable($message)) {
$message = call_user_func($message);
if (!$message instanceof Swift_Message && is_callable($message)) {
$message = $message();
}
if (!$message instanceof \Swift_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;
@@ -44,15 +48,22 @@ class MandrillHandler extends MailHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function send($content, array $records)
protected function send(string $content, array $records): void
{
$mime = 'text/plain';
if ($this->isHtmlBody($content)) {
$mime = 'text/html';
}
$message = clone $this->message;
$message->setBody($content);
if (version_compare(\Swift::VERSION, '6.0.0', '>=')) {
$message->setBody($content, $mime);
/** @phpstan-ignore-next-line */
if (version_compare(Swift::VERSION, '6.0.0', '>=')) {
$message->setDate(new \DateTimeImmutable());
} else {
/** @phpstan-ignore-next-line */
$message->setDate(time());
}
@@ -61,11 +72,11 @@ class MandrillHandler extends MailHandler
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(
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'key' => $this->apiKey,
'raw_message' => (string) $message,
'async' => false,
)));
]));
Curl\Util::execute($ch);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,9 +12,9 @@
namespace Monolog\Handler;
/**
* Exception can be thrown if an extension for an handler is missing
* Exception can be thrown if an extension for a handler is missing
*
* @author Christian Bergau <cbergau86@gmail.com>
* @author Christian Bergau <cbergau86@gmail.com>
*/
class MissingExtensionException extends \Exception
{

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,49 +11,76 @@
namespace Monolog\Handler;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Manager;
use MongoDB\Client;
use Monolog\Logger;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\MongoDBFormatter;
/**
* Logs to a MongoDB database.
*
* usage example:
* Usage example:
*
* $log = new Logger('application');
* $mongodb = new MongoDBHandler(new \Mongo("mongodb://localhost:27017"), "logs", "prod");
* $log = new \Monolog\Logger('application');
* $client = new \MongoDB\Client('mongodb://localhost:27017');
* $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod');
* $log->pushHandler($mongodb);
*
* @author Thomas Tourlourat <thomas@tourlourat.com>
* The above examples uses the MongoDB PHP library's client class; however, the
* MongoDB\Driver\Manager class from ext-mongodb is also supported.
*/
class MongoDBHandler extends AbstractProcessingHandler
{
protected $mongoCollection;
/** @var \MongoDB\Collection */
private $collection;
/** @var Client|Manager */
private $manager;
/** @var string */
private $namespace;
public function __construct($mongo, $database, $collection, $level = Logger::DEBUG, $bubble = true)
/**
* Constructor.
*
* @param Client|Manager $mongodb MongoDB library or driver client
* @param string $database Database name
* @param string $collection Collection name
*/
public function __construct($mongodb, string $database, string $collection, $level = Logger::DEBUG, bool $bubble = true)
{
if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo || $mongo instanceof \MongoDB\Client)) {
throw new \InvalidArgumentException('MongoClient, Mongo or MongoDB\Client instance required');
if (!($mongodb instanceof Client || $mongodb instanceof Manager)) {
throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required');
}
$this->mongoCollection = $mongo->selectCollection($database, $collection);
if ($mongodb instanceof Client) {
$this->collection = $mongodb->selectCollection($database, $collection);
} else {
$this->manager = $mongodb;
$this->namespace = $database . '.' . $collection;
}
parent::__construct($level, $bubble);
}
protected function write(array $record)
protected function write(array $record): void
{
if ($this->mongoCollection instanceof \MongoDB\Collection) {
$this->mongoCollection->insertOne($record["formatted"]);
} else {
$this->mongoCollection->save($record["formatted"]);
if (isset($this->collection)) {
$this->collection->insertOne($record['formatted']);
}
if (isset($this->manager, $this->namespace)) {
$bulk = new BulkWrite;
$bulk->insert($record["formatted"]);
$this->manager->executeBulkWrite($this->namespace, $bulk);
}
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter();
return new MongoDBFormatter;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -24,7 +24,7 @@ class NativeMailerHandler extends MailHandler
{
/**
* The email addresses to which the message will be sent
* @var array
* @var string[]
*/
protected $to;
@@ -36,15 +36,15 @@ class NativeMailerHandler extends MailHandler
/**
* Optional headers for the message
* @var array
* @var string[]
*/
protected $headers = array();
protected $headers = [];
/**
* Optional parameters for the message
* @var array
* @var string[]
*/
protected $parameters = array();
protected $parameters = [];
/**
* The wordwrap length for the message
@@ -54,9 +54,9 @@ class NativeMailerHandler extends MailHandler
/**
* The Content-type for the message
* @var string
* @var string|null
*/
protected $contentType = 'text/plain';
protected $contentType;
/**
* The encoding for the message
@@ -65,17 +65,15 @@ class NativeMailerHandler extends MailHandler
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
* @param string|string[] $to The receiver of the mail
* @param string $subject The subject of the mail
* @param string $from The sender of the mail
* @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)
public function __construct($to, string $subject, string $from, $level = Logger::ERROR, bool $bubble = true, int $maxColumnWidth = 70)
{
parent::__construct($level, $bubble);
$this->to = is_array($to) ? $to : array($to);
$this->to = (array) $to;
$this->subject = $subject;
$this->addHeader(sprintf('From: %s', $from));
$this->maxColumnWidth = $maxColumnWidth;
@@ -84,10 +82,9 @@ class NativeMailerHandler extends MailHandler
/**
* Add headers to the message
*
* @param string|array $headers Custom added headers
* @return self
* @param string|string[] $headers Custom added headers
*/
public function addHeader($headers)
public function addHeader($headers): self
{
foreach ((array) $headers as $header) {
if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) {
@@ -102,10 +99,9 @@ class NativeMailerHandler extends MailHandler
/**
* Add parameters to the message
*
* @param string|array $parameters Custom added parameters
* @return self
* @param string|string[] $parameters Custom added parameters
*/
public function addParameter($parameters)
public function addParameter($parameters): self
{
$this->parameters = array_merge($this->parameters, (array) $parameters);
@@ -113,14 +109,19 @@ class NativeMailerHandler extends MailHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function send($content, array $records)
protected function send(string $content, array $records): void
{
$content = wordwrap($content, $this->maxColumnWidth);
$contentType = $this->getContentType() ?: ($this->isHtmlBody($content) ? 'text/html' : 'text/plain');
if ($contentType !== 'text/html') {
$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 .= 'Content-type: ' . $contentType . '; charset=' . $this->getEncoding() . "\r\n";
if ($contentType === 'text/html' && false === strpos($headers, 'MIME-Version:')) {
$headers .= 'MIME-Version: 1.0' . "\r\n";
}
@@ -136,28 +137,20 @@ class NativeMailerHandler extends MailHandler
}
}
/**
* @return string $contentType
*/
public function getContentType()
public function getContentType(): ?string
{
return $this->contentType;
}
/**
* @return string $encoding
*/
public function getEncoding()
public function getEncoding(): string
{
return $this->encoding;
}
/**
* @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML
* messages.
* @return self
* @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML messages.
*/
public function setContentType($contentType)
public function setContentType(string $contentType): self
{
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');
@@ -168,11 +161,7 @@ class NativeMailerHandler extends MailHandler
return $this;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
public function setEncoding(string $encoding): self
{
if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) {
throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection');

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -14,6 +14,7 @@ namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;
/**
* Class to record a log on a NewRelic application.
@@ -29,14 +30,14 @@ class NewRelicHandler extends AbstractProcessingHandler
/**
* Name of the New Relic application that will receive logs from this handler.
*
* @var string
* @var ?string
*/
protected $appName;
/**
* Name of the current transaction
*
* @var string
* @var ?string
*/
protected $transactionName;
@@ -51,16 +52,16 @@ class NewRelicHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*
* @param string $appName
* @param bool $explodeArrays
* @param string $transactionName
* @param string|null $appName
* @param bool $explodeArrays
* @param string|null $transactionName
*/
public function __construct(
$level = Logger::ERROR,
$bubble = true,
$appName = null,
$explodeArrays = false,
$transactionName = null
bool $bubble = true,
?string $appName = null,
bool $explodeArrays = false,
?string $transactionName = null
) {
parent::__construct($level, $bubble);
@@ -72,7 +73,7 @@ class NewRelicHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
if (!$this->isNewRelicEnabled()) {
throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler');
@@ -87,7 +88,7 @@ class NewRelicHandler extends AbstractProcessingHandler
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))) {
if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
newrelic_notice_error($record['message'], $record['context']['exception']);
unset($record['formatted']['context']['exception']);
} else {
@@ -124,7 +125,7 @@ class NewRelicHandler extends AbstractProcessingHandler
*
* @return bool
*/
protected function isNewRelicEnabled()
protected function isNewRelicEnabled(): bool
{
return extension_loaded('newrelic');
}
@@ -133,10 +134,9 @@ class NewRelicHandler extends AbstractProcessingHandler
* 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
* @param mixed[] $context
*/
protected function getAppName(array $context)
protected function getAppName(array $context): ?string
{
if (isset($context['appname'])) {
return $context['appname'];
@@ -149,11 +149,9 @@ class NewRelicHandler extends AbstractProcessingHandler
* 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
* @param mixed[] $context
*/
protected function getTransactionName(array $context)
protected function getTransactionName(array $context): ?string
{
if (isset($context['transaction_name'])) {
return $context['transaction_name'];
@@ -164,20 +162,16 @@ class NewRelicHandler extends AbstractProcessingHandler
/**
* Sets the NewRelic application that should receive this log.
*
* @param string $appName
*/
protected function setNewRelicAppName($appName)
protected function setNewRelicAppName(string $appName): void
{
newrelic_set_appname($appName);
}
/**
* Overwrites the name of the current transaction
*
* @param string $transactionName
*/
protected function setNewRelicTransactionName($transactionName)
protected function setNewRelicTransactionName(string $transactionName): void
{
newrelic_name_transaction($transactionName);
}
@@ -186,7 +180,7 @@ class NewRelicHandler extends AbstractProcessingHandler
* @param string $key
* @param mixed $value
*/
protected function setNewRelicParameter($key, $value)
protected function setNewRelicParameter(string $key, $value): void
{
if (null === $value || is_scalar($value)) {
newrelic_add_custom_parameter($key, $value);
@@ -198,7 +192,7 @@ class NewRelicHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new NormalizerFormatter();
}

View File

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/*
* 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;
/**
* No-op
*
* This handler handles anything, but does nothing, and does not stop bubbling to the rest of the stack.
* This can be used for testing, or to disable a handler when overriding a configuration without
* influencing the rest of the stack.
*
* @author Roel Harbers <roelharbers@gmail.com>
*/
class NoopHandler extends Handler
{
/**
* {@inheritDoc}
*/
public function isHandling(array $record): bool
{
return true;
}
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
return false;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Psr\Log\LogLevel;
/**
* Blackhole
@@ -20,26 +21,40 @@ use Monolog\Logger;
* to put on top of an existing stack to override it temporarily.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class NullHandler extends AbstractHandler
class NullHandler extends Handler
{
/**
* @param int $level The minimum logging level at which this handler will be triggered
* @var int
*/
private $level;
/**
* @param string|int $level The minimum logging level at which this handler will be triggered
*
* @phpstan-param Level|LevelName|LogLevel::* $level
*/
public function __construct($level = Logger::DEBUG)
{
parent::__construct($level, false);
$this->level = Logger::toMonologLevel($level);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function handle(array $record)
public function isHandling(array $record): bool
{
if ($record['level'] < $this->level) {
return false;
}
return $record['level'] >= $this->level;
}
return true;
/**
* {@inheritDoc}
*/
public function handle(array $record): bool
{
return $record['level'] >= $this->level;
}
}

View File

@@ -0,0 +1,149 @@
<?php declare(strict_types=1);
/*
* 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;
/**
* Handler to only pass log messages when a certain threshold of number of messages is reached.
*
* This can be useful in cases of processing a batch of data, but you're for example only interested
* in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right?
*
* Usage example:
*
* ```
* $log = new Logger('application');
* $handler = new SomeHandler(...)
*
* // Pass all warnings to the handler when more than 10 & all error messages when more then 5
* $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]);
*
* $log->pushHandler($overflow);
*```
*
* @author Kris Buist <krisbuist@gmail.com>
*/
class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface
{
/** @var HandlerInterface */
private $handler;
/** @var int[] */
private $thresholdMap = [
Logger::DEBUG => 0,
Logger::INFO => 0,
Logger::NOTICE => 0,
Logger::WARNING => 0,
Logger::ERROR => 0,
Logger::CRITICAL => 0,
Logger::ALERT => 0,
Logger::EMERGENCY => 0,
];
/**
* Buffer of all messages passed to the handler before the threshold was reached
*
* @var mixed[][]
*/
private $buffer = [];
/**
* @param HandlerInterface $handler
* @param int[] $thresholdMap Dictionary of logger level => threshold
*/
public function __construct(
HandlerInterface $handler,
array $thresholdMap = [],
$level = Logger::DEBUG,
bool $bubble = true
) {
$this->handler = $handler;
foreach ($thresholdMap as $thresholdLevel => $threshold) {
$this->thresholdMap[$thresholdLevel] = $threshold;
}
parent::__construct($level, $bubble);
}
/**
* 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.
*
* {@inheritDoc}
*/
public function handle(array $record): bool
{
if ($record['level'] < $this->level) {
return false;
}
$level = $record['level'];
if (!isset($this->thresholdMap[$level])) {
$this->thresholdMap[$level] = 0;
}
if ($this->thresholdMap[$level] > 0) {
// The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1
$this->thresholdMap[$level]--;
$this->buffer[$level][] = $record;
return false === $this->bubble;
}
if ($this->thresholdMap[$level] == 0) {
// This current message is breaking the threshold. Flush the buffer and continue handling the current record
foreach ($this->buffer[$level] ?? [] as $buffered) {
$this->handler->handle($buffered);
}
$this->thresholdMap[$level]--;
unset($this->buffer[$level]);
}
$this->handler->handle($record);
return false === $this->bubble;
}
/**
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
$this->handler->setFormatter($formatter);
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
/**
* {@inheritDoc}
*/
public function getFormatter(): FormatterInterface
{
if ($this->handler instanceof FormattableHandlerInterface) {
return $this->handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,12 +11,12 @@
namespace Monolog\Handler;
use Exception;
use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use PhpConsole\Connector;
use PhpConsole\Handler;
use PhpConsole\Handler as VendorPhpConsoleHandler;
use PhpConsole\Helper;
/**
@@ -25,7 +25,7 @@ use PhpConsole\Helper;
* 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
* 1. Install Google Chrome extension [now dead and removed from the chrome store]
* 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)
@@ -33,17 +33,21 @@ use PhpConsole\Helper;
* $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));
* $logger->debug('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
*
* @phpstan-import-type Record from \Monolog\Logger
* @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4
*/
class PHPConsoleHandler extends AbstractProcessingHandler
{
private $options = array(
/** @var array<string, mixed> */
private $options = [
'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
'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with...
'debugTagsKeysInContext' => [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
@@ -52,7 +56,7 @@ class PHPConsoleHandler extends AbstractProcessingHandler
'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')
'ipMasks' => [], // 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
@@ -60,40 +64,43 @@ class PHPConsoleHandler extends AbstractProcessingHandler
'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)
);
'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
* @param array<string, mixed> $options See \Monolog\Handler\PHPConsoleHandler::$options for more details
* @param Connector|null $connector Instance of \PhpConsole\Connector class (optional)
* @throws \RuntimeException
*/
public function __construct(array $options = array(), Connector $connector = null, $level = Logger::DEBUG, $bubble = true)
public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true)
{
if (!class_exists('PhpConsole\Connector')) {
throw new Exception('PHP Console library not found. See https://github.com/barbushin/php-console#installation');
throw new \RuntimeException('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)
/**
* @param array<string, mixed> $options
*
* @return array<string, mixed>
*/
private function initOptions(array $options): array
{
$wrongOptions = array_diff(array_keys($options), array_keys($this->options));
if ($wrongOptions) {
throw new Exception('Unknown options: ' . implode(', ', $wrongOptions));
throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions));
}
return array_replace($this->options, $options);
}
private function initConnector(Connector $connector = null)
private function initConnector(?Connector $connector = null): Connector
{
if (!$connector) {
if ($this->options['dataStorage']) {
@@ -108,7 +115,7 @@ class PHPConsoleHandler extends AbstractProcessingHandler
if ($this->options['enabled'] && $connector->isActiveClient()) {
if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) {
$handler = Handler::getInstance();
$handler = VendorPhpConsoleHandler::getInstance();
$handler->setHandleErrors($this->options['useOwnErrorsHandler']);
$handler->setHandleExceptions($this->options['useOwnExceptionsHandler']);
$handler->start();
@@ -148,17 +155,20 @@ class PHPConsoleHandler extends AbstractProcessingHandler
return $connector;
}
public function getConnector()
public function getConnector(): Connector
{
return $this->connector;
}
public function getOptions()
/**
* @return array<string, mixed>
*/
public function getOptions(): array
{
return $this->options;
}
public function handle(array $record)
public function handle(array $record): bool
{
if ($this->options['enabled'] && $this->connector->isActiveClient()) {
return parent::handle($record);
@@ -169,22 +179,22 @@ class PHPConsoleHandler extends AbstractProcessingHandler
/**
* Writes the record down to the log of the implementing handler
*
* @param array $record
* @return void
*/
protected function write(array $record)
protected function write(array $record): void
{
if ($record['level'] < Logger::NOTICE) {
$this->handleDebugRecord($record);
} elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof Exception) {
} elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
$this->handleExceptionRecord($record);
} else {
$this->handleErrorRecord($record);
}
}
private function handleDebugRecord(array $record)
/**
* @phpstan-param Record $record
*/
private function handleDebugRecord(array $record): void
{
$tags = $this->getRecordTags($record);
$message = $record['message'];
@@ -194,24 +204,34 @@ class PHPConsoleHandler extends AbstractProcessingHandler
$this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']);
}
private function handleExceptionRecord(array $record)
/**
* @phpstan-param Record $record
*/
private function handleExceptionRecord(array $record): void
{
$this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']);
}
private function handleErrorRecord(array $record)
/**
* @phpstan-param Record $record
*/
private function handleErrorRecord(array $record): void
{
$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,
$context['code'] ?? null,
$context['message'] ?? $record['message'],
$context['file'] ?? null,
$context['line'] ?? null,
$this->options['classesPartialsTraceIgnore']
);
}
/**
* @phpstan-param Record $record
* @return string
*/
private function getRecordTags(array &$record)
{
$tags = null;
@@ -236,7 +256,7 @@ class PHPConsoleHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter('%message%');
}

View File

@@ -0,0 +1,191 @@
<?php declare(strict_types=1);
/*
* 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 STDIN of any process, specified by a command.
*
* Usage example:
* <pre>
* $log = new Logger('myLogger');
* $log->pushHandler(new ProcessHandler('/usr/bin/php /var/www/monolog/someScript.php'));
* </pre>
*
* @author Kolja Zuelsdorf <koljaz@web.de>
*/
class ProcessHandler extends AbstractProcessingHandler
{
/**
* Holds the process to receive data on its STDIN.
*
* @var resource|bool|null
*/
private $process;
/**
* @var string
*/
private $command;
/**
* @var string|null
*/
private $cwd;
/**
* @var resource[]
*/
private $pipes = [];
/**
* @var array<int, string[]>
*/
protected const DESCRIPTOR_SPEC = [
0 => ['pipe', 'r'], // STDIN is a pipe that the child will read from
1 => ['pipe', 'w'], // STDOUT is a pipe that the child will write to
2 => ['pipe', 'w'], // STDERR is a pipe to catch the any errors
];
/**
* @param string $command Command for the process to start. Absolute paths are recommended,
* especially if you do not use the $cwd parameter.
* @param string|null $cwd "Current working directory" (CWD) for the process to be executed in.
* @throws \InvalidArgumentException
*/
public function __construct(string $command, $level = Logger::DEBUG, bool $bubble = true, ?string $cwd = null)
{
if ($command === '') {
throw new \InvalidArgumentException('The command argument must be a non-empty string.');
}
if ($cwd === '') {
throw new \InvalidArgumentException('The optional CWD argument must be a non-empty string or null.');
}
parent::__construct($level, $bubble);
$this->command = $command;
$this->cwd = $cwd;
}
/**
* Writes the record down to the log of the implementing handler
*
* @throws \UnexpectedValueException
*/
protected function write(array $record): void
{
$this->ensureProcessIsStarted();
$this->writeProcessInput($record['formatted']);
$errors = $this->readProcessErrors();
if (empty($errors) === false) {
throw new \UnexpectedValueException(sprintf('Errors while writing to process: %s', $errors));
}
}
/**
* Makes sure that the process is actually started, and if not, starts it,
* assigns the stream pipes, and handles startup errors, if any.
*/
private function ensureProcessIsStarted(): void
{
if (is_resource($this->process) === false) {
$this->startProcess();
$this->handleStartupErrors();
}
}
/**
* Starts the actual process and sets all streams to non-blocking.
*/
private function startProcess(): void
{
$this->process = proc_open($this->command, static::DESCRIPTOR_SPEC, $this->pipes, $this->cwd);
foreach ($this->pipes as $pipe) {
stream_set_blocking($pipe, false);
}
}
/**
* Selects the STDERR stream, handles upcoming startup errors, and throws an exception, if any.
*
* @throws \UnexpectedValueException
*/
private function handleStartupErrors(): void
{
$selected = $this->selectErrorStream();
if (false === $selected) {
throw new \UnexpectedValueException('Something went wrong while selecting a stream.');
}
$errors = $this->readProcessErrors();
if (is_resource($this->process) === false || empty($errors) === false) {
throw new \UnexpectedValueException(
sprintf('The process "%s" could not be opened: ' . $errors, $this->command)
);
}
}
/**
* Selects the STDERR stream.
*
* @return int|bool
*/
protected function selectErrorStream()
{
$empty = [];
$errorPipes = [$this->pipes[2]];
return stream_select($errorPipes, $empty, $empty, 1);
}
/**
* Reads the errors of the process, if there are any.
*
* @codeCoverageIgnore
* @return string Empty string if there are no errors.
*/
protected function readProcessErrors(): string
{
return (string) stream_get_contents($this->pipes[2]);
}
/**
* Writes to the input stream of the opened process.
*
* @codeCoverageIgnore
*/
protected function writeProcessInput(string $string): void
{
fwrite($this->pipes[0], $string);
}
/**
* {@inheritDoc}
*/
public function close(): void
{
if (is_resource($this->process)) {
foreach ($this->pipes as $pipe) {
fclose($pipe);
}
proc_close($this->process);
$this->process = null;
}
}
}

View File

@@ -16,25 +16,29 @@ use Monolog\Processor\ProcessorInterface;
/**
* Interface to describe loggers that have processors
*
* This interface is present in monolog 1.x to ease forward compatibility.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
interface ProcessableHandlerInterface
{
/**
* Adds a processor in the stack.
*
* @psalm-param ProcessorInterface|callable(Record): Record $callback
*
* @param ProcessorInterface|callable $callback
* @return HandlerInterface self
*/
public function pushProcessor($callback): HandlerInterface;
public function pushProcessor(callable $callback): HandlerInterface;
/**
* Removes the processor on top of the stack and returns it.
*
* @throws \LogicException In case the processor stack is empty
* @return callable
* @psalm-return ProcessorInterface|callable(Record): Record $callback
*
* @throws \LogicException In case the processor stack is empty
* @return callable|ProcessorInterface
*/
public function popProcessor(): callable;
}

View File

@@ -12,26 +12,27 @@
namespace Monolog\Handler;
use Monolog\ResettableInterface;
use Monolog\Processor\ProcessorInterface;
/**
* Helper trait for implementing ProcessableInterface
*
* This trait is present in monolog 1.x to ease forward compatibility.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
trait ProcessableHandlerTrait
{
/**
* @var callable[]
* @phpstan-var array<ProcessorInterface|callable(Record): Record>
*/
protected $processors = [];
/**
* {@inheritdoc}
* @suppress PhanTypeMismatchReturn
* {@inheritDoc}
*/
public function pushProcessor($callback): HandlerInterface
public function pushProcessor(callable $callback): HandlerInterface
{
array_unshift($this->processors, $callback);
@@ -39,7 +40,7 @@ trait ProcessableHandlerTrait
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function popProcessor(): callable
{
@@ -52,6 +53,9 @@ trait ProcessableHandlerTrait
/**
* Processes a record.
*
* @phpstan-param Record $record
* @phpstan-return Record
*/
protected function processRecord(array $record): array
{

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -13,13 +13,18 @@ namespace Monolog\Handler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Monolog\Formatter\FormatterInterface;
/**
* Proxies log messages to an existing PSR-3 compliant logger.
*
* If a formatter is configured, the formatter's output MUST be a string and the
* formatted message will be fed to the wrapped PSR logger instead of the original
* log record's message.
*
* @author Michael Moussa <michael.moussa@gmail.com>
*/
class PsrHandler extends AbstractHandler
class PsrHandler extends AbstractHandler implements FormattableHandlerInterface
{
/**
* PSR-3 compliant logger
@@ -29,11 +34,14 @@ class PsrHandler extends AbstractHandler
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
* @var FormatterInterface|null
*/
public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, $bubble = true)
protected $formatter;
/**
* @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied
*/
public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
@@ -43,14 +51,45 @@ class PsrHandler extends AbstractHandler
/**
* {@inheritDoc}
*/
public function handle(array $record)
public function handle(array $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
$this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']);
if ($this->formatter) {
$formatted = $this->formatter->format($record);
$this->logger->log(strtolower($record['level_name']), (string) $formatted, $record['context']);
} else {
$this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']);
}
return false === $this->bubble;
}
/**
* Sets the formatter.
*
* @param FormatterInterface $formatter
*/
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->formatter = $formatter;
return $this;
}
/**
* Gets the formatter.
*
* @return FormatterInterface
*/
public function getFormatter(): FormatterInterface
{
if (!$this->formatter) {
throw new \LogicException('No formatter has been set and this handler does not have a default formatter');
}
return $this->formatter;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,32 +12,47 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Utils;
use Psr\Log\LogLevel;
/**
* Sends notifications through the pushover api to mobile phones
*
* @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com>
* @see https://www.pushover.net/api
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
* @phpstan-import-type Level from \Monolog\Logger
* @phpstan-import-type LevelName from \Monolog\Logger
*/
class PushoverHandler extends SocketHandler
{
/** @var string */
private $token;
/** @var array<int|string> */
private $users;
/** @var string */
private $title;
private $user;
/** @var string|int|null */
private $user = null;
/** @var int */
private $retry;
/** @var int */
private $expire;
/** @var int */
private $highPriorityLevel;
/** @var int */
private $emergencyLevel;
/** @var bool */
private $useFormattedMessage = false;
/**
* All parameters that can be sent to Pushover
* @see https://pushover.net/api
* @var array
* @var array<string, bool>
*/
private $parameterNames = array(
private $parameterNames = [
'token' => true,
'user' => true,
'message' => true,
@@ -51,72 +66,103 @@ class PushoverHandler extends SocketHandler
'retry' => true,
'expire' => true,
'callback' => true,
);
];
/**
* Sounds the api supports by default
* @see https://pushover.net/api#sounds
* @var array
* @var string[]
*/
private $sounds = array(
private $sounds = [
'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 string|null $title Title sent to the Pushover API
* @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
* @param string|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
* @param string|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).
* @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).
*
* @phpstan-param string|array<int|string> $users
* @phpstan-param Level|LevelName|LogLevel::* $highPriorityLevel
* @phpstan-param Level|LevelName|LogLevel::* $emergencyLevel
*/
public function __construct($token, $users, $title = null, $level = Logger::CRITICAL, $bubble = true, $useSSL = true, $highPriorityLevel = Logger::CRITICAL, $emergencyLevel = Logger::EMERGENCY, $retry = 30, $expire = 25200)
{
public function __construct(
string $token,
$users,
?string $title = null,
$level = Logger::CRITICAL,
bool $bubble = true,
bool $useSSL = true,
$highPriorityLevel = Logger::CRITICAL,
$emergencyLevel = Logger::EMERGENCY,
int $retry = 30,
int $expire = 25200,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
$connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80';
parent::__construct($connectionString, $level, $bubble);
parent::__construct(
$connectionString,
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->token = $token;
$this->users = (array) $users;
$this->title = $title ?: gethostname();
$this->title = $title ?: (string) gethostname();
$this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel);
$this->emergencyLevel = Logger::toMonologLevel($emergencyLevel);
$this->retry = $retry;
$this->expire = $expire;
}
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
return $this->buildHeader($content) . $content;
}
private function buildContent($record)
/**
* @phpstan-param FormattedRecord $record
*/
private function buildContent(array $record): string
{
// 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);
$message = Utils::substr($message, 0, $maxMessageLength);
$timestamp = $record['datetime']->getTimestamp();
$dataArray = array(
$dataArray = [
'token' => $this->token,
'user' => $this->user,
'message' => $message,
'title' => $this->title,
'timestamp' => $timestamp,
);
];
if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) {
$dataArray['priority'] = 2;
@@ -141,7 +187,7 @@ class PushoverHandler extends SocketHandler
return http_build_query($dataArray);
}
private function buildHeader($content)
private function buildHeader(string $content): string
{
$header = "POST /1/messages.json HTTP/1.1\r\n";
$header .= "Host: api.pushover.net\r\n";
@@ -152,7 +198,7 @@ class PushoverHandler extends SocketHandler
return $header;
}
protected function write(array $record)
protected function write(array $record): void
{
foreach ($this->users as $user) {
$this->user = $user;
@@ -164,22 +210,37 @@ class PushoverHandler extends SocketHandler
$this->user = null;
}
public function setHighPriorityLevel($value)
/**
* @param int|string $value
*
* @phpstan-param Level|LevelName|LogLevel::* $value
*/
public function setHighPriorityLevel($value): self
{
$this->highPriorityLevel = $value;
$this->highPriorityLevel = Logger::toMonologLevel($value);
return $this;
}
public function setEmergencyLevel($value)
/**
* @param int|string $value
*
* @phpstan-param Level|LevelName|LogLevel::* $value
*/
public function setEmergencyLevel($value): self
{
$this->emergencyLevel = $value;
$this->emergencyLevel = Logger::toMonologLevel($value);
return $this;
}
/**
* Use the formatted message?
* @param bool $value
*/
public function useFormattedMessage($value)
public function useFormattedMessage(bool $value): self
{
$this->useFormattedMessage = (bool) $value;
$this->useFormattedMessage = $value;
return $this;
}
}

View File

@@ -1,234 +0,0 @@
<?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 FormatterInterface 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)
{
@trigger_error('The Monolog\Handler\RavenHandler class is deprecated. You should rather upgrade to the sentry/sentry 2.x and use Sentry\Monolog\Handler, see https://github.com/getsentry/sentry-php/blob/master/src/Monolog/Handler.php', E_USER_DEPRECATED);
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 (null === $highest || $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;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler;
use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
/**
@@ -24,21 +25,24 @@ use Monolog\Logger;
* $log->pushHandler($redis);
*
* @author Thomas Tourlourat <thomas@tourlourat.com>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class RedisHandler extends AbstractProcessingHandler
{
/** @var \Predis\Client<\Predis\Client>|\Redis */
private $redisClient;
/** @var string */
private $redisKey;
/** @var int */
protected $capSize;
/**
* @param \Predis\Client|\Redis $redis The redis instance
* @param \Predis\Client<\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|false $capSize Number of entries to limit list size to
* @param int $capSize Number of entries to limit list size to, 0 = unlimited
*/
public function __construct($redis, $key, $level = Logger::DEBUG, $bubble = true, $capSize = false)
public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true, int $capSize = 0)
{
if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) {
throw new \InvalidArgumentException('Predis\Client or Redis instance required');
@@ -54,7 +58,7 @@ class RedisHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
if ($this->capSize) {
$this->writeCapped($record);
@@ -67,10 +71,9 @@ class RedisHandler extends AbstractProcessingHandler
* Write and cap the collection
* Writes the record to the redis list and caps its
*
* @param array $record associative record array
* @return void
* @phpstan-param FormattedRecord $record
*/
protected function writeCapped(array $record)
protected function writeCapped(array $record): void
{
if ($this->redisClient instanceof \Redis) {
$mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1;
@@ -91,7 +94,7 @@ class RedisHandler extends AbstractProcessingHandler
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter()
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter();
}

View File

@@ -0,0 +1,67 @@
<?php declare(strict_types=1);
/*
* 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;
/**
* Sends the message to a Redis Pub/Sub channel using PUBLISH
*
* usage example:
*
* $log = new Logger('application');
* $redis = new RedisPubSubHandler(new Predis\Client("tcp://localhost:6379"), "logs", Logger::WARNING);
* $log->pushHandler($redis);
*
* @author Gaëtan Faugère <gaetan@fauge.re>
*/
class RedisPubSubHandler extends AbstractProcessingHandler
{
/** @var \Predis\Client<\Predis\Client>|\Redis */
private $redisClient;
/** @var string */
private $channelKey;
/**
* @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance
* @param string $key The channel key to publish records to
*/
public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true)
{
if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) {
throw new \InvalidArgumentException('Predis\Client or Redis instance required');
}
$this->redisClient = $redis;
$this->channelKey = $key;
parent::__construct($level, $bubble);
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
$this->redisClient->publish($this->channelKey, $record["formatted"]);
}
/**
* {@inheritDoc}
*/
protected function getDefaultFormatter(): FormatterInterface
{
return new LineFormatter();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,15 +11,15 @@
namespace Monolog\Handler;
use RollbarNotifier;
use Exception;
use Rollbar\RollbarLogger;
use Throwable;
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.
* of payload options to RollbarLogger's log method.
*
* Rollbar's context info will contain the context + extra keys from the log record
* merged, and then on top of that a few keys:
@@ -34,13 +34,12 @@ use Monolog\Logger;
class RollbarHandler extends AbstractProcessingHandler
{
/**
* Rollbar notifier
*
* @var RollbarNotifier
* @var RollbarLogger
*/
protected $rollbarNotifier;
protected $rollbarLogger;
protected $levelMap = array(
/** @var string[] */
protected $levelMap = [
Logger::DEBUG => 'debug',
Logger::INFO => 'info',
Logger::NOTICE => 'info',
@@ -49,7 +48,7 @@ class RollbarHandler extends AbstractProcessingHandler
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
@@ -58,24 +57,23 @@ class RollbarHandler extends AbstractProcessingHandler
*/
private $hasRecords = false;
/** @var bool */
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
* @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token
*/
public function __construct(RollbarNotifier $rollbarNotifier, $level = Logger::ERROR, $bubble = true)
public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, bool $bubble = true)
{
$this->rollbarNotifier = $rollbarNotifier;
$this->rollbarLogger = $rollbarLogger;
parent::__construct($level, $bubble);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
if (!$this->initialized) {
// __destructor() doesn't get called on Fatal errors
@@ -84,54 +82,45 @@ class RollbarHandler extends AbstractProcessingHandler
}
$context = $record['context'];
$payload = array();
if (isset($context['payload'])) {
$payload = $context['payload'];
unset($context['payload']);
}
$context = array_merge($context, $record['extra'], array(
$context = array_merge($context, $record['extra'], [
'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'];
if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
$exception = $context['exception'];
unset($context['exception']);
$this->rollbarNotifier->report_exception($exception, $context, $payload);
$toLog = $exception;
} else {
$this->rollbarNotifier->report_message(
$record['message'],
$context['level'],
$context,
$payload
);
$toLog = $record['message'];
}
// @phpstan-ignore-next-line
$this->rollbarLogger->log($context['level'], $toLog, $context);
$this->hasRecords = true;
}
public function flush()
public function flush(): void
{
if ($this->hasRecords) {
$this->rollbarNotifier->flush();
$this->rollbarLogger->flush();
$this->hasRecords = false;
}
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
$this->flush();
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function reset()
{
@@ -139,6 +128,4 @@ class RollbarHandler extends AbstractProcessingHandler
parent::reset();
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,6 +11,7 @@
namespace Monolog\Handler;
use InvalidArgumentException;
use Monolog\Logger;
use Monolog\Utils;
@@ -25,40 +26,44 @@ use Monolog\Utils;
*/
class RotatingFileHandler extends StreamHandler
{
const FILE_PER_DAY = 'Y-m-d';
const FILE_PER_MONTH = 'Y-m';
const FILE_PER_YEAR = 'Y';
public const FILE_PER_DAY = 'Y-m-d';
public const FILE_PER_MONTH = 'Y-m';
public const FILE_PER_YEAR = 'Y';
/** @var string */
protected $filename;
/** @var int */
protected $maxFiles;
/** @var bool */
protected $mustRotate;
/** @var \DateTimeImmutable */
protected $nextRotation;
/** @var string */
protected $filenameFormat;
/** @var string */
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
* @param string $filename
* @param int $maxFiles The maximal amount of files to keep (0 means unlimited)
* @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)
public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
{
$this->filename = Utils::canonicalizePath($filename);
$this->maxFiles = (int) $maxFiles;
$this->nextRotation = new \DateTime('tomorrow');
$this->maxFiles = $maxFiles;
$this->nextRotation = new \DateTimeImmutable('tomorrow');
$this->filenameFormat = '{filename}-{date}';
$this->dateFormat = 'Y-m-d';
$this->dateFormat = static::FILE_PER_DAY;
parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
parent::close();
@@ -68,7 +73,7 @@ class RotatingFileHandler extends StreamHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function reset()
{
@@ -79,40 +84,40 @@ class RotatingFileHandler extends StreamHandler
}
}
public function setFilenameFormat($filenameFormat, $dateFormat)
public function setFilenameFormat(string $filenameFormat, string $dateFormat): self
{
if (!preg_match('{^Y(([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) {
trigger_error(
if (!preg_match('{^[Yy](([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) {
throw new InvalidArgumentException(
'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
'date formats using slashes, underscores and/or dots instead of dashes.'
);
}
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
throw new InvalidArgumentException(
'Invalid filename format - format must contain at least `{date}`, because otherwise rotating is impossible.'
);
}
$this->filenameFormat = $filenameFormat;
$this->dateFormat = $dateFormat;
$this->url = $this->getTimedFilename();
$this->close();
return $this;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
// 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);
$this->mustRotate = null === $this->url || !file_exists($this->url);
}
if ($this->nextRotation < $record['datetime']) {
if ($this->nextRotation <= $record['datetime']) {
$this->mustRotate = true;
$this->close();
}
@@ -123,11 +128,11 @@ class RotatingFileHandler extends StreamHandler
/**
* Rotates the files.
*/
protected function rotate()
protected function rotate(): void
{
// update filename
$this->url = $this->getTimedFilename();
$this->nextRotation = new \DateTime('tomorrow');
$this->nextRotation = new \DateTimeImmutable('tomorrow');
// skip GC of old logs if files are unlimited
if (0 === $this->maxFiles) {
@@ -135,6 +140,11 @@ class RotatingFileHandler extends StreamHandler
}
$logFiles = glob($this->getGlobPattern());
if (false === $logFiles) {
// failed to glob
return;
}
if ($this->maxFiles >= count($logFiles)) {
// no files to remove
return;
@@ -149,7 +159,9 @@ class RotatingFileHandler extends StreamHandler
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) {});
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
return false;
});
unlink($file);
restore_error_handler();
}
@@ -158,31 +170,35 @@ class RotatingFileHandler extends StreamHandler
$this->mustRotate = false;
}
protected function getTimedFilename()
protected function getTimedFilename(): string
{
$fileInfo = pathinfo($this->filename);
$timedFilename = str_replace(
array('{filename}', '{date}'),
array($fileInfo['filename'], date($this->dateFormat)),
['{filename}', '{date}'],
[$fileInfo['filename'], date($this->dateFormat)],
$fileInfo['dirname'] . '/' . $this->filenameFormat
);
if (!empty($fileInfo['extension'])) {
if (isset($fileInfo['extension'])) {
$timedFilename .= '.'.$fileInfo['extension'];
}
return $timedFilename;
}
protected function getGlobPattern()
protected function getGlobPattern(): string
{
$fileInfo = pathinfo($this->filename);
$glob = str_replace(
array('{filename}', '{date}'),
array($fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'),
['{filename}', '{date}'],
[$fileInfo['filename'], str_replace(
['Y', 'y', 'm', 'd'],
['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'],
$this->dateFormat)
],
$fileInfo['dirname'] . '/' . $this->filenameFormat
);
if (!empty($fileInfo['extension'])) {
if (isset($fileInfo['extension'])) {
$glob .= '.'.$fileInfo['extension'];
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -26,11 +26,17 @@ use Monolog\Formatter\FormatterInterface;
*
* @author Bryan Davis <bd808@wikimedia.org>
* @author Kunal Mehta <legoktm@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type Level from \Monolog\Logger
*/
class SamplingHandler extends AbstractHandler
class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* @var callable|HandlerInterface $handler
* @var HandlerInterface|callable
* @phpstan-var HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface
*/
protected $handler;
@@ -40,10 +46,12 @@ class SamplingHandler extends AbstractHandler
protected $factor;
/**
* @psalm-param HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface $handler
*
* @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler).
* @param int $factor Sample factor
* @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled)
*/
public function __construct($handler, $factor)
public function __construct($handler, int $factor)
{
parent::__construct();
$this->handler = $handler;
@@ -54,18 +62,17 @@ class SamplingHandler extends AbstractHandler
}
}
public function isHandling(array $record)
public function isHandling(array $record): bool
{
return $this->getHandler($record)->isHandling($record);
}
public function handle(array $record)
public function handle(array $record): bool
{
if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) {
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
/** @var Record $record */
$record = $this->processRecord($record);
}
$this->getHandler($record)->handle($record);
@@ -79,12 +86,14 @@ class SamplingHandler extends AbstractHandler
*
* If the handler was provided as a factory callable, this will trigger the handler's instantiation.
*
* @phpstan-param Record|array{level: Level}|null $record
*
* @return HandlerInterface
*/
public function getHandler(array $record = null)
{
if (!$this->handler instanceof HandlerInterface) {
$this->handler = call_user_func($this->handler, $record, $this);
$this->handler = ($this->handler)($record, $this);
if (!$this->handler instanceof HandlerInterface) {
throw new \RuntimeException("The factory callable should return a HandlerInterface");
}
@@ -94,20 +103,30 @@ class SamplingHandler extends AbstractHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
$this->getHandler()->setFormatter($formatter);
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($formatter);
return $this;
return $this;
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function getFormatter()
public function getFormatter(): FormatterInterface
{
return $this->getHandler()->getFormatter();
$handler = $this->getHandler();
if ($handler instanceof FormattableHandlerInterface) {
return $handler->getFormatter();
}
throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
}
}

View File

@@ -0,0 +1,102 @@
<?php declare(strict_types=1);
/*
* 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;
/**
* SendGridrHandler uses the SendGrid API v2 function to send Log emails, more information in https://sendgrid.com/docs/API_Reference/Web_API/mail.html
*
* @author Ricardo Fontanelli <ricardo.fontanelli@hotmail.com>
*/
class SendGridHandler extends MailHandler
{
/**
* The SendGrid API User
* @var string
*/
protected $apiUser;
/**
* The SendGrid API Key
* @var string
*/
protected $apiKey;
/**
* The email addresses to which the message will be sent
* @var string
*/
protected $from;
/**
* The email addresses to which the message will be sent
* @var string[]
*/
protected $to;
/**
* The subject of the email
* @var string
*/
protected $subject;
/**
* @param string $apiUser The SendGrid API User
* @param string $apiKey The SendGrid API Key
* @param string $from The sender of the email
* @param string|string[] $to The recipients of the email
* @param string $subject The subject of the mail
*/
public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the SendGridHandler');
}
parent::__construct($level, $bubble);
$this->apiUser = $apiUser;
$this->apiKey = $apiKey;
$this->from = $from;
$this->to = (array) $to;
$this->subject = $subject;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$message = [];
$message['api_user'] = $this->apiUser;
$message['api_key'] = $this->apiKey;
$message['from'] = $this->from;
foreach ($this->to as $recipient) {
$message['to[]'] = $recipient;
}
$message['subject'] = $this->subject;
$message['date'] = date('r');
if ($this->isHtmlBody($content)) {
$message['html'] = $content;
} else {
$message['text'] = $content;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/api/mail.send.json');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($message));
Curl\Util::execute($ch, 2);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -23,16 +23,19 @@ use Monolog\Formatter\FormatterInterface;
* @author Haralan Dobrev <hkdobrev@gmail.com>
* @see https://api.slack.com/incoming-webhooks
* @see https://api.slack.com/docs/message-attachments
*
* @phpstan-import-type FormattedRecord from \Monolog\Handler\AbstractProcessingHandler
* @phpstan-import-type Record from \Monolog\Logger
*/
class SlackRecord
{
const COLOR_DANGER = 'danger';
public const COLOR_DANGER = 'danger';
const COLOR_WARNING = 'warning';
public const COLOR_WARNING = 'warning';
const COLOR_GOOD = 'good';
public const COLOR_GOOD = 'good';
const COLOR_DEFAULT = '#e3e4e6';
public const COLOR_DEFAULT = '#e3e4e6';
/**
* Slack channel (encoded ID or name)
@@ -48,7 +51,7 @@ class SlackRecord
/**
* User icon e.g. 'ghost', 'http://example.com/user.png'
* @var string
* @var string|null
*/
private $userIcon;
@@ -72,12 +75,12 @@ class SlackRecord
/**
* Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
* @var array
* @var string[]
*/
private $excludeFields;
/**
* @var FormatterInterface
* @var ?FormatterInterface
*/
private $formatter;
@@ -86,26 +89,45 @@ class SlackRecord
*/
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;
/**
* @param string[] $excludeFields
*/
public function __construct(
?string $channel = null,
?string $username = null,
bool $useAttachment = true,
?string $userIcon = null,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
array $excludeFields = array(),
FormatterInterface $formatter = null
) {
$this
->setChannel($channel)
->setUsername($username)
->useAttachment($useAttachment)
->setUserIcon($userIcon)
->useShortAttachment($useShortAttachment)
->includeContextAndExtra($includeContextAndExtra)
->excludeFields($excludeFields)
->setFormatter($formatter);
if ($this->includeContextAndExtra) {
$this->normalizerFormatter = new NormalizerFormatter();
}
}
public function getSlackData(array $record)
/**
* Returns required data in format that Slack
* is expecting.
*
* @phpstan-param FormattedRecord $record
* @phpstan-return mixed[]
*/
public function getSlackData(array $record): array
{
$dataArray = array();
$record = $this->excludeFields($record);
$record = $this->removeExcludedFields($record);
if ($this->username) {
$dataArray['username'] = $this->username;
@@ -116,6 +138,7 @@ class SlackRecord
}
if ($this->formatter && !$this->useAttachment) {
/** @phpstan-ignore-next-line */
$message = $this->formatter->format($record);
} else {
$message = $record['message'];
@@ -123,12 +146,14 @@ class SlackRecord
if ($this->useAttachment) {
$attachment = array(
'fallback' => $message,
'text' => $message,
'color' => $this->getAttachmentColor($record['level']),
'fields' => array(),
'mrkdwn_in' => array('fields'),
'ts' => $record['datetime']->getTimestamp()
'fallback' => $message,
'text' => $message,
'color' => $this->getAttachmentColor($record['level']),
'fields' => array(),
'mrkdwn_in' => array('fields'),
'ts' => $record['datetime']->getTimestamp(),
'footer' => $this->username,
'footer_icon' => $this->userIcon,
);
if ($this->useShortAttachment) {
@@ -138,7 +163,6 @@ class SlackRecord
$attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']);
}
if ($this->includeContextAndExtra) {
foreach (array('extra', 'context') as $key) {
if (empty($record[$key])) {
@@ -147,7 +171,7 @@ class SlackRecord
if ($this->useShortAttachment) {
$attachment['fields'][] = $this->generateAttachmentField(
$key,
(string) $key,
$record[$key]
);
} else {
@@ -177,93 +201,157 @@ class SlackRecord
}
/**
* Returned a Slack message attachment color associated with
* Returns a Slack message attachment color associated with
* provided level.
*
* @param int $level
* @return string
*/
public function getAttachmentColor($level)
public function getAttachmentColor(int $level): string
{
switch (true) {
case $level >= Logger::ERROR:
return self::COLOR_DANGER;
return static::COLOR_DANGER;
case $level >= Logger::WARNING:
return self::COLOR_WARNING;
return static::COLOR_WARNING;
case $level >= Logger::INFO:
return self::COLOR_GOOD;
return static::COLOR_GOOD;
default:
return self::COLOR_DEFAULT;
return static::COLOR_DEFAULT;
}
}
/**
* Stringifies an array of key/value pairs to be used in attachment fields
*
* @param array $fields
*
* @return string
* @param mixed[] $fields
*/
public function stringify($fields)
public function stringify(array $fields): string
{
/** @var Record $fields */
$normalized = $this->normalizerFormatter->format($fields);
$prettyPrintFlag = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 128;
$flags = 0;
if (PHP_VERSION_ID >= 50400) {
$flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
}
$hasSecondDimension = count(array_filter($normalized, 'is_array'));
$hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric'));
return $hasSecondDimension || $hasNonNumericKeys
? Utils::jsonEncode($normalized, $prettyPrintFlag | $flags)
: Utils::jsonEncode($normalized, $flags);
? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|Utils::DEFAULT_JSON_FLAGS)
: Utils::jsonEncode($normalized, Utils::DEFAULT_JSON_FLAGS);
}
/**
* Sets the formatter
* Channel used by the bot when posting
*
* @param FormatterInterface $formatter
* @param ?string $channel
*
* @return static
*/
public function setFormatter(FormatterInterface $formatter)
public function setChannel(?string $channel = null): self
{
$this->channel = $channel;
return $this;
}
/**
* Username used by the bot when posting
*
* @param ?string $username
*
* @return static
*/
public function setUsername(?string $username = null): self
{
$this->username = $username;
return $this;
}
public function useAttachment(bool $useAttachment = true): self
{
$this->useAttachment = $useAttachment;
return $this;
}
public function setUserIcon(?string $userIcon = null): self
{
$this->userIcon = $userIcon;
if (\is_string($userIcon)) {
$this->userIcon = trim($userIcon, ':');
}
return $this;
}
public function useShortAttachment(bool $useShortAttachment = false): self
{
$this->useShortAttachment = $useShortAttachment;
return $this;
}
public function includeContextAndExtra(bool $includeContextAndExtra = false): self
{
$this->includeContextAndExtra = $includeContextAndExtra;
if ($this->includeContextAndExtra) {
$this->normalizerFormatter = new NormalizerFormatter();
}
return $this;
}
/**
* @param string[] $excludeFields
*/
public function excludeFields(array $excludeFields = []): self
{
$this->excludeFields = $excludeFields;
return $this;
}
public function setFormatter(?FormatterInterface $formatter = null): self
{
$this->formatter = $formatter;
return $this;
}
/**
* Generates attachment field
*
* @param string $title
* @param string|array $value
* @param string|mixed[] $value
*
* @return array
* @return array{title: string, value: string, short: false}
*/
private function generateAttachmentField($title, $value)
private function generateAttachmentField(string $title, $value): array
{
$value = is_array($value)
? sprintf('```%s```', $this->stringify($value))
? sprintf('```%s```', substr($this->stringify($value), 0, 1990))
: $value;
return array(
'title' => ucfirst($title),
'value' => $value,
'short' => false
'short' => false,
);
}
/**
* Generates a collection of attachment fields from array
*
* @param array $data
* @param mixed[] $data
*
* @return array
* @return array<array{title: string, value: string, short: false}>
*/
private function generateAttachmentFields(array $data)
private function generateAttachmentFields(array $data): array
{
/** @var Record $data */
$normalized = $this->normalizerFormatter->format($data);
$fields = array();
foreach ($this->normalizerFormatter->format($data) as $key => $value) {
$fields[] = $this->generateAttachmentField($key, $value);
foreach ($normalized as $key => $value) {
$fields[] = $this->generateAttachmentField((string) $key, $value);
}
return $fields;
@@ -272,11 +360,11 @@ class SlackRecord
/**
* Get a copy of record with fields excluded according to $this->excludeFields
*
* @param array $record
* @phpstan-param FormattedRecord $record
*
* @return array
* @return mixed[]
*/
private function excludeFields(array $record)
private function removeExcludedFields(array $record): array
{
foreach ($this->excludeFields as $field) {
$keys = explode('.', $field);

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -21,6 +21,8 @@ use Monolog\Handler\Slack\SlackRecord;
*
* @author Greg Kedzierski <greg@gregkedzierski.com>
* @see https://api.slack.com/
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class SlackHandler extends SocketHandler
{
@@ -42,20 +44,42 @@ class SlackHandler extends SocketHandler
* @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 $useShortAttachment Whether 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']
* @param string[] $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())
{
public function __construct(
string $token,
string $channel,
?string $username = null,
bool $useAttachment = true,
?string $iconEmoji = null,
$level = Logger::CRITICAL,
bool $bubble = true,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
array $excludeFields = array(),
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
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);
parent::__construct(
'ssl://slack.com:443',
$level,
$bubble,
$persistent,
$timeout,
$writingTimeout,
$connectionTimeout,
$chunkSize
);
$this->slackRecord = new SlackRecord(
$channel,
@@ -64,30 +88,26 @@ class SlackHandler extends SocketHandler
$iconEmoji,
$useShortAttachment,
$includeContextAndExtra,
$excludeFields,
$this->formatter
$excludeFields
);
$this->token = $token;
}
public function getSlackRecord()
public function getSlackRecord(): SlackRecord
{
return $this->slackRecord;
}
public function getToken()
public function getToken(): string
{
return $this->token;
}
/**
* {@inheritdoc}
*
* @param array $record
* @return string
* {@inheritDoc}
*/
protected function generateDataStream($record)
protected function generateDataStream(array $record): string
{
$content = $this->buildContent($record);
@@ -97,10 +117,9 @@ class SlackHandler extends SocketHandler
/**
* Builds the body of API call
*
* @param array $record
* @return string
* @phpstan-param FormattedRecord $record
*/
private function buildContent($record)
private function buildContent(array $record): string
{
$dataArray = $this->prepareContentData($record);
@@ -108,12 +127,10 @@ class SlackHandler extends SocketHandler
}
/**
* Prepares content data
*
* @param array $record
* @return array
* @phpstan-param FormattedRecord $record
* @return string[]
*/
protected function prepareContentData($record)
protected function prepareContentData(array $record): array
{
$dataArray = $this->slackRecord->getSlackData($record);
$dataArray['token'] = $this->token;
@@ -127,11 +144,8 @@ class SlackHandler extends SocketHandler
/**
* Builds the header of the API Call
*
* @param string $content
* @return string
*/
private function buildHeader($content)
private function buildHeader(string $content): string
{
$header = "POST /api/chat.postMessage HTTP/1.1\r\n";
$header .= "Host: slack.com\r\n";
@@ -143,11 +157,9 @@ class SlackHandler extends SocketHandler
}
/**
* {@inheritdoc}
*
* @param array $record
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
parent::write($record);
$this->finalizeWrite();
@@ -159,7 +171,7 @@ class SlackHandler extends SocketHandler
* If we do not read some but close the socket too early, slack sometimes
* drops the request entirely.
*/
protected function finalizeWrite()
protected function finalizeWrite(): void
{
$res = $this->getResource();
if (is_resource($res)) {
@@ -168,42 +180,7 @@ class SlackHandler extends SocketHandler
$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)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
parent::setFormatter($formatter);
$this->slackRecord->setFormatter($formatter);
@@ -211,11 +188,69 @@ class SlackHandler extends SocketHandler
return $this;
}
public function getFormatter()
public function getFormatter(): FormatterInterface
{
$formatter = parent::getFormatter();
$this->slackRecord->setFormatter($formatter);
return $formatter;
}
/**
* Channel used by the bot when posting
*/
public function setChannel(string $channel): self
{
$this->slackRecord->setChannel($channel);
return $this;
}
/**
* Username used by the bot when posting
*/
public function setUsername(string $username): self
{
$this->slackRecord->setUsername($username);
return $this;
}
public function useAttachment(bool $useAttachment): self
{
$this->slackRecord->useAttachment($useAttachment);
return $this;
}
public function setIconEmoji(string $iconEmoji): self
{
$this->slackRecord->setUserIcon($iconEmoji);
return $this;
}
public function useShortAttachment(bool $useShortAttachment): self
{
$this->slackRecord->useShortAttachment($useShortAttachment);
return $this;
}
public function includeContextAndExtra(bool $includeContextAndExtra): self
{
$this->slackRecord->includeContextAndExtra($includeContextAndExtra);
return $this;
}
/**
* @param string[] $excludeFields
*/
public function excludeFields(array $excludeFields): self
{
$this->slackRecord->excludeFields($excludeFields);
return $this;
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -37,19 +37,31 @@ class SlackWebhookHandler extends AbstractProcessingHandler
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']
* @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 string[] $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())
{
public function __construct(
string $webhookUrl,
?string $channel = null,
?string $username = null,
bool $useAttachment = true,
?string $iconEmoji = null,
bool $useShortAttachment = false,
bool $includeContextAndExtra = false,
$level = Logger::CRITICAL,
bool $bubble = true,
array $excludeFields = array()
) {
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the SlackWebhookHandler');
}
parent::__construct($level, $bubble);
$this->webhookUrl = $webhookUrl;
@@ -61,27 +73,24 @@ class SlackWebhookHandler extends AbstractProcessingHandler
$iconEmoji,
$useShortAttachment,
$includeContextAndExtra,
$excludeFields,
$this->formatter
$excludeFields
);
}
public function getSlackRecord()
public function getSlackRecord(): SlackRecord
{
return $this->slackRecord;
}
public function getWebhookUrl()
public function getWebhookUrl(): string
{
return $this->webhookUrl;
}
/**
* {@inheritdoc}
*
* @param array $record
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
$postData = $this->slackRecord->getSlackData($record);
$postString = Utils::jsonEncode($postData);
@@ -92,7 +101,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array('Content-type: application/json'),
CURLOPT_POSTFIELDS => $postString
CURLOPT_POSTFIELDS => $postString,
);
if (defined('CURLOPT_SAFE_UPLOAD')) {
$options[CURLOPT_SAFE_UPLOAD] = true;
@@ -103,7 +112,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler
Curl\Util::execute($ch);
}
public function setFormatter(FormatterInterface $formatter)
public function setFormatter(FormatterInterface $formatter): HandlerInterface
{
parent::setFormatter($formatter);
$this->slackRecord->setFormatter($formatter);
@@ -111,7 +120,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler
return $this;
}
public function getFormatter()
public function getFormatter(): FormatterInterface
{
$formatter = parent::getFormatter();
$this->slackRecord->setFormatter($formatter);

View File

@@ -1,84 +0,0 @@
<?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
* @deprecated According to Slack the API used on this handler it is deprecated.
* Therefore this handler will be removed on 2.x
* Slack suggests to use webhooks instead. Please contact slack for more information.
*/
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)
{
@trigger_error('SlackbotHandler is deprecated and will be removed on 2.x', E_USER_DEPRECATED);
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);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -18,42 +18,81 @@ use Monolog\Logger;
*
* @author Pablo de Leon Belloc <pablolb@gmail.com>
* @see http://php.net/manual/en/function.fsockopen.php
*
* @phpstan-import-type Record from \Monolog\Logger
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class SocketHandler extends AbstractProcessingHandler
{
/** @var string */
private $connectionString;
/** @var float */
private $connectionTimeout;
/** @var resource|null */
private $resource;
private $timeout = 0;
private $writingTimeout = 10;
/** @var float */
private $timeout;
/** @var float */
private $writingTimeout;
/** @var ?int */
private $lastSentBytes = null;
private $chunkSize = null;
private $persistent = false;
private $errno;
private $errstr;
private $lastWritingAt;
/** @var ?int */
private $chunkSize;
/** @var bool */
private $persistent;
/** @var ?int */
private $errno = null;
/** @var ?string */
private $errstr = null;
/** @var ?float */
private $lastWritingAt = null;
/**
* @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
* @param string $connectionString Socket connection string
* @param bool $persistent Flag to enable/disable persistent connections
* @param float $timeout Socket timeout to wait until the request is being aborted
* @param float $writingTimeout Socket timeout to wait until the request should've been sent/written
* @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been
* established
* @param int|null $chunkSize Sets the chunk size. Only has effect during connection in the writing cycle
*
* @throws \InvalidArgumentException If an invalid timeout value (less than 0) is passed.
*/
public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true)
{
public function __construct(
string $connectionString,
$level = Logger::DEBUG,
bool $bubble = true,
bool $persistent = false,
float $timeout = 0.0,
float $writingTimeout = 10.0,
?float $connectionTimeout = null,
?int $chunkSize = null
) {
parent::__construct($level, $bubble);
$this->connectionString = $connectionString;
$this->connectionTimeout = (float) ini_get('default_socket_timeout');
if ($connectionTimeout !== null) {
$this->validateTimeout($connectionTimeout);
}
$this->connectionTimeout = $connectionTimeout ?? (float) ini_get('default_socket_timeout');
$this->persistent = $persistent;
$this->validateTimeout($timeout);
$this->timeout = $timeout;
$this->validateTimeout($writingTimeout);
$this->writingTimeout = $writingTimeout;
$this->chunkSize = $chunkSize;
}
/**
* Connect (if necessary) and write to the socket
*
* @param array $record
* {@inheritDoc}
*
* @throws \UnexpectedValueException
* @throws \RuntimeException
*/
protected function write(array $record)
protected function write(array $record): void
{
$this->connectIfNotConnected();
$data = $this->generateDataStream($record);
@@ -63,7 +102,7 @@ class SocketHandler extends AbstractProcessingHandler
/**
* We will not close a PersistentSocket instance so it can be reused in other requests.
*/
public function close()
public function close(): void
{
if (!$this->isPersistent()) {
$this->closeSocket();
@@ -73,7 +112,7 @@ class SocketHandler extends AbstractProcessingHandler
/**
* Close socket, if open
*/
public function closeSocket()
public function closeSocket(): void
{
if (is_resource($this->resource)) {
fclose($this->resource);
@@ -82,39 +121,39 @@ class SocketHandler extends AbstractProcessingHandler
}
/**
* Set socket connection to nbe persistent. It only has effect before the connection is initiated.
*
* @param bool $persistent
* Set socket connection to be persistent. It only has effect before the connection is initiated.
*/
public function setPersistent($persistent)
public function setPersistent(bool $persistent): self
{
$this->persistent = (bool) $persistent;
$this->persistent = $persistent;
return $this;
}
/**
* 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)
public function setConnectionTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->connectionTimeout = (float) $seconds;
$this->connectionTimeout = $seconds;
return $this;
}
/**
* 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)
public function setTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->timeout = (float) $seconds;
$this->timeout = $seconds;
return $this;
}
/**
@@ -122,58 +161,52 @@ class SocketHandler extends AbstractProcessingHandler
*
* @param float $seconds 0 for no timeout
*/
public function setWritingTimeout($seconds)
public function setWritingTimeout(float $seconds): self
{
$this->validateTimeout($seconds);
$this->writingTimeout = (float) $seconds;
$this->writingTimeout = $seconds;
return $this;
}
/**
* Set chunk size. Only has effect during connection in the writing cycle.
*
* @param float $bytes
*/
public function setChunkSize($bytes)
public function setChunkSize(int $bytes): self
{
$this->chunkSize = $bytes;
return $this;
}
/**
* Get current connection string
*
* @return string
*/
public function getConnectionString()
public function getConnectionString(): string
{
return $this->connectionString;
}
/**
* Get persistent setting
*
* @return bool
*/
public function isPersistent()
public function isPersistent(): bool
{
return $this->persistent;
}
/**
* Get current connection timeout setting
*
* @return float
*/
public function getConnectionTimeout()
public function getConnectionTimeout(): float
{
return $this->connectionTimeout;
}
/**
* Get current in-transfer timeout
*
* @return float
*/
public function getTimeout()
public function getTimeout(): float
{
return $this->timeout;
}
@@ -183,17 +216,15 @@ class SocketHandler extends AbstractProcessingHandler
*
* @return float
*/
public function getWritingTimeout()
public function getWritingTimeout(): float
{
return $this->writingTimeout;
}
/**
* Get current chunk size
*
* @return float
*/
public function getChunkSize()
public function getChunkSize(): ?int
{
return $this->chunkSize;
}
@@ -202,10 +233,8 @@ class SocketHandler extends AbstractProcessingHandler
* 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()
public function isConnected(): bool
{
return is_resource($this->resource)
&& !feof($this->resource); // on TCP - other party can close connection.
@@ -213,6 +242,8 @@ class SocketHandler extends AbstractProcessingHandler
/**
* Wrapper to allow mocking
*
* @return resource|false
*/
protected function pfsockopen()
{
@@ -221,6 +252,8 @@ class SocketHandler extends AbstractProcessingHandler
/**
* Wrapper to allow mocking
*
* @return resource|false
*/
protected function fsockopen()
{
@@ -231,50 +264,77 @@ class SocketHandler extends AbstractProcessingHandler
* Wrapper to allow mocking
*
* @see http://php.net/manual/en/function.stream-set-timeout.php
*
* @return bool
*/
protected function streamSetTimeout()
{
$seconds = floor($this->timeout);
$microseconds = round(($this->timeout - $seconds) * 1e6);
return stream_set_timeout($this->resource, $seconds, $microseconds);
if (!is_resource($this->resource)) {
throw new \LogicException('streamSetTimeout called but $this->resource is not a resource');
}
return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds);
}
/**
* Wrapper to allow mocking
*
* @see http://php.net/manual/en/function.stream-set-chunk-size.php
*
* @return int|bool
*/
protected function streamSetChunkSize()
{
if (!is_resource($this->resource)) {
throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource');
}
if (null === $this->chunkSize) {
throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set');
}
return stream_set_chunk_size($this->resource, $this->chunkSize);
}
/**
* Wrapper to allow mocking
*
* @return int|bool
*/
protected function fwrite($data)
protected function fwrite(string $data)
{
if (!is_resource($this->resource)) {
throw new \LogicException('fwrite called but $this->resource is not a resource');
}
return @fwrite($this->resource, $data);
}
/**
* Wrapper to allow mocking
*
* @return mixed[]|bool
*/
protected function streamGetMetadata()
{
if (!is_resource($this->resource)) {
throw new \LogicException('streamGetMetadata called but $this->resource is not a resource');
}
return stream_get_meta_data($this->resource);
}
private function validateTimeout($value)
private function validateTimeout(float $value): void
{
$ok = filter_var($value, FILTER_VALIDATE_FLOAT);
if ($ok === false || $value < 0) {
if ($value < 0) {
throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
}
}
private function connectIfNotConnected()
private function connectIfNotConnected(): void
{
if ($this->isConnected()) {
return;
@@ -282,7 +342,10 @@ class SocketHandler extends AbstractProcessingHandler
$this->connect();
}
protected function generateDataStream($record)
/**
* @phpstan-param FormattedRecord $record
*/
protected function generateDataStream(array $record): string
{
return (string) $record['formatted'];
}
@@ -295,41 +358,41 @@ class SocketHandler extends AbstractProcessingHandler
return $this->resource;
}
private function connect()
private function connect(): void
{
$this->createSocketResource();
$this->setSocketTimeout();
$this->setStreamChunkSize();
}
private function createSocketResource()
private function createSocketResource(): void
{
if ($this->isPersistent()) {
$resource = $this->pfsockopen();
} else {
$resource = $this->fsockopen();
}
if (!$resource) {
if (is_bool($resource)) {
throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
}
$this->resource = $resource;
}
private function setSocketTimeout()
private function setSocketTimeout(): void
{
if (!$this->streamSetTimeout()) {
throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
}
}
private function setStreamChunkSize()
private function setStreamChunkSize(): void
{
if ($this->chunkSize && !$this->streamSetChunkSize()) {
throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()");
}
}
private function writeToSocket($data)
private function writeToSocket(string $data): void
{
$length = strlen($data);
$sent = 0;
@@ -345,7 +408,7 @@ class SocketHandler extends AbstractProcessingHandler
}
$sent += $chunk;
$socketInfo = $this->streamGetMetadata();
if ($socketInfo['timed_out']) {
if (is_array($socketInfo) && $socketInfo['timed_out']) {
throw new \RuntimeException("Write timed-out");
}
@@ -358,15 +421,15 @@ class SocketHandler extends AbstractProcessingHandler
}
}
private function writingIsTimedOut($sent)
private function writingIsTimedOut(int $sent): bool
{
$writingTimeout = (int) floor($this->writingTimeout);
if (0 === $writingTimeout) {
// convert to ms
if (0.0 == $this->writingTimeout) {
return false;
}
if ($sent !== $this->lastSentBytes) {
$this->lastWritingAt = time();
$this->lastWritingAt = microtime(true);
$this->lastSentBytes = $sent;
return false;
@@ -374,7 +437,7 @@ class SocketHandler extends AbstractProcessingHandler
usleep(100);
}
if ((time() - $this->lastWritingAt) >= $writingTimeout) {
if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) {
$this->closeSocket();
return true;

View File

@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
/*
* 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\Sqs\SqsClient;
use Monolog\Logger;
use Monolog\Utils;
/**
* Writes to any sqs queue.
*
* @author Martijn van Calker <git@amvc.nl>
*/
class SqsHandler extends AbstractProcessingHandler
{
/** 256 KB in bytes - maximum message size in SQS */
protected const MAX_MESSAGE_SIZE = 262144;
/** 100 KB in bytes - head message size for new error log */
protected const HEAD_MESSAGE_SIZE = 102400;
/** @var SqsClient */
private $client;
/** @var string */
private $queueUrl;
public function __construct(SqsClient $sqsClient, string $queueUrl, $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->client = $sqsClient;
$this->queueUrl = $queueUrl;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
if (!isset($record['formatted']) || 'string' !== gettype($record['formatted'])) {
throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string' . Utils::getRecordMessageForException($record));
}
$messageBody = $record['formatted'];
if (strlen($messageBody) >= static::MAX_MESSAGE_SIZE) {
$messageBody = Utils::substr($messageBody, 0, static::HEAD_MESSAGE_SIZE);
}
$this->client->sendMessage([
'QueueUrl' => $this->queueUrl,
'MessageBody' => $messageBody,
]);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -20,36 +20,58 @@ use Monolog\Utils;
* Can be used to store into php://stderr, remote and local files, etc.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type FormattedRecord from AbstractProcessingHandler
*/
class StreamHandler extends AbstractProcessingHandler
{
/** @private 512KB */
const CHUNK_SIZE = 524288;
/** @const int */
protected const MAX_CHUNK_SIZE = 2147483647;
/** @const int 10MB */
protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
/** @var int */
protected $streamChunkSize;
/** @var resource|null */
protected $stream;
protected $url;
private $errorMessage;
/** @var ?string */
protected $url = null;
/** @var ?string */
private $errorMessage = null;
/** @var ?int */
protected $filePermission;
/** @var bool */
protected $useLocking;
private $dirCreated;
/** @var true|null */
private $dirCreated = null;
/**
* @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 resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write
* @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)
public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
{
parent::__construct($level, $bubble);
if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
if ($phpMemoryLimit > 0) {
// use max 10% of allowed memory for the chunk size, and at least 100KB
$this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024));
} else {
// memory is unlimited, set to the default 10MB
$this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
}
} else {
// no memory limit information, set to the default 10MB
$this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
}
if (is_resource($stream)) {
$this->stream = $stream;
$this->streamSetChunkSize();
stream_set_chunk_size($this->stream, $this->streamChunkSize);
} elseif (is_string($stream)) {
$this->url = Utils::canonicalizePath($stream);
} else {
@@ -61,9 +83,9 @@ class StreamHandler extends AbstractProcessingHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
if ($this->url && is_resource($this->stream)) {
fclose($this->stream);
@@ -87,78 +109,83 @@ class StreamHandler extends AbstractProcessingHandler
*
* @return string|null
*/
public function getUrl()
public function getUrl(): ?string
{
return $this->url;
}
/**
* {@inheritdoc}
* @return int
*/
protected function write(array $record)
public function getStreamChunkSize(): int
{
return $this->streamChunkSize;
}
/**
* {@inheritDoc}
*/
protected function write(array $record): void
{
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().');
$url = $this->url;
if (null === $url || '' === $url) {
throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' . Utils::getRecordMessageForException($record));
}
$this->createDir();
$this->createDir($url);
$this->errorMessage = null;
set_error_handler(array($this, 'customErrorHandler'));
$this->stream = fopen($this->url, 'a');
set_error_handler([$this, 'customErrorHandler']);
$stream = fopen($url, 'a');
if ($this->filePermission !== null) {
@chmod($this->url, $this->filePermission);
@chmod($url, $this->filePermission);
}
restore_error_handler();
if (!is_resource($this->stream)) {
if (!is_resource($stream)) {
$this->stream = null;
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $this->url));
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record));
}
$this->streamSetChunkSize();
stream_set_chunk_size($stream, $this->streamChunkSize);
$this->stream = $stream;
}
$stream = $this->stream;
if (!is_resource($stream)) {
throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record));
}
if ($this->useLocking) {
// ignoring errors here, there's not much we can do about them
flock($this->stream, LOCK_EX);
flock($stream, LOCK_EX);
}
$this->streamWrite($this->stream, $record);
$this->streamWrite($stream, $record);
if ($this->useLocking) {
flock($this->stream, LOCK_UN);
flock($stream, LOCK_UN);
}
}
/**
* Write to stream
* @param resource $stream
* @param array $record
* @param array $record
*
* @phpstan-param FormattedRecord $record
*/
protected function streamWrite($stream, array $record)
protected function streamWrite($stream, array $record): void
{
fwrite($stream, (string) $record['formatted']);
}
protected function streamSetChunkSize()
{
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
return stream_set_chunk_size($this->stream, self::CHUNK_SIZE);
}
return false;
}
private function customErrorHandler($code, $msg)
private function customErrorHandler(int $code, string $msg): bool
{
$this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
return true;
}
/**
* @param string $stream
*
* @return null|string
*/
private function getDirFromStream($stream)
private function getDirFromStream(string $stream): ?string
{
$pos = strpos($stream, '://');
if ($pos === false) {
@@ -172,21 +199,21 @@ class StreamHandler extends AbstractProcessingHandler
return null;
}
private function createDir()
private function createDir(string $url): void
{
// Do not try to create dir if it has already been tried.
if ($this->dirCreated) {
return;
}
$dir = $this->getDirFromStream($this->url);
$dir = $this->getDirFromStream($url);
if (null !== $dir && !is_dir($dir)) {
$this->errorMessage = null;
set_error_handler(array($this, 'customErrorHandler'));
set_error_handler([$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));
if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) {
throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir));
}
}
$this->dirCreated = true;

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,38 +12,47 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Swift_Message;
use Swift;
/**
* SwiftMailerHandler uses Swift_Mailer to send the emails
*
* @author Gyula Sallai
*
* @phpstan-import-type Record from \Monolog\Logger
* @deprecated Since Monolog 2.6. Use SymfonyMailerHandler instead.
*/
class SwiftMailerHandler extends MailHandler
{
/** @var \Swift_Mailer */
protected $mailer;
/** @var Swift_Message|callable(string, Record[]): Swift_Message */
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
* @psalm-param Swift_Message|callable(string, Record[]): Swift_Message $message
*
* @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
*/
public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, $bubble = true)
public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
@trigger_error('The SwiftMailerHandler is deprecated since Monolog 2.6. Use SymfonyMailerHandler instead.', E_USER_DEPRECATED);
$this->mailer = $mailer;
$this->messageTemplate = $message;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function send($content, array $records)
protected function send(string $content, array $records): void
{
$this->mailer->send($this->buildMessage($content, $records));
}
@@ -51,10 +60,9 @@ class SwiftMailerHandler extends MailHandler
/**
* Gets the formatter for the Swift_Message subject.
*
* @param string $format The format of the subject
* @return FormatterInterface
* @param string|null $format The format of the subject
*/
protected function getSubjectFormatter($format)
protected function getSubjectFormatter(?string $format): FormatterInterface
{
return new LineFormatter($format);
}
@@ -62,22 +70,25 @@ class SwiftMailerHandler extends MailHandler
/**
* 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
* @param string $content formatted email body to be sent
* @param array $records Log records that formed the content
* @return Swift_Message
*
* @phpstan-param Record[] $records
*/
protected function buildMessage($content, array $records)
protected function buildMessage(string $content, array $records): Swift_Message
{
$message = null;
if ($this->messageTemplate instanceof \Swift_Message) {
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);
$message = ($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 (!$message instanceof Swift_Message) {
$record = reset($records);
throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : ''));
}
if ($records) {
@@ -85,27 +96,20 @@ class SwiftMailerHandler extends MailHandler
$message->setSubject($subjectFormatter->format($this->getHighestRecord($records)));
}
$message->setBody($content);
$mime = 'text/plain';
if ($this->isHtmlBody($content)) {
$mime = 'text/html';
}
$message->setBody($content, $mime);
/** @phpstan-ignore-next-line */
if (version_compare(Swift::VERSION, '6.0.0', '>=')) {
$message->setDate(new \DateTimeImmutable());
} else {
/** @phpstan-ignore-next-line */
$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);
}
}

View File

@@ -0,0 +1,111 @@
<?php declare(strict_types=1);
/*
* 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\Utils;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Email;
/**
* SymfonyMailerHandler uses Symfony's Mailer component to send the emails
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class SymfonyMailerHandler extends MailHandler
{
/** @var MailerInterface|TransportInterface */
protected $mailer;
/** @var Email|callable(string, Record[]): Email */
private $emailTemplate;
/**
* @psalm-param Email|callable(string, Record[]): Email $email
*
* @param MailerInterface|TransportInterface $mailer The mailer to use
* @param callable|Email $email An email template, the subject/body will be replaced
*/
public function __construct($mailer, $email, $level = Logger::ERROR, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->mailer = $mailer;
$this->emailTemplate = $email;
}
/**
* {@inheritDoc}
*/
protected function send(string $content, array $records): void
{
$this->mailer->send($this->buildMessage($content, $records));
}
/**
* Gets the formatter for the Swift_Message subject.
*
* @param string|null $format The format of the subject
*/
protected function getSubjectFormatter(?string $format): FormatterInterface
{
return new LineFormatter($format);
}
/**
* Creates instance of Email to be sent
*
* @param string $content formatted email body to be sent
* @param array $records Log records that formed the content
*
* @phpstan-param Record[] $records
*/
protected function buildMessage(string $content, array $records): Email
{
$message = null;
if ($this->emailTemplate instanceof Email) {
$message = clone $this->emailTemplate;
} elseif (is_callable($this->emailTemplate)) {
$message = ($this->emailTemplate)($content, $records);
}
if (!$message instanceof Email) {
$record = reset($records);
throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : ''));
}
if ($records) {
$subjectFormatter = $this->getSubjectFormatter($message->getSubject());
$message->subject($subjectFormatter->format($this->getHighestRecord($records)));
}
if ($this->isHtmlBody($content)) {
if (null !== ($charset = $message->getHtmlCharset())) {
$message->html($content, $charset);
} else {
$message->html($content);
}
} else {
if (null !== ($charset = $message->getTextCharset())) {
$message->text($content, $charset);
} else {
$message->text($content);
}
}
return $message->date(new \DateTimeImmutable());
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -12,6 +12,7 @@
namespace Monolog\Handler;
use Monolog\Logger;
use Monolog\Utils;
/**
* Logs to syslog service.
@@ -28,17 +29,17 @@ use Monolog\Logger;
*/
class SyslogHandler extends AbstractSyslogHandler
{
/** @var string */
protected $ident;
/** @var int */
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
* @param string $ident
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
* @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)
public function __construct(string $ident, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, int $logopts = LOG_PID)
{
parent::__construct($facility, $level, $bubble);
@@ -47,20 +48,20 @@ class SyslogHandler extends AbstractSyslogHandler
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
public function close(): void
{
closelog();
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
protected function write(array $record)
protected function write(array $record): void
{
if (!openlog($this->ident, $this->logopts, $this->facility)) {
throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"');
throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"' . Utils::getRecordMessageForException($record));
}
syslog($this->logLevels[$record['level']], (string) $record['formatted']);
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,46 +11,78 @@
namespace Monolog\Handler\SyslogUdp;
use Monolog\Utils;
use Socket;
class UdpSocket
{
const DATAGRAM_MAX_LENGTH = 65023;
protected const DATAGRAM_MAX_LENGTH = 65023;
/** @var string */
protected $ip;
/** @var int */
protected $port;
protected $socket;
/** @var resource|Socket|null */
protected $socket = null;
public function __construct($ip, $port = 514)
public function __construct(string $ip, int $port = 514)
{
$this->ip = $ip;
$this->port = $port;
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
}
/**
* @param string $line
* @param string $header
* @return void
*/
public function write($line, $header = "")
{
$this->send($this->assembleMessage($line, $header));
}
public function close()
public function close(): void
{
if (is_resource($this->socket)) {
if (is_resource($this->socket) || $this->socket instanceof Socket) {
socket_close($this->socket);
$this->socket = null;
}
}
protected function send($chunk)
/**
* @return resource|Socket
*/
protected function getSocket()
{
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');
if (null !== $this->socket) {
return $this->socket;
}
socket_sendto($this->socket, $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port);
$domain = AF_INET;
$protocol = SOL_UDP;
// Check if we are using unix sockets.
if ($this->port === 0) {
$domain = AF_UNIX;
$protocol = IPPROTO_IP;
}
$this->socket = socket_create($domain, SOCK_DGRAM, $protocol) ?: null;
if (null === $this->socket) {
throw new \RuntimeException('The UdpSocket to '.$this->ip.':'.$this->port.' could not be opened via socket_create');
}
return $this->socket;
}
protected function assembleMessage($line, $header)
protected function send(string $chunk): void
{
$chunkSize = self::DATAGRAM_MAX_LENGTH - strlen($header);
socket_sendto($this->getSocket(), $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port);
}
return $header . substr($line, 0, $chunkSize);
protected function assembleMessage(string $line, string $header): string
{
$chunkSize = static::DATAGRAM_MAX_LENGTH - strlen($header);
return $header . Utils::substr($line, 0, $chunkSize);
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
@@ -11,8 +11,10 @@
namespace Monolog\Handler;
use DateTimeInterface;
use Monolog\Logger;
use Monolog\Handler\SyslogUdp\UdpSocket;
use Monolog\Utils;
/**
* A Handler for logging to a remote syslogd server.
@@ -24,64 +26,86 @@ class SyslogUdpHandler extends AbstractSyslogHandler
{
const RFC3164 = 0;
const RFC5424 = 1;
const RFC5424e = 2;
/** @var array<self::RFC*, string> */
private $dateFormats = array(
self::RFC3164 => 'M d H:i:s',
self::RFC5424 => \DateTime::RFC3339,
self::RFC5424e => \DateTime::RFC3339_EXTENDED,
);
/** @var UdpSocket */
protected $socket;
/** @var string */
protected $ident;
/** @var self::RFC* */
protected $rfc;
/**
* @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.
* @param int $rfc RFC to format the message for.
* @param string $host Either IP/hostname or a path to a unix socket (port must be 0 then)
* @param int $port Port number, or 0 if $host is a unix socket
* @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant
* @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.
* @param int $rfc RFC to format the message for.
* @throws MissingExtensionException
*
* @phpstan-param self::RFC* $rfc
*/
public function __construct($host, $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $ident = 'php', $rfc = self::RFC5424)
public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424)
{
if (!extension_loaded('sockets')) {
throw new MissingExtensionException('The sockets extension is required to use the SyslogUdpHandler');
}
parent::__construct($facility, $level, $bubble);
$this->ident = $ident;
$this->rfc = $rfc;
$this->socket = new UdpSocket($host, $port ?: 514);
$this->socket = new UdpSocket($host, $port);
}
protected function write(array $record)
protected function write(array $record): void
{
$lines = $this->splitMessageIntoLines($record['formatted']);
$header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']]);
$header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']], $record['datetime']);
foreach ($lines as $line) {
$this->socket->write($line, $header);
}
}
public function close()
public function close(): void
{
$this->socket->close();
}
private function splitMessageIntoLines($message)
/**
* @param string|string[] $message
* @return string[]
*/
private function splitMessageIntoLines($message): array
{
if (is_array($message)) {
$message = implode("\n", $message);
}
return preg_split('/$\R?^/m', $message, -1, PREG_SPLIT_NO_EMPTY);
$lines = preg_split('/$\R?^/m', (string) $message, -1, PREG_SPLIT_NO_EMPTY);
if (false === $lines) {
$pcreErrorCode = preg_last_error();
throw new \RuntimeException('Could not preg_split: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
}
return $lines;
}
/**
* Make common syslog header (see rfc5424 or rfc3164)
*/
protected function makeCommonSyslogHeader($severity)
protected function makeCommonSyslogHeader(int $severity, DateTimeInterface $datetime): string
{
$priority = $severity + $this->facility;
@@ -93,32 +117,34 @@ class SyslogUdpHandler extends AbstractSyslogHandler
$hostname = '-';
}
$date = $this->getDateTime();
if ($this->rfc === self::RFC3164) {
// see https://github.com/phpstan/phpstan/issues/5348
// @phpstan-ignore-next-line
$dateNew = $datetime->setTimezone(new \DateTimeZone('UTC'));
$date = $dateNew->format($this->dateFormats[$this->rfc]);
return "<$priority>" .
$date . " " .
$hostname . " " .
$this->ident . "[" . $pid . "]: ";
} else {
return "<$priority>1 " .
$date . " " .
$hostname . " " .
$this->ident . " " .
$pid . " - - ";
}
}
protected function getDateTime()
{
return date($this->dateFormats[$this->rfc]);
$date = $datetime->format($this->dateFormats[$this->rfc]);
return "<$priority>1 " .
$date . " " .
$hostname . " " .
$this->ident . " " .
$pid . " - - ";
}
/**
* Inject your own socket, mainly used for testing
*/
public function setSocket($socket)
public function setSocket(UdpSocket $socket): self
{
$this->socket = $socket;
return $this;
}
}

View File

@@ -0,0 +1,274 @@
<?php declare(strict_types=1);
/*
* 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 RuntimeException;
use Monolog\Logger;
use Monolog\Utils;
/**
* Handler send logs to Telegram using Telegram Bot API.
*
* How to use:
* 1) Create telegram bot with https://telegram.me/BotFather
* 2) Create a telegram channel where logs will be recorded.
* 3) Add created bot from step 1 to the created channel from step 2.
*
* Use telegram bot API key from step 1 and channel name with '@' prefix from step 2 to create instance of TelegramBotHandler
*
* @link https://core.telegram.org/bots/api
*
* @author Mazur Alexandr <alexandrmazur96@gmail.com>
*
* @phpstan-import-type Record from \Monolog\Logger
*/
class TelegramBotHandler extends AbstractProcessingHandler
{
private const BOT_API = 'https://api.telegram.org/bot';
/**
* The available values of parseMode according to the Telegram api documentation
*/
private const AVAILABLE_PARSE_MODES = [
'HTML',
'MarkdownV2',
'Markdown', // legacy mode without underline and strikethrough, use MarkdownV2 instead
];
/**
* The maximum number of characters allowed in a message according to the Telegram api documentation
*/
private const MAX_MESSAGE_LENGTH = 4096;
/**
* Telegram bot access token provided by BotFather.
* Create telegram bot with https://telegram.me/BotFather and use access token from it.
* @var string
*/
private $apiKey;
/**
* Telegram channel name.
* Since to start with '@' symbol as prefix.
* @var string
*/
private $channel;
/**
* The kind of formatting that is used for the message.
* See available options at https://core.telegram.org/bots/api#formatting-options
* or in AVAILABLE_PARSE_MODES
* @var ?string
*/
private $parseMode;
/**
* Disables link previews for links in the message.
* @var ?bool
*/
private $disableWebPagePreview;
/**
* Sends the message silently. Users will receive a notification with no sound.
* @var ?bool
*/
private $disableNotification;
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
* @var bool
*/
private $splitLongMessages;
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
* @var bool
*/
private $delayBetweenMessages;
/**
* @param string $apiKey Telegram bot access token provided by BotFather
* @param string $channel Telegram channel name
* @param bool $splitLongMessages Split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages
* @param bool $delayBetweenMessages Adds delay between sending a split message according to Telegram API
* @throws MissingExtensionException
*/
public function __construct(
string $apiKey,
string $channel,
$level = Logger::DEBUG,
bool $bubble = true,
string $parseMode = null,
bool $disableWebPagePreview = null,
bool $disableNotification = null,
bool $splitLongMessages = false,
bool $delayBetweenMessages = false
)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler');
}
parent::__construct($level, $bubble);
$this->apiKey = $apiKey;
$this->channel = $channel;
$this->setParseMode($parseMode);
$this->disableWebPagePreview($disableWebPagePreview);
$this->disableNotification($disableNotification);
$this->splitLongMessages($splitLongMessages);
$this->delayBetweenMessages($delayBetweenMessages);
}
public function setParseMode(string $parseMode = null): self
{
if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES)) {
throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.');
}
$this->parseMode = $parseMode;
return $this;
}
public function disableWebPagePreview(bool $disableWebPagePreview = null): self
{
$this->disableWebPagePreview = $disableWebPagePreview;
return $this;
}
public function disableNotification(bool $disableNotification = null): self
{
$this->disableNotification = $disableNotification;
return $this;
}
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
* @param bool $splitLongMessages
* @return $this
*/
public function splitLongMessages(bool $splitLongMessages = false): self
{
$this->splitLongMessages = $splitLongMessages;
return $this;
}
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
* @param bool $delayBetweenMessages
* @return $this
*/
public function delayBetweenMessages(bool $delayBetweenMessages = false): self
{
$this->delayBetweenMessages = $delayBetweenMessages;
return $this;
}
/**
* {@inheritDoc}
*/
public function handleBatch(array $records): void
{
/** @var Record[] $messages */
$messages = [];
foreach ($records as $record) {
if (!$this->isHandling($record)) {
continue;
}
if ($this->processors) {
/** @var Record $record */
$record = $this->processRecord($record);
}
$messages[] = $record;
}
if (!empty($messages)) {
$this->send((string)$this->getFormatter()->formatBatch($messages));
}
}
/**
* @inheritDoc
*/
protected function write(array $record): void
{
$this->send($record['formatted']);
}
/**
* Send request to @link https://api.telegram.org/bot on SendMessage action.
* @param string $message
*/
protected function send(string $message): void
{
$messages = $this->handleMessageLength($message);
foreach ($messages as $key => $msg) {
if ($this->delayBetweenMessages && $key > 0) {
sleep(1);
}
$this->sendCurl($msg);
}
}
protected function sendCurl(string $message): void
{
$ch = curl_init();
$url = self::BOT_API . $this->apiKey . '/SendMessage';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'text' => $message,
'chat_id' => $this->channel,
'parse_mode' => $this->parseMode,
'disable_web_page_preview' => $this->disableWebPagePreview,
'disable_notification' => $this->disableNotification,
]));
$result = Curl\Util::execute($ch);
if (!is_string($result)) {
throw new RuntimeException('Telegram API error. Description: No response');
}
$result = json_decode($result, true);
if ($result['ok'] === false) {
throw new RuntimeException('Telegram API error. Description: ' . $result['description']);
}
}
/**
* Handle a message that is too long: truncates or splits into several
* @param string $message
* @return string[]
*/
private function handleMessageLength(string $message): array
{
$truncatedMarker = ' (...truncated)';
if (!$this->splitLongMessages && strlen($message) > self::MAX_MESSAGE_LENGTH) {
return [Utils::substr($message, 0, self::MAX_MESSAGE_LENGTH - strlen($truncatedMarker)) . $truncatedMarker];
}
return str_split($message, self::MAX_MESSAGE_LENGTH);
}
}

Some files were not shown because too many files have changed in this diff Show More