From c3858f77186f28f74eebc398a56e8225a2b9f17a Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Mon, 16 Sep 2024 15:50:11 +0300 Subject: [PATCH 1/3] Symfony catcher --- .gitignore | 7 + .php-cs-fixer.php | 63 ++++ README.md | 111 ++++++- composer.json | 34 +++ psalm.xml | 5 + src/Catcher.php | 112 +++++++ src/DependencyInjection/Configuration.php | 29 ++ src/DependencyInjection/HawkExtension.php | 45 +++ src/Exception/SilencedErrorException.php | 9 + src/HawkBundle.php | 17 ++ src/HawkHandler.php | 346 ++++++++++++++++++++++ src/Monolog/Handler.php | 39 +++ src/Resources/config/services.yaml | 7 + src/Transport/GuzzlePromisesTransport.php | 69 +++++ 14 files changed, 891 insertions(+), 2 deletions(-) create mode 100644 .php-cs-fixer.php create mode 100644 composer.json create mode 100644 psalm.xml create mode 100644 src/Catcher.php create mode 100644 src/DependencyInjection/Configuration.php create mode 100644 src/DependencyInjection/HawkExtension.php create mode 100644 src/Exception/SilencedErrorException.php create mode 100644 src/HawkBundle.php create mode 100644 src/HawkHandler.php create mode 100644 src/Monolog/Handler.php create mode 100644 src/Resources/config/services.yaml create mode 100644 src/Transport/GuzzlePromisesTransport.php diff --git a/.gitignore b/.gitignore index 3dab634..92e81b9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,10 @@ # Embedded web-server pid file /.web-server-pid + +# PhpStorm +/.idea + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..94881a8 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,63 @@ +setUsingCache(true) + ->setCacheFile(__DIR__ . '/.php-cs-fixer.cache') + ->setRules([ + '@PSR2' => true, + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => ['statements' => ['return']], + 'cast_spaces' => true, + 'concat_space' => ['spacing' => 'one'], + 'constant_case' => true, + 'declare_equal_normalize' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one']], + 'lowercase_cast' => true, + 'method_argument_space' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'multiline_whitespace_before_semicolons' => false, + 'ordered_imports' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types_order' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'single_quote' => true, + 'short_scalar_cast' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trim_array_spaces' => true, + 'visibility_required' => ['elements' => ['property', 'method', 'const']], + 'yoda_style' => false, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in([__DIR__ . '/src']) + ->append([__FILE__]) + ->exclude([ + 'tests', + 'vendor', + 'var' + ]) + ); diff --git a/README.md b/README.md index 03d514e..731f313 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ -# hawk.symfony -Symfony errors Catcher module for Hawk.so +# Hawk Symfony + +Symfony errors Catcher for [Hawk.so](https://hawk.so). + +## Setup + +1. [Register](https://garage.hawk.so/sign-up) an account, create a Project and get an Integration Token. + +2. Install SDK via [composer](https://getcomposer.org) to install the Catcher + +Catcher provides support for PHP 7.2 or later + +```bash +$ composer require codex-team/hawk.symfony +``` + +### Configuration + +Add the following authorization information to your `.env` file: + +```env +HAWK_TOKEN= +``` + +Create a configuration file at `config/packages/hawk.yaml` with the following content: + +```php +HawkBundle\HawkBundle::class => ['all' => true] +``` + +In the `config/packages/monolog.yaml` file, specify the handler settings under the appropriate section (`dev` or `prod`): + +```yaml +hawk: + type: service + id: HawkBundle\Monolog\Handler + level: error +``` + +### Adding User Information to Error Reports: + +```php +$this->catcher->setUser([ + 'name' => 'user name', + 'photo' => 'user photo', +]); + +$this->catcher->setContext([ + // Additional context information +]); +``` + +### Sending Exceptions Manually: +To manually send exceptions, initialize `__construct(\HawkBundle\Catcher $catcher)` class via dependency injection (DI), and use the following method: + +```php +$this->catcher->sendException($exception); +``` + +### Sending Custom Messages: + +You can also send custom messages using the `->sendMessage(...)` method: + +```php +$this->catcher->sendMessage( + 'your message', + [ + // Additional context information + ] +); +``` + +### Example: Sending Manually + +```php +private $catcher; + +public function __construct(\HawkBundle\Catcher $catcher) +{ + $this->catcher = $catcher; +} + +public function test() +{ + try { + // The code where you need to catch the error + } catch (\Exception $exception) { + $this->catcher->sendException($exception); + } +} +``` + +## Issues and improvements + +Feel free to ask questions or improve the project. + +## Links + +Repository: https://github.com/codex-team/hawk.symfony + +Report a bug: https://github.com/codex-team/hawk.symfony/issues + +Composer Package: https://packagist.org/packages/codex-team/hawk.symfony + +CodeX Team: https://codex.so + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..bd83b78 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "codex-team/hawk.php-symfony", + "description": "Symfony errors Catcher module for Hawk.so", + "keywords": ["hawk", "php", "error", "catcher", "monolog", "symfony"], + "type": "library", + "version": "0.0.1", + "license": "MIT", + "require": { + "php": "^7.2 || ^8.0", + "monolog/monolog": "^2.2 || ^3.0", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "codex-team/hawk.php": "^2.2.0", + "jean85/pretty-package-versions": "^1.5 || ^2.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19||^3.40" + }, + "autoload": { + "psr-4": { + "HawkBundle\\": "src/" + } + }, + "extra": { + "symfony": { + "autoload": true + } + }, + "scripts": { + "csfix": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --using-cache=no --verbose" + } +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..02e1c19 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Catcher.php b/src/Catcher.php new file mode 100644 index 0000000..4393714 --- /dev/null +++ b/src/Catcher.php @@ -0,0 +1,112 @@ +registerAddon(new Headers()); + + $transport = new GuzzlePromisesTransport($options->getUrl(), $client); + + $this->handler = new HawkHandler($options, $transport, $builder); + + $this->handler->registerErrorHandler(); + $this->handler->registerExceptionHandler(); + $this->handler->registerFatalHandler(); + } + + /** + * @param array $user + * + * @return $this + */ + public function setUser(array $user): self + { + $this->handler->withUser($user); + + return $this; + } + + /** + * @param array $context + * + * @return $this + */ + public function setContext(array $context): self + { + $this->handler->withContext($context); + + return $this; + } + + /** + * @param string $message + * @param array $context + */ + public function sendMessage(string $message, array $context = []): void + { + $this->handler->sendEvent([ + 'title' => $message, + 'context' => $context + ]); + } + + /** + * @param \Throwable $throwable + * @param array $context + */ + public function sendException(\Throwable $throwable, array $context = []) + { + $this->handler->handleException($throwable, $context); + } + + /** + * @param array $payload + */ + public function sendEvent(array $payload): void + { + $this->handler->sendEvent($payload); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..067e4fe --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,29 @@ +getRootNode(); + + $rootNode + ->children() + ->scalarNode('integration_token') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/HawkExtension.php b/src/DependencyInjection/HawkExtension.php new file mode 100644 index 0000000..57bebee --- /dev/null +++ b/src/DependencyInjection/HawkExtension.php @@ -0,0 +1,45 @@ +processConfiguration($configuration, $configs); + + // Load configuration files + $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yaml'); + + // Set parameters and register services + $container->setParameter('hawk.integration_token', $config['integration_token']); + + // Register Catcher + $container->register(Catcher::class) + ->setArgument('$options', ['integrationToken' => $config['integration_token']]) + ->setArgument('$client', new Reference(Client::class)); + + // Register Monolog\Handler + $container->register(Handler::class) + ->setArgument('$catcher', new Reference(Catcher::class)) + ->setArgument('$level', Logger::ERROR) + ->addTag('monolog.handler'); + } +} diff --git a/src/Exception/SilencedErrorException.php b/src/Exception/SilencedErrorException.php new file mode 100644 index 0000000..d8e842a --- /dev/null +++ b/src/Exception/SilencedErrorException.php @@ -0,0 +1,9 @@ + 'Deprecated', + \E_USER_DEPRECATED => 'User Deprecated', + \E_NOTICE => 'Notice', + \E_USER_NOTICE => 'User Notice', + \E_STRICT => 'Runtime Notice', + \E_WARNING => 'Warning', + \E_USER_WARNING => 'User Warning', + \E_COMPILE_WARNING => 'Compile Warning', + \E_CORE_WARNING => 'Core Warning', + \E_USER_ERROR => 'User Error', + \E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + \E_COMPILE_ERROR => 'Compile Error', + \E_PARSE => 'Parse Error', + \E_ERROR => 'Error', + \E_CORE_ERROR => 'Core Error', + ]; + + public function __construct( + Options $options, + TransportInterface $transport, + EventPayloadBuilder $eventPayloadBuilder + ) { + $this->options = $options; + $this->transport = $transport; + $this->eventPayloadBuilder = $eventPayloadBuilder; + } + + /** + * Attach user data for event logging. + * + * @param array $user + */ + public function setUser(array $user): void + { + $this->user = $user; + } + + /** + * Attach contextual data to provide more details about the event. + * + * @param array $context + */ + public function setContext(array $context): void + { + $this->context = $context; + } + + /** + * Register the error handler once to handle PHP errors. + */ + public function registerErrorHandler(): self + { + if ($this->isErrorHandlerRegistered) { + return $this; + } + + $errorHandlerCallback = \Closure::fromCallable([$this, 'handleError']); + + $this->previousErrorHandler = set_error_handler($errorHandlerCallback); + if (null === $this->previousErrorHandler) { + restore_error_handler(); + set_error_handler($errorHandlerCallback, $this->options->getErrorTypes()); + } + + $this->isErrorHandlerRegistered = true; + + return $this; + } + + /** + * Register the exception handler once to manage uncaught exceptions. + */ + public function registerExceptionHandler(): self + { + if ($this->isExceptionHandlerRegistered) { + return $this; + } + + $exceptionHandlerCallback = \Closure::fromCallable([$this, 'handleException']); + + $this->previousExceptionHandler = set_exception_handler($exceptionHandlerCallback); + $this->isExceptionHandlerRegistered = true; + + return $this; + } + + /** + * Register the fatal error handler to catch shutdown errors. + */ + public function registerFatalHandler(): self + { + if ($this->isFatalHandlerRegistered) { + return $this; + } + + register_shutdown_function(\Closure::fromCallable([$this, 'handleFatal'])); + $this->isFatalHandlerRegistered = true; + + return $this; + } + + /** + * Handle PHP errors, convert them to exceptions, and send the event. + */ + public function handleError(int $level, string $message, string $file, int $line): bool + { + $isSilencedError = 0 === error_reporting(); + + if (\PHP_MAJOR_VERSION >= 8) { + // Detect if the error was silenced in PHP 8+ + $isSilencedError = 0 === (error_reporting() & ~self::PHP8_FATAL_ERRORS); + + if ($level === (self::PHP8_FATAL_ERRORS & $level)) { + $isSilencedError = false; + } + } + + if ($isSilencedError) { + $exception = new SilencedErrorException(self::ERROR_LEVEL_DESCRIPTIONS[$level] . ': ' . $message, 0, $level, $file, $line); + } else { + $exception = new \ErrorException(self::ERROR_LEVEL_DESCRIPTIONS[$level] . ': ' . $message, 0, $level, $file, $line); + } + + $data = [ + 'exception' => $exception, + 'context' => $this->context, + 'user' => $this->user, + 'type' => $exception->getSeverity() + ]; + + $eventPayload = $this->eventPayloadBuilder->create($data); + $event = $this->buildEvent($eventPayload); + + if ($event !== null) { + $this->send($event); + + return false !== ($this->previousErrorHandler)($level, $message, $file, $line); + } + + return false; + } + + /** + * Handle uncaught exceptions and send the event. + */ + public function handleException(\Throwable $exception, array $context = []): void + { + $data = [ + 'exception' => $exception, + 'context' => array_merge($this->context, $context), + 'user' => $this->user + ]; + + $eventPayload = $this->eventPayloadBuilder->create($data); + $event = $this->buildEvent($eventPayload); + + if ($event !== null) { + $this->send($event); + } + + $previousExceptionHandlerException = $exception; + + $previousExceptionHandler = $this->previousExceptionHandler; + $this->previousExceptionHandler = null; + + try { + if (null !== $previousExceptionHandler) { + $previousExceptionHandler($exception); + + return; + } + } catch (\Throwable $previousExceptionHandlerException) { + // This `catch` statement is here to forcefully override the + // $previousExceptionHandlerException variable with the exception + // we just caught + } + + // If the instance of the exception we're handling is the same as the one + // caught from the previous exception handler then we give it back to the + // native PHP handler to prevent an infinite loop + if ($exception === $previousExceptionHandlerException) { + // Disable the fatal error handler or the error will be reported twice + $this->disableFatalErrorHandler = true; + + throw $exception; + } + + $this->handleException($previousExceptionHandlerException); + } + + /** + * Handle fatal errors that occur during script shutdown. + */ + public function handleFatal(): void + { + if ($this->disableFatalErrorHandler) { + return; + } + + $error = error_get_last(); + + if ( + $error === null + || is_array($error) && $error['type'] && (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING) + ) { + return; + } + + $payload = [ + 'exception' => new ErrorException( + $error['message'], + 0, + $error['type'], + $error['file'], + $error['line'] + ), + 'context' => $this->context, + 'user' => $this->user + ]; + + $eventPayload = $this->eventPayloadBuilder->create($payload); + $event = $this->buildEvent($eventPayload); + + if ($event !== null) { + $this->send($event); + } + } + + /** + * Prepare the event for sending by applying release information and optional modifications. + */ + public function sendEvent(array $payload): void + { + $payload['context'] = array_merge($this->context, $payload['context'] ?? []); + $payload['user'] = $this->user; + + $eventPayload = $this->eventPayloadBuilder->create($payload); + $event = $this->buildEvent($eventPayload); + + if ($event !== null) { + $this->send($event); + } + } + + /** + * Prepare the event for sending by applying release information and optional modifications. + */ + public function buildEvent(EventPayload $eventPayload): ?Event + { + $eventPayload->setRelease($this->options->getRelease()); + $beforeSendCallback = $this->options->getBeforeSend(); + if ($beforeSendCallback) { + $eventPayload = $beforeSendCallback($eventPayload); + if ($eventPayload === null) { + return null; + } + } + $event = new Event( + $this->options->getIntegrationToken(), + $eventPayload + ); + + return $event; + } + + /** + * Send the event to the remote server. + */ + private function send(Event $event): void + { + $this->transport->send($event); + } +} diff --git a/src/Monolog/Handler.php b/src/Monolog/Handler.php new file mode 100644 index 0000000..7f5a5e9 --- /dev/null +++ b/src/Monolog/Handler.php @@ -0,0 +1,39 @@ +catcher = $catcher; + } + + /** + * @inheritDoc + */ + public function write(array $record): void + { + $data = [ + 'level' => $record['level'], + 'title' => (new LineFormatter('%message%'))->format($record) + ]; + + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + $data['exception'] = $record['context']['exception']; + } + + $this->catcher->sendEvent($data); + } +} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml new file mode 100644 index 0000000..96be3ce --- /dev/null +++ b/src/Resources/config/services.yaml @@ -0,0 +1,7 @@ +services: + HawkBundle\Monolog\Handler: + public: false + arguments: + $level: 'error' + tags: + - { name: monolog.handler } \ No newline at end of file diff --git a/src/Transport/GuzzlePromisesTransport.php b/src/Transport/GuzzlePromisesTransport.php new file mode 100644 index 0000000..b191d84 --- /dev/null +++ b/src/Transport/GuzzlePromisesTransport.php @@ -0,0 +1,69 @@ +url = $url; + $this->client = $client; + } + + /** + * @inheritDoc + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * @inheritDoc + */ + public function send(Event $event): mixed + { + $promise = $this->client->postAsync($this->url, [ + 'json' => $event->jsonSerialize(), + 'headers' => [ + 'Content-Type' => 'application/json' + ], + 'timeout' => 10 + ]); + + return $promise->then( + function ($response) { + return $response->getBody()->getContents(); + }, + function (RequestException $e) { + throw new \Exception('Failed to send event: ' . $e->getMessage(), 0, $e); + } + )->wait(); + } +} From a979c7e5a4364901c9f5549dfb3158dbc48c10d2 Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Mon, 16 Sep 2024 20:23:25 +0300 Subject: [PATCH 2/3] Update codex-team/hawk.php version in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bd83b78..4d96bc7 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "php": "^7.2 || ^8.0", "monolog/monolog": "^2.2 || ^3.0", "guzzlehttp/guzzle": "^6.0 || ^7.0", - "codex-team/hawk.php": "^2.2.0", + "codex-team/hawk.php": "^2.2.1", "jean85/pretty-package-versions": "^1.5 || ^2.0", "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", From 8a446d3a447e1660b404386f4c2911ee95b5b63c Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Tue, 17 Sep 2024 15:37:02 +0300 Subject: [PATCH 3/3] Move code to main library and resolve Monolog version issues --- composer.json | 4 +- src/Catcher.php | 18 +- src/DependencyInjection/HawkExtension.php | 7 +- src/Exception/SilencedErrorException.php | 9 - src/HawkHandler.php | 346 ---------------------- src/Monolog/Handler.php | 9 +- src/Monolog/HandlerTrait.php | 34 +++ src/Transport/GuzzlePromisesTransport.php | 19 +- 8 files changed, 60 insertions(+), 386 deletions(-) delete mode 100644 src/Exception/SilencedErrorException.php delete mode 100644 src/HawkHandler.php create mode 100644 src/Monolog/HandlerTrait.php diff --git a/composer.json b/composer.json index 4d96bc7..1c1acc8 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "codex-team/hawk.php-symfony", + "name": "codex-team/hawk.symfony", "description": "Symfony errors Catcher module for Hawk.so", "keywords": ["hawk", "php", "error", "catcher", "monolog", "symfony"], "type": "library", @@ -9,7 +9,7 @@ "php": "^7.2 || ^8.0", "monolog/monolog": "^2.2 || ^3.0", "guzzlehttp/guzzle": "^6.0 || ^7.0", - "codex-team/hawk.php": "^2.2.1", + "codex-team/hawk.php": "^2.2.2", "jean85/pretty-package-versions": "^1.5 || ^2.0", "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", diff --git a/src/Catcher.php b/src/Catcher.php index 4393714..fb9e55b 100644 --- a/src/Catcher.php +++ b/src/Catcher.php @@ -4,13 +4,13 @@ namespace HawkBundle; -use GuzzleHttp\Client; use Hawk\Addons\Headers; use Hawk\EventPayloadBuilder; +use Hawk\Handler; use Hawk\Options; use Hawk\Serializer; use Hawk\StacktraceFrameBuilder; -use HawkBundle\Transport\GuzzlePromisesTransport; +use Hawk\Transport\TransportInterface; /** * Hawk PHP Catcher SDK @@ -29,10 +29,10 @@ final class Catcher private $handler; /** - * @param array $options - * @param Client $client + * @param array $options + * @param TransportInterface $transport */ - public function __construct(array $options, Client $client) + public function __construct(array $options, TransportInterface $transport) { $options = new Options($options); @@ -48,9 +48,7 @@ public function __construct(array $options, Client $client) $builder = new EventPayloadBuilder($stacktraceBuilder); $builder->registerAddon(new Headers()); - $transport = new GuzzlePromisesTransport($options->getUrl(), $client); - - $this->handler = new HawkHandler($options, $transport, $builder); + $this->handler = new Handler($options, $transport, $builder); $this->handler->registerErrorHandler(); $this->handler->registerExceptionHandler(); @@ -64,7 +62,7 @@ public function __construct(array $options, Client $client) */ public function setUser(array $user): self { - $this->handler->withUser($user); + $this->handler->setUser($user); return $this; } @@ -76,7 +74,7 @@ public function setUser(array $user): self */ public function setContext(array $context): self { - $this->handler->withContext($context); + $this->handler->setContext($context); return $this; } diff --git a/src/DependencyInjection/HawkExtension.php b/src/DependencyInjection/HawkExtension.php index 57bebee..bb82d2b 100644 --- a/src/DependencyInjection/HawkExtension.php +++ b/src/DependencyInjection/HawkExtension.php @@ -7,6 +7,7 @@ use GuzzleHttp\Client; use HawkBundle\Catcher; use HawkBundle\Monolog\Handler; +use HawkBundle\Transport\GuzzlePromisesTransport; use Monolog\Logger; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -31,10 +32,14 @@ public function load(array $configs, ContainerBuilder $container) // Set parameters and register services $container->setParameter('hawk.integration_token', $config['integration_token']); + // Register TransportInterface + $container->register(GuzzlePromisesTransport::class) + ->setArgument('$client', new Reference(Client::class)); + // Register Catcher $container->register(Catcher::class) ->setArgument('$options', ['integrationToken' => $config['integration_token']]) - ->setArgument('$client', new Reference(Client::class)); + ->setArgument('$transport', new Reference(GuzzlePromisesTransport::class)); // Register Monolog\Handler $container->register(Handler::class) diff --git a/src/Exception/SilencedErrorException.php b/src/Exception/SilencedErrorException.php deleted file mode 100644 index d8e842a..0000000 --- a/src/Exception/SilencedErrorException.php +++ /dev/null @@ -1,9 +0,0 @@ - 'Deprecated', - \E_USER_DEPRECATED => 'User Deprecated', - \E_NOTICE => 'Notice', - \E_USER_NOTICE => 'User Notice', - \E_STRICT => 'Runtime Notice', - \E_WARNING => 'Warning', - \E_USER_WARNING => 'User Warning', - \E_COMPILE_WARNING => 'Compile Warning', - \E_CORE_WARNING => 'Core Warning', - \E_USER_ERROR => 'User Error', - \E_RECOVERABLE_ERROR => 'Catchable Fatal Error', - \E_COMPILE_ERROR => 'Compile Error', - \E_PARSE => 'Parse Error', - \E_ERROR => 'Error', - \E_CORE_ERROR => 'Core Error', - ]; - - public function __construct( - Options $options, - TransportInterface $transport, - EventPayloadBuilder $eventPayloadBuilder - ) { - $this->options = $options; - $this->transport = $transport; - $this->eventPayloadBuilder = $eventPayloadBuilder; - } - - /** - * Attach user data for event logging. - * - * @param array $user - */ - public function setUser(array $user): void - { - $this->user = $user; - } - - /** - * Attach contextual data to provide more details about the event. - * - * @param array $context - */ - public function setContext(array $context): void - { - $this->context = $context; - } - - /** - * Register the error handler once to handle PHP errors. - */ - public function registerErrorHandler(): self - { - if ($this->isErrorHandlerRegistered) { - return $this; - } - - $errorHandlerCallback = \Closure::fromCallable([$this, 'handleError']); - - $this->previousErrorHandler = set_error_handler($errorHandlerCallback); - if (null === $this->previousErrorHandler) { - restore_error_handler(); - set_error_handler($errorHandlerCallback, $this->options->getErrorTypes()); - } - - $this->isErrorHandlerRegistered = true; - - return $this; - } - - /** - * Register the exception handler once to manage uncaught exceptions. - */ - public function registerExceptionHandler(): self - { - if ($this->isExceptionHandlerRegistered) { - return $this; - } - - $exceptionHandlerCallback = \Closure::fromCallable([$this, 'handleException']); - - $this->previousExceptionHandler = set_exception_handler($exceptionHandlerCallback); - $this->isExceptionHandlerRegistered = true; - - return $this; - } - - /** - * Register the fatal error handler to catch shutdown errors. - */ - public function registerFatalHandler(): self - { - if ($this->isFatalHandlerRegistered) { - return $this; - } - - register_shutdown_function(\Closure::fromCallable([$this, 'handleFatal'])); - $this->isFatalHandlerRegistered = true; - - return $this; - } - - /** - * Handle PHP errors, convert them to exceptions, and send the event. - */ - public function handleError(int $level, string $message, string $file, int $line): bool - { - $isSilencedError = 0 === error_reporting(); - - if (\PHP_MAJOR_VERSION >= 8) { - // Detect if the error was silenced in PHP 8+ - $isSilencedError = 0 === (error_reporting() & ~self::PHP8_FATAL_ERRORS); - - if ($level === (self::PHP8_FATAL_ERRORS & $level)) { - $isSilencedError = false; - } - } - - if ($isSilencedError) { - $exception = new SilencedErrorException(self::ERROR_LEVEL_DESCRIPTIONS[$level] . ': ' . $message, 0, $level, $file, $line); - } else { - $exception = new \ErrorException(self::ERROR_LEVEL_DESCRIPTIONS[$level] . ': ' . $message, 0, $level, $file, $line); - } - - $data = [ - 'exception' => $exception, - 'context' => $this->context, - 'user' => $this->user, - 'type' => $exception->getSeverity() - ]; - - $eventPayload = $this->eventPayloadBuilder->create($data); - $event = $this->buildEvent($eventPayload); - - if ($event !== null) { - $this->send($event); - - return false !== ($this->previousErrorHandler)($level, $message, $file, $line); - } - - return false; - } - - /** - * Handle uncaught exceptions and send the event. - */ - public function handleException(\Throwable $exception, array $context = []): void - { - $data = [ - 'exception' => $exception, - 'context' => array_merge($this->context, $context), - 'user' => $this->user - ]; - - $eventPayload = $this->eventPayloadBuilder->create($data); - $event = $this->buildEvent($eventPayload); - - if ($event !== null) { - $this->send($event); - } - - $previousExceptionHandlerException = $exception; - - $previousExceptionHandler = $this->previousExceptionHandler; - $this->previousExceptionHandler = null; - - try { - if (null !== $previousExceptionHandler) { - $previousExceptionHandler($exception); - - return; - } - } catch (\Throwable $previousExceptionHandlerException) { - // This `catch` statement is here to forcefully override the - // $previousExceptionHandlerException variable with the exception - // we just caught - } - - // If the instance of the exception we're handling is the same as the one - // caught from the previous exception handler then we give it back to the - // native PHP handler to prevent an infinite loop - if ($exception === $previousExceptionHandlerException) { - // Disable the fatal error handler or the error will be reported twice - $this->disableFatalErrorHandler = true; - - throw $exception; - } - - $this->handleException($previousExceptionHandlerException); - } - - /** - * Handle fatal errors that occur during script shutdown. - */ - public function handleFatal(): void - { - if ($this->disableFatalErrorHandler) { - return; - } - - $error = error_get_last(); - - if ( - $error === null - || is_array($error) && $error['type'] && (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING) - ) { - return; - } - - $payload = [ - 'exception' => new ErrorException( - $error['message'], - 0, - $error['type'], - $error['file'], - $error['line'] - ), - 'context' => $this->context, - 'user' => $this->user - ]; - - $eventPayload = $this->eventPayloadBuilder->create($payload); - $event = $this->buildEvent($eventPayload); - - if ($event !== null) { - $this->send($event); - } - } - - /** - * Prepare the event for sending by applying release information and optional modifications. - */ - public function sendEvent(array $payload): void - { - $payload['context'] = array_merge($this->context, $payload['context'] ?? []); - $payload['user'] = $this->user; - - $eventPayload = $this->eventPayloadBuilder->create($payload); - $event = $this->buildEvent($eventPayload); - - if ($event !== null) { - $this->send($event); - } - } - - /** - * Prepare the event for sending by applying release information and optional modifications. - */ - public function buildEvent(EventPayload $eventPayload): ?Event - { - $eventPayload->setRelease($this->options->getRelease()); - $beforeSendCallback = $this->options->getBeforeSend(); - if ($beforeSendCallback) { - $eventPayload = $beforeSendCallback($eventPayload); - if ($eventPayload === null) { - return null; - } - } - $event = new Event( - $this->options->getIntegrationToken(), - $eventPayload - ); - - return $event; - } - - /** - * Send the event to the remote server. - */ - private function send(Event $event): void - { - $this->transport->send($event); - } -} diff --git a/src/Monolog/Handler.php b/src/Monolog/Handler.php index 7f5a5e9..e4dee5a 100644 --- a/src/Monolog/Handler.php +++ b/src/Monolog/Handler.php @@ -9,8 +9,10 @@ use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; -class Handler extends AbstractProcessingHandler +final class Handler extends AbstractProcessingHandler { + use HandlerTrait; + private $catcher; public function __construct(Catcher $catcher, $level = Logger::ERROR, bool $bubble = true) @@ -20,10 +22,7 @@ public function __construct(Catcher $catcher, $level = Logger::ERROR, bool $bubb $this->catcher = $catcher; } - /** - * @inheritDoc - */ - public function write(array $record): void + public function doWrite($record): void { $data = [ 'level' => $record['level'], diff --git a/src/Monolog/HandlerTrait.php b/src/Monolog/HandlerTrait.php new file mode 100644 index 0000000..f6e4514 --- /dev/null +++ b/src/Monolog/HandlerTrait.php @@ -0,0 +1,34 @@ += 3) { + trait HandlerTrait + { + /** + * @param array|LogRecord $record + */ + abstract protected function doWrite($record): void; + + protected function write(LogRecord $record): void + { + $this->doWrite($record); + } + } +} else { + trait HandlerTrait + { + /** + * @param array|LogRecord $record + */ + abstract protected function doWrite($record): void; + + protected function write(array $record): void + { + $this->doWrite($record); + } + } +} diff --git a/src/Transport/GuzzlePromisesTransport.php b/src/Transport/GuzzlePromisesTransport.php index b191d84..187fdf0 100644 --- a/src/Transport/GuzzlePromisesTransport.php +++ b/src/Transport/GuzzlePromisesTransport.php @@ -7,17 +7,11 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use Hawk\Event; +use Hawk\Options; use Hawk\Transport\TransportInterface; class GuzzlePromisesTransport implements TransportInterface { - /** - * URL to send occurred event - * - * @var string - */ - private $url; - /** * Guzzle Client * @@ -28,11 +22,10 @@ class GuzzlePromisesTransport implements TransportInterface /** * CurlTransport constructor. * - * @param string $url + * @param Client $client */ - public function __construct(string $url, Client $client) + public function __construct(Client $client) { - $this->url = $url; $this->client = $client; } @@ -41,15 +34,15 @@ public function __construct(string $url, Client $client) */ public function getUrl(): string { - return $this->url; + return (new Options())->getUrl(); } /** * @inheritDoc */ - public function send(Event $event): mixed + public function send(Event $event) { - $promise = $this->client->postAsync($this->url, [ + $promise = $this->client->postAsync($this->getUrl(), [ 'json' => $event->jsonSerialize(), 'headers' => [ 'Content-Type' => 'application/json'