From c00a2ee97931334a769b0c70de604b36549cb763 Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Sun, 21 Feb 2016 23:56:56 +0100 Subject: [PATCH 01/13] Configurable channel to post the message to. Configurable username to post the message as. Configurable icon_emoji to post the message with. Configurable message pretext. Color message according to the priority. Treat the logUrl configuration as optional. Detailed documentation on configuration options. --- README.md | 25 ++++++--- src/DI/SlackLoggerExtension.php | 29 +++++++--- src/SlackLogger.php | 96 ++++++++++++++++++++++++++------- 3 files changed, 119 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index dbfcf33..67ea036 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # nette-slack-logger -Log your errors directly into Slack room +Log your errors directly into a Slack room ## Installation @@ -12,15 +12,28 @@ extensions: slackLogger: greeny\NetteSlackLogger\DI\SlackLoggerExtension ``` -By default the logger is just turned off, since you probably do not want to log errors from dev environment. If you want to enable it, add following lines to config.local.neon at your production server: +By default the logger is just turned off, since you probably do not want to log errors from dev environment. If you want +to enable it, add following lines to config.local.neon at your production server: ```yaml slackLogger: enabled: true slackUrl: https://hooks.slack.com/services/XXX - logUrl: http://path/to/your/logs/directory/__FILE__ + logUrl: http://path/to/your/logs/directory/__FILE__ + channel: "#somechannel" + username: "PHP Bot" + icon: ":joystick:" + pretext: "Error at example.com" ``` -Of course replace `slackUrl` with payload URL from your incomming webhook from Slack. - -You can leave `logUrl` empty, but if you have your logs accessible through web (of course e.g. protected by HTTP auth or available only from company IPs), you can define this URL here. `__FILE__` will be replaced by filename of file with exception. +Details: +- `slackUrl` must contain your incoming webhook URL - see https://api.slack.com/incoming-webhooks. +- `logUrl`, if specified, tells the URL at which the log file will be available. The substring `__FILE__` within the URL + will be replaced with the actual log file basename. The resulting URL gets appended to the message posted to Slack. + Note the file should not be available for public as it contains sensitive information. It is your responsibility to + protect the file, e.g., by HTTP auth or restricting access by IP addresses. +- `channel`: Name or ID of channel to post to. If not specified, the message gets posted to the default channel + according to the incoming webhook specification. +- `username`: Username to use for the post. Optional. +- `icon`: Icon to use besides the post instead of the default icon. Optional. +- `pretext`: Pretext for the message. Useful for distinguishing, e.g., the site. diff --git a/src/DI/SlackLoggerExtension.php b/src/DI/SlackLoggerExtension.php index a2f910e..2fedf7b 100644 --- a/src/DI/SlackLoggerExtension.php +++ b/src/DI/SlackLoggerExtension.php @@ -1,6 +1,7 @@ FALSE, + 'logUrl' => NULL, + 'channel' => NULL, + 'username' => NULL, + 'icon' => NULL, + 'pretext' => NULL, ]; @@ -29,12 +35,23 @@ public function afterCompile(ClassType $class) if ($config['enabled']) { Validators::assertField($config, 'slackUrl', 'string'); - Validators::assertField($config, 'logUrl', 'string'); + Validators::assertField($config, 'logUrl', 'string|null'); + Validators::assertField($config, 'channel', 'string|null'); + Validators::assertField($config, 'username', 'string|null'); + Validators::assertField($config, 'icon', 'string|null'); + Validators::assertField($config, 'pretext', 'string|null'); $init = $class->getMethod('initialize'); - $init->addBody(Debugger::class . '::setLogger(new ' . SlackLogger::class . '(?, ?));', [$config['slackUrl'], $config['logUrl']]); + $init->addBody('?::setLogger(new ?(?, ?, ?, ?, ?, ?));', [ + new PhpLiteral(Debugger::class), + new PhpLiteral(SlackLogger::class), + $config['slackUrl'], + $config['logUrl'], + $config['channel'], + $config['username'], + $config['icon'], + $config['pretext'], + ]); } } - - } diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 0037fe7..dffe699 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -1,6 +1,7 @@ */ namespace greeny\NetteSlackLogger; @@ -8,24 +9,35 @@ use Exception; use Tracy\BlueScreen; use Tracy\Debugger; +use Tracy\ILogger; use Tracy\Logger; class SlackLogger extends Logger { - /** @var string */ private $slackUrl; - /** @var string */ private $logUrl; + /** @var string */ + private $channel; + /** @var string */ + private $username; + /** @var string */ + private $icon; + /** @var string */ + private $pretext; - public function __construct($slackUrl, $logUrl) + public function __construct($slackUrl, $logUrl, $channel = NULL, $username = NULL, $icon = NULL, $pretext = NULL) { parent::__construct(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen()); $this->slackUrl = $slackUrl; $this->logUrl = $logUrl; + $this->channel = $channel; + $this->username = $username; + $this->icon = $icon; + $this->pretext = $pretext; } @@ -39,39 +51,85 @@ public function log($value, $priority = self::INFO) $message = ucfirst($priority) . ': '; if ($value instanceof Exception) { $message .= $value->getMessage(); + } elseif (is_array($value)) { + $message .= reset($value); } else { - if (is_array($value)) { - $message .= reset($value); - } else { - $message .= (string) $value; - } + $message .= (string) $value; } - if ($this->logUrl && $logFile) { + if ($logFile && $this->logUrl) { $message .= ' (<' . str_replace('__FILE__', basename($logFile), $this->logUrl) . '|Open log file>)'; } - $this->sendSlackMessage($message); + $this->sendSlackMessage($message, $priority); return $logFile; } /** * @param string $message + * @param string $priority one of {@link ILogger} priority constants */ - private function sendSlackMessage($message) + private function sendSlackMessage($message, $priority) { - file_get_contents($this->slackUrl, NULL, stream_context_create([ + $payload = array_filter([ + 'channel' => $this->channel, + 'username' => $this->username, + 'icon_emoji' => $this->icon, + 'attachments' => [ + array_filter([ + 'text' => $message, + 'color' => self::getColor($priority), + 'pretext' => $this->pretext, + ]), + ], + ]); + + self::slackPost($this->slackUrl, ['payload' => json_encode($payload)]); + } + + private static function getColor($priority) + { + switch ($priority) { + case ILogger::DEBUG: + case ILogger::INFO: + return '#444444'; + + case ILogger::WARNING: + return 'warning'; + + case ILogger::ERROR: + case ILogger::EXCEPTION: + case ILogger::CRITICAL: + return 'danger'; + + default: + return null; + } + } + + private static function slackPost($url, array $postContent) + { + $ctxOptions = [ 'http' => [ 'method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => http_build_query([ - 'payload' => json_encode([ - 'text' => $message, - ]) - ]), + 'content' => http_build_query($postContent), ], - ])); - } + ]; + $ctx = stream_context_create($ctxOptions); + $resultStr = file_get_contents($url, NULL, $ctx); + if ($resultStr === FALSE) { + throw new \RuntimeException('Error sending request to the Slack API.'); + } + $result = json_decode($resultStr); + if ($result === NULL) { + throw new \RuntimeException('Error decoding response from Slack - not a well-formed JSON.'); + } + if (!$result->ok) { + throw new \RuntimeException('Slack Error: ' . $result->error); + } + return $result; + } } From f0fed9207360606da409e3761476f845d9affbf8 Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Wed, 2 Mar 2016 22:28:39 +0100 Subject: [PATCH 02/13] re-package to namespace OndrejBouda until the pull request gets accepted by the original author --- README.md | 4 ++-- composer.json | 6 +++--- src/DI/SlackLoggerExtension.php | 7 +++---- src/SlackLogger.php | 4 +--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 67ea036..e8cb5f6 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ Log your errors directly into a Slack room ## Installation -`composer require greeny/nette-slack-logger` +`composer require OndrejBouda/nette-slack-logger` And register extension to your config.neon: ```yaml extensions: - slackLogger: greeny\NetteSlackLogger\DI\SlackLoggerExtension + slackLogger: OndrejBouda\NetteSlackLogger\DI\SlackLoggerExtension ``` By default the logger is just turned off, since you probably do not want to log errors from dev environment. If you want diff --git a/composer.json b/composer.json index 90b3196..05f7f49 100644 --- a/composer.json +++ b/composer.json @@ -1,11 +1,11 @@ { - "name": "greeny/nette-slack-logger", - "description": "Log your error messages directly into Slack room", + "name": "ondrej-bouda/nette-slack-logger", + "description": "Log your error messages directly into a Slack room", "license": "MIT", "keywords": ["Nette", "Slack", "logger"], "autoload": { "psr-4": { - "greeny\\NetteSlackLogger\\": "src/" + "OndrejBouda\\NetteSlackLogger\\": "src/" } }, "require": { diff --git a/src/DI/SlackLoggerExtension.php b/src/DI/SlackLoggerExtension.php index 2fedf7b..4b28493 100644 --- a/src/DI/SlackLoggerExtension.php +++ b/src/DI/SlackLoggerExtension.php @@ -1,12 +1,11 @@ */ +namespace OndrejBouda\NetteSlackLogger\DI; -namespace greeny\NetteSlackLogger\DI; - -use greeny\NetteSlackLogger\SlackLogger; +use OndrejBouda\NetteSlackLogger\SlackLogger; use Nette\DI\CompilerExtension; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpLiteral; diff --git a/src/SlackLogger.php b/src/SlackLogger.php index dffe699..583b439 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -3,11 +3,9 @@ * @author Tomáš Blatný * @author Ondřej Bouda */ - -namespace greeny\NetteSlackLogger; +namespace OndrejBouda\NetteSlackLogger; use Exception; -use Tracy\BlueScreen; use Tracy\Debugger; use Tracy\ILogger; use Tracy\Logger; From ef3b0454d5fa9901b5798b01bc470507e12724c2 Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Wed, 2 Mar 2016 23:40:23 +0100 Subject: [PATCH 03/13] fix the installation instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8cb5f6..c2b2d26 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Log your errors directly into a Slack room ## Installation -`composer require OndrejBouda/nette-slack-logger` +`composer require ondrej-bouda/nette-slack-logger` And register extension to your config.neon: From faba68436de423a8e475169ca9403b94717f1a0a Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Thu, 3 Mar 2016 00:19:25 +0100 Subject: [PATCH 04/13] ignore .idea directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 987e2a2..db7c62c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/.idea composer.lock vendor From 65fb6d82a45d281157846c6fb38fd2af2c0ee0f5 Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Thu, 3 Mar 2016 00:21:36 +0100 Subject: [PATCH 05/13] fix processing response from Slack --- src/SlackLogger.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 583b439..ed1e6f6 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -116,18 +116,10 @@ private static function slackPost($url, array $postContent) ], ]; $ctx = stream_context_create($ctxOptions); - $resultStr = file_get_contents($url, NULL, $ctx); + $resultStr = @file_get_contents($url, NULL, $ctx); - if ($resultStr === FALSE) { - throw new \RuntimeException('Error sending request to the Slack API.'); + if ($resultStr != 'ok') { + throw new \RuntimeException('Error sending request to the Slack API: ' . $http_response_header[0]); } - $result = json_decode($resultStr); - if ($result === NULL) { - throw new \RuntimeException('Error decoding response from Slack - not a well-formed JSON.'); - } - if (!$result->ok) { - throw new \RuntimeException('Slack Error: ' . $result->error); - } - return $result; } } From 09cca43df4ba5d44d18ee203be9e503a9f410ff6 Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Tue, 22 Mar 2016 01:25:50 +0100 Subject: [PATCH 06/13] update for PHP 7 Throwables --- src/SlackLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlackLogger.php b/src/SlackLogger.php index ed1e6f6..4eee7f8 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -47,7 +47,7 @@ public function log($value, $priority = self::INFO) $logFile = parent::log($value, $priority); $message = ucfirst($priority) . ': '; - if ($value instanceof Exception) { + if ($value instanceof Exception || $value instanceof \Throwable) { // NOTE: backwards compatibility with PHP 5 $message .= $value->getMessage(); } elseif (is_array($value)) { $message .= reset($value); From 153aad08e78bcbb6f1cbeba00bd7c435565d9177 Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Wed, 20 Sep 2017 10:38:53 +0200 Subject: [PATCH 07/13] allow using min interval between two logged events to prevent flooding Slack with similar messages --- src/DI/SlackLoggerExtension.php | 78 ++++----- src/SlackLogger.php | 277 +++++++++++++++++++------------- 2 files changed, 209 insertions(+), 146 deletions(-) diff --git a/src/DI/SlackLoggerExtension.php b/src/DI/SlackLoggerExtension.php index 4b28493..4c2372a 100644 --- a/src/DI/SlackLoggerExtension.php +++ b/src/DI/SlackLoggerExtension.php @@ -16,41 +16,45 @@ class SlackLoggerExtension extends CompilerExtension { - private $defaults = [ - 'enabled' => FALSE, - 'logUrl' => NULL, - 'channel' => NULL, - 'username' => NULL, - 'icon' => NULL, - 'pretext' => NULL, - ]; - - - public function afterCompile(ClassType $class) - { - $config = $this->getConfig($this->defaults); - - Validators::assertField($config, 'enabled', 'boolean'); - - if ($config['enabled']) { - Validators::assertField($config, 'slackUrl', 'string'); - Validators::assertField($config, 'logUrl', 'string|null'); - Validators::assertField($config, 'channel', 'string|null'); - Validators::assertField($config, 'username', 'string|null'); - Validators::assertField($config, 'icon', 'string|null'); - Validators::assertField($config, 'pretext', 'string|null'); - - $init = $class->getMethod('initialize'); - $init->addBody('?::setLogger(new ?(?, ?, ?, ?, ?, ?));', [ - new PhpLiteral(Debugger::class), - new PhpLiteral(SlackLogger::class), - $config['slackUrl'], - $config['logUrl'], - $config['channel'], - $config['username'], - $config['icon'], - $config['pretext'], - ]); - } - } + private $defaults = [ + 'enabled' => FALSE, + 'logUrl' => NULL, + 'channel' => NULL, + 'username' => NULL, + 'icon' => NULL, + 'pretext' => NULL, + ]; + + + public function afterCompile(ClassType $class) + { + $config = $this->getConfig($this->defaults); + + Validators::assertField($config, 'enabled', 'boolean'); + + if ($config['enabled']) { + Validators::assertField($config, 'slackUrl', 'string'); + Validators::assertField($config, 'logUrl', 'string|null'); + Validators::assertField($config, 'channel', 'string|null'); + Validators::assertField($config, 'username', 'string|null'); + Validators::assertField($config, 'icon', 'string|null'); + Validators::assertField($config, 'pretext', 'string|null'); + Validators::assertField($config, 'file', 'string|null'); + Validators::assertField($config, 'interval', 'int|null'); + + $init = $class->getMethod('initialize'); + $init->addBody('?::setLogger(new ?(?, ?, ?, ?, ?, ?, ?, ?));', [ + new PhpLiteral(Debugger::class), + new PhpLiteral(SlackLogger::class), + $config['slackUrl'], + $config['logUrl'], + $config['channel'], + $config['username'], + $config['icon'], + $config['pretext'], + $config['file'], + $config['interval'], + ]); + } + } } diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 4eee7f8..84ce81e 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -3,6 +3,7 @@ * @author Tomáš Blatný * @author Ondřej Bouda */ + namespace OndrejBouda\NetteSlackLogger; use Exception; @@ -13,113 +14,171 @@ class SlackLogger extends Logger { - /** @var string */ - private $slackUrl; - /** @var string */ - private $logUrl; - /** @var string */ - private $channel; - /** @var string */ - private $username; - /** @var string */ - private $icon; - /** @var string */ - private $pretext; - - - public function __construct($slackUrl, $logUrl, $channel = NULL, $username = NULL, $icon = NULL, $pretext = NULL) - { - parent::__construct(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen()); - $this->slackUrl = $slackUrl; - $this->logUrl = $logUrl; - $this->channel = $channel; - $this->username = $username; - $this->icon = $icon; - $this->pretext = $pretext; - } - - - /** - * @inheritdoc - */ - public function log($value, $priority = self::INFO) - { - $logFile = parent::log($value, $priority); - - $message = ucfirst($priority) . ': '; - if ($value instanceof Exception || $value instanceof \Throwable) { // NOTE: backwards compatibility with PHP 5 - $message .= $value->getMessage(); - } elseif (is_array($value)) { - $message .= reset($value); - } else { - $message .= (string) $value; - } - - if ($logFile && $this->logUrl) { - $message .= ' (<' . str_replace('__FILE__', basename($logFile), $this->logUrl) . '|Open log file>)'; - } - - $this->sendSlackMessage($message, $priority); - return $logFile; - } - - - /** - * @param string $message - * @param string $priority one of {@link ILogger} priority constants - */ - private function sendSlackMessage($message, $priority) - { - $payload = array_filter([ - 'channel' => $this->channel, - 'username' => $this->username, - 'icon_emoji' => $this->icon, - 'attachments' => [ - array_filter([ - 'text' => $message, - 'color' => self::getColor($priority), - 'pretext' => $this->pretext, - ]), - ], - ]); - - self::slackPost($this->slackUrl, ['payload' => json_encode($payload)]); - } - - private static function getColor($priority) - { - switch ($priority) { - case ILogger::DEBUG: - case ILogger::INFO: - return '#444444'; - - case ILogger::WARNING: - return 'warning'; - - case ILogger::ERROR: - case ILogger::EXCEPTION: - case ILogger::CRITICAL: - return 'danger'; - - default: - return null; - } - } - - private static function slackPost($url, array $postContent) - { - $ctxOptions = [ - 'http' => [ - 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => http_build_query($postContent), - ], - ]; - $ctx = stream_context_create($ctxOptions); - $resultStr = @file_get_contents($url, NULL, $ctx); - - if ($resultStr != 'ok') { - throw new \RuntimeException('Error sending request to the Slack API: ' . $http_response_header[0]); - } - } + /** @var string */ + private $slackUrl; + /** @var string */ + private $logUrl; + /** @var string */ + private $channel; + /** @var string */ + private $username; + /** @var string */ + private $icon; + /** @var string */ + private $pretext; + /** @var int|string The minimum interval between two events. */ + private $interval = '1 day'; + /** @var string|null File where the filter persists the state. */ + private $file; + /** @var bool Indicates whether problems with the file write-ability was already logged (prevents recursion). */ + private $loggedUnwriteableFile = false; + + + public function __construct($slackUrl, $logUrl, $channel = null, $username = null, $icon = null, $pretext = null, $file = null, $interval = null) + { + parent::__construct(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen()); + $this->slackUrl = $slackUrl; + $this->logUrl = $logUrl; + $this->channel = $channel; + $this->username = $username; + $this->icon = $icon; + $this->pretext = $pretext; + $this->file = $file; + $this->interval = $interval; + } + + + /** + * @inheritdoc + */ + public function log($value, $priority = self::INFO) + { + $logFile = parent::log($value, $priority); + + $message = ucfirst($priority) . ': '; + if ($value instanceof Exception || $value instanceof \Throwable) { // NOTE: backwards compatibility with PHP 5 + $message .= $value->getMessage(); + } elseif (is_array($value)) { + $message .= reset($value); + } else { + $message .= (string)$value; + } + + if ($logFile && $this->logUrl) { + $message .= ' (<' . str_replace('__FILE__', basename($logFile), $this->logUrl) . '|Open log file>)'; + } + + $notify = $this->isAllowedByInterval(); + //add logged message to buffer + $logSuccessful = (bool) @file_put_contents($this->getFile(), "\n" . date("r") . "\n" . $message, FILE_APPEND); + + if (!$logSuccessful && !$this->loggedUnwriteableFile) { + $this->loggedUnwriteableFile = true; + trigger_error("Unable to write to file '{$this->getFile()}'. Filter will deny the incoming events.", E_USER_WARNING); + } + + if ($notify) { + //if interval after last update has passed, flush all messages in queue to Slack + $this->sendSlackMessage(@file_get_contents($this->getFile()), $priority); + @file_put_contents($this->getFile(), null); + } + return $logFile; + } + + private function isAllowedByInterval(): bool + { + $now = time(); + + $interval = $this->getInterval(); + if (!isset($interval)) { + return true; + } + if (!is_numeric($interval)) { + $interval = strtotime($interval) - $now; + } + + $lastEventTime = @filemtime($this->getFile()); + $nextPossibleEventTime = $lastEventTime + $interval; + + return $now >= $nextPossibleEventTime; + } + + /** + * @return null|string + */ + public function getFile() + { + return $this->file; + } + + /** + * @return int|string + */ + public function getInterval() + { + return $this->interval; + } + + /** + * @param string $message + * @param string $priority one of {@link ILogger} priority constants + */ + private function sendSlackMessage($message, $priority) + { + $payload = array_filter( + [ + 'channel' => $this->channel, + 'username' => $this->username, + 'icon_emoji' => $this->icon, + 'attachments' => [ + array_filter( + [ + 'text' => $message, + 'color' => self::getColor($priority), + 'pretext' => $this->pretext, + ] + ), + ], + ] + ); + + self::slackPost($this->slackUrl, ['payload' => json_encode($payload)]); + } + + private static function getColor($priority) + { + switch ($priority) { + case ILogger::DEBUG: + case ILogger::INFO: + return '#444444'; + + case ILogger::WARNING: + return 'warning'; + + case ILogger::ERROR: + case ILogger::EXCEPTION: + case ILogger::CRITICAL: + return 'danger'; + + default: + return null; + } + } + + private static function slackPost($url, array $postContent) + { + $ctxOptions = [ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => http_build_query($postContent), + ], + ]; + $ctx = stream_context_create($ctxOptions); + $resultStr = @file_get_contents($url, null, $ctx); + + if ($resultStr != 'ok') { + throw new \RuntimeException('Error sending request to the Slack API: ' . $http_response_header[0]); + } + } } From e84fb2a64867960091c2ca2e4a6d8dfc7ceed8af Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Wed, 20 Sep 2017 15:36:36 +0200 Subject: [PATCH 08/13] * throw away error messages not fitting into interval * add optional warning about blind time after notification was sent * truncate long messages --- src/DI/SlackLoggerExtension.php | 22 +++++--- src/SlackLogger.php | 90 +++++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/DI/SlackLoggerExtension.php b/src/DI/SlackLoggerExtension.php index 4c2372a..ed598be 100644 --- a/src/DI/SlackLoggerExtension.php +++ b/src/DI/SlackLoggerExtension.php @@ -3,6 +3,7 @@ * @author Tomáš Blatný * @author Ondřej Bouda */ + namespace OndrejBouda\NetteSlackLogger\DI; use OndrejBouda\NetteSlackLogger\SlackLogger; @@ -17,12 +18,15 @@ class SlackLoggerExtension extends CompilerExtension { private $defaults = [ - 'enabled' => FALSE, - 'logUrl' => NULL, - 'channel' => NULL, - 'username' => NULL, - 'icon' => NULL, - 'pretext' => NULL, + 'enabled' => false, + 'logUrl' => null, + 'channel' => null, + 'username' => null, + 'icon' => null, + 'pretext' => null, + 'interval' => null, + 'file' => null, + 'showIntervalWarning' => false, ]; @@ -40,10 +44,11 @@ public function afterCompile(ClassType $class) Validators::assertField($config, 'icon', 'string|null'); Validators::assertField($config, 'pretext', 'string|null'); Validators::assertField($config, 'file', 'string|null'); - Validators::assertField($config, 'interval', 'int|null'); + Validators::assertField($config, 'interval', 'int|string|null'); + Validators::assertField($config, 'showIntervalWarning', 'boolean|null'); $init = $class->getMethod('initialize'); - $init->addBody('?::setLogger(new ?(?, ?, ?, ?, ?, ?, ?, ?));', [ + $init->addBody('?::setLogger(new ?(?, ?, ?, ?, ?, ?, ?, ?, ?));', [ new PhpLiteral(Debugger::class), new PhpLiteral(SlackLogger::class), $config['slackUrl'], @@ -54,6 +59,7 @@ public function afterCompile(ClassType $class) $config['pretext'], $config['file'], $config['interval'], + $config['showIntervalWarning'], ]); } } diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 84ce81e..a53fbf2 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -14,6 +14,9 @@ class SlackLogger extends Logger { + //Default maximum length of Slack message is 5000 characters. Note that ~150 characters should be reserved for interval warning (see SlackLogger::$showIntervalWarning) + const MAX_MESSAGE_LENGTH = 4850; + /** @var string */ private $slackUrl; /** @var string */ @@ -26,15 +29,17 @@ class SlackLogger extends Logger private $icon; /** @var string */ private $pretext; - /** @var int|string The minimum interval between two events. */ - private $interval = '1 day'; + /** @var int|string|null The minimum interval between two events. Accepts either number of seconds or date/time string for strtotime(). Attribute $file has to be set as well, otherwise $interval won't be acknowledged. */ + private $interval; /** @var string|null File where the filter persists the state. */ private $file; /** @var bool Indicates whether problems with the file write-ability was already logged (prevents recursion). */ private $loggedUnwriteableFile = false; + /** @var bool Whether information about no more slack notifications for seconds shall be sent with error message */ + private $showIntervalWarning = false; - public function __construct($slackUrl, $logUrl, $channel = null, $username = null, $icon = null, $pretext = null, $file = null, $interval = null) + public function __construct($slackUrl, $logUrl, $channel = null, $username = null, $icon = null, $pretext = null, $file = null, $interval = null, $showIntervalWarning = true) { parent::__construct(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen()); $this->slackUrl = $slackUrl; @@ -45,6 +50,7 @@ public function __construct($slackUrl, $logUrl, $channel = null, $username = nul $this->pretext = $pretext; $this->file = $file; $this->interval = $interval; + $this->showIntervalWarning = $showIntervalWarning; } @@ -53,36 +59,63 @@ public function __construct($slackUrl, $logUrl, $channel = null, $username = nul */ public function log($value, $priority = self::INFO) { + $notify = $this->isAllowedByInterval(); + $logFile = parent::log($value, $priority); - $message = ucfirst($priority) . ': '; - if ($value instanceof Exception || $value instanceof \Throwable) { // NOTE: backwards compatibility with PHP 5 - $message .= $value->getMessage(); - } elseif (is_array($value)) { - $message .= reset($value); - } else { - $message .= (string)$value; - } + if ($notify) { - if ($logFile && $this->logUrl) { - $message .= ' (<' . str_replace('__FILE__', basename($logFile), $this->logUrl) . '|Open log file>)'; + $message = ucfirst($priority) . ': '; + if ($value instanceof Exception || $value instanceof \Throwable) { // NOTE: backwards compatibility with PHP 5 + $message .= $value->getMessage(); + } elseif (is_array($value)) { + $message .= reset($value); + } else { + $message .= (string)$value; + } + + if ($logFile && $this->logUrl) { + $message .= ' (<' . str_replace('__FILE__', basename($logFile), $this->logUrl) . '|Open log file>)'; + } + + $truncated = mb_strlen($message) - self::MAX_MESSAGE_LENGTH; + $message = mb_substr($message, 0, self::MAX_MESSAGE_LENGTH); + + if ($truncated > 0) { + $message .= '...' . PHP_EOL . '(' . $truncated . ')'; + } + + $interval = $this->getInterval(); + + if (isset($interval)) { + //log time of this notification + $this->mark(); + if ($this->showIntervalWarning) { + $message .= PHP_EOL . '*NOTE: No further Slack notifications will be sent for another ' . strtotime($interval) . ' seconds*'; + } + } + + //if interval after last update has passed, flush error message to Slack + $this->sendSlackMessage($message, $priority); } + return $logFile; + } - $notify = $this->isAllowedByInterval(); - //add logged message to buffer - $logSuccessful = (bool) @file_put_contents($this->getFile(), "\n" . date("r") . "\n" . $message, FILE_APPEND); + /** + * Marks the passed event and updates the stateful information. + * + * @return bool True whether mark was successful; false otherwise. + */ + private function mark(): bool + { + $hasMarked = (bool)@file_put_contents($this->getFile(), static::class . PHP_EOL . date("r")); - if (!$logSuccessful && !$this->loggedUnwriteableFile) { + if (!$hasMarked && !$this->loggedUnwriteableFile) { $this->loggedUnwriteableFile = true; trigger_error("Unable to write to file '{$this->getFile()}'. Filter will deny the incoming events.", E_USER_WARNING); } - if ($notify) { - //if interval after last update has passed, flush all messages in queue to Slack - $this->sendSlackMessage(@file_get_contents($this->getFile()), $priority); - @file_put_contents($this->getFile(), null); - } - return $logFile; + return $hasMarked; } private function isAllowedByInterval(): bool @@ -90,8 +123,13 @@ private function isAllowedByInterval(): bool $now = time(); $interval = $this->getInterval(); - if (!isset($interval)) { - return true; + $file = $this->getFile(); + if (!isset($interval) || !isset($file)) { + if (!isset($interval)) { + return true; + } else { + throw new \InvalidArgumentException('Interval for SlackLogger is set, but no file for storing time of last notification is specified.'); + } } if (!is_numeric($interval)) { $interval = strtotime($interval) - $now; @@ -100,7 +138,7 @@ private function isAllowedByInterval(): bool $lastEventTime = @filemtime($this->getFile()); $nextPossibleEventTime = $lastEventTime + $interval; - return $now >= $nextPossibleEventTime; + return ($now >= $nextPossibleEventTime); } /** From b02555dc72c80138232803977aeef51e5b2e9f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Bouda?= Date: Mon, 2 Oct 2017 12:28:52 +0200 Subject: [PATCH 09/13] fix indentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c2b2d26..9388386 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ And register extension to your config.neon: ```yaml extensions: - slackLogger: OndrejBouda\NetteSlackLogger\DI\SlackLoggerExtension + slackLogger: OndrejBouda\NetteSlackLogger\DI\SlackLoggerExtension ``` By default the logger is just turned off, since you probably do not want to log errors from dev environment. If you want @@ -17,8 +17,8 @@ to enable it, add following lines to config.local.neon at your production server ```yaml slackLogger: - enabled: true - slackUrl: https://hooks.slack.com/services/XXX + enabled: true + slackUrl: https://hooks.slack.com/services/XXX logUrl: http://path/to/your/logs/directory/__FILE__ channel: "#somechannel" username: "PHP Bot" From ae162b782b65b63154ffd0f71405faa569822efb Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Mon, 2 Oct 2017 18:45:05 +0200 Subject: [PATCH 10/13] fix composer: declare requirement of PHP ^7.0 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 05f7f49..ac35c97 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ } }, "require": { + "php": "^7.0", "tracy/tracy": "^2.3" } } From 1aecda6e92f951d88f288f812254b0a89023b50c Mon Sep 17 00:00:00 2001 From: Ondrej Bouda Date: Mon, 2 Oct 2017 18:46:09 +0200 Subject: [PATCH 11/13] refactor isset() checks to ===null tests, fix printing number of seconds in the interval warning --- src/SlackLogger.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/SlackLogger.php b/src/SlackLogger.php index a53fbf2..81ff77e 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -87,11 +87,12 @@ public function log($value, $priority = self::INFO) $interval = $this->getInterval(); - if (isset($interval)) { + if ($interval !== null) { //log time of this notification $this->mark(); if ($this->showIntervalWarning) { - $message .= PHP_EOL . '*NOTE: No further Slack notifications will be sent for another ' . strtotime($interval) . ' seconds*'; + $secs = (is_numeric($interval) ? $interval : (strtotime($interval) ?: '?')); + $message .= PHP_EOL . '*NOTE: No further Slack notifications will be sent for another ' . $secs . ' seconds*'; } } @@ -124,13 +125,16 @@ private function isAllowedByInterval(): bool $interval = $this->getInterval(); $file = $this->getFile(); - if (!isset($interval) || !isset($file)) { - if (!isset($interval)) { - return true; - } else { - throw new \InvalidArgumentException('Interval for SlackLogger is set, but no file for storing time of last notification is specified.'); - } + + if ($interval === null) { + return true; } + if ($file === null) { + throw new \InvalidArgumentException( + 'Interval for SlackLogger is set, but no file for storing time of last notification is specified.' + ); + } + if (!is_numeric($interval)) { $interval = strtotime($interval) - $now; } From 24f6b8c57eca35a91965eba7cca4e755fa87aafa Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Fri, 13 Dec 2019 16:21:07 +0100 Subject: [PATCH 12/13] ignore the interval if a higher priority error than the last is being thrown --- src/SlackLogger.php | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 81ff77e..35bcee7 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -59,7 +59,7 @@ public function __construct($slackUrl, $logUrl, $channel = null, $username = nul */ public function log($value, $priority = self::INFO) { - $notify = $this->isAllowedByInterval(); + $notify = $this->isAllowedByInterval($priority); $logFile = parent::log($value, $priority); @@ -107,9 +107,14 @@ public function log($value, $priority = self::INFO) * * @return bool True whether mark was successful; false otherwise. */ - private function mark(): bool + private function mark($priority = self::INFO): bool { - $hasMarked = (bool)@file_put_contents($this->getFile(), static::class . PHP_EOL . date("r")); + $hasMarked = (bool)@file_put_contents( + $this->getFile(), + static::class . PHP_EOL . + date("r") . PHP_EOL . + $priority + ); if (!$hasMarked && !$this->loggedUnwriteableFile) { $this->loggedUnwriteableFile = true; @@ -119,7 +124,7 @@ private function mark(): bool return $hasMarked; } - private function isAllowedByInterval(): bool + private function isAllowedByInterval($priority = self::INFO): bool { $now = time(); @@ -139,6 +144,13 @@ private function isAllowedByInterval(): bool $interval = strtotime($interval) - $now; } + $data = explode(PHP_EOL, file_get_contents($file)); + + //ignore the interval if a higher priority error than the last is being logged + if(self::isHigherPriority($priority, $data[2] ?? null)) { + return true; + } + $lastEventTime = @filemtime($this->getFile()); $nextPossibleEventTime = $lastEventTime + $interval; @@ -223,4 +235,22 @@ private static function slackPost($url, array $postContent) throw new \RuntimeException('Error sending request to the Slack API: ' . $http_response_header[0]); } } + + private static function isHigherPriority(?string $currentPriority, ?string $previousPriority): bool + { + $priorityList = [ + self::DEBUG => 1, + self::INFO => 2, + self::WARNING => 3, + self::ERROR => 4, + self::EXCEPTION => 5, + self::CRITICAL => 6, + ]; + + //always send notification for unknown priorities + return ( + ($priorityList[$currentPriority] ?? PHP_INT_MAX) > + ($priorityList[$previousPriority] ?? 0) + ); + } } From 10b8dc1ea4b97f79109580318d0eda92e511ee61 Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Thu, 9 Jan 2020 09:12:49 +0100 Subject: [PATCH 13/13] fix marking the last message priority --- src/SlackLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlackLogger.php b/src/SlackLogger.php index 35bcee7..2aebaea 100644 --- a/src/SlackLogger.php +++ b/src/SlackLogger.php @@ -89,7 +89,7 @@ public function log($value, $priority = self::INFO) if ($interval !== null) { //log time of this notification - $this->mark(); + $this->mark($priority); if ($this->showIntervalWarning) { $secs = (is_numeric($interval) ? $interval : (strtotime($interval) ?: '?')); $message .= PHP_EOL . '*NOTE: No further Slack notifications will be sent for another ' . $secs . ' seconds*';