diff --git a/_config/cache.yml b/_config/cache.yml new file mode 100644 index 0000000..263cb75 --- /dev/null +++ b/_config/cache.yml @@ -0,0 +1,8 @@ +--- +Name: raygun-cache +--- +SilverStripe\Core\Injector\Injector: + Psr\SimpleCache\CacheInterface.raygunCache: + factory: SilverStripe\Core\Cache\CacheFactory + constructor: + namespace: "raygunCache" diff --git a/composer.json b/composer.json index 7db0903..f5402b4 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,8 @@ ], "require": { "php": ">=7.1", - "mindscape/raygun4php": "^1", - "silverstripe/framework": "^4.3", - "graze/monolog-extensions": "^2" + "mindscape/raygun4php": "^1 || ^2", + "silverstripe/framework": "^4.3" }, "autoload": { "psr-4": { diff --git a/src/RaygunClientFactory.php b/src/RaygunClientFactory.php index e5cb947..f4a35fb 100644 --- a/src/RaygunClientFactory.php +++ b/src/RaygunClientFactory.php @@ -2,13 +2,20 @@ namespace SilverStripe\Raygun; +use GuzzleHttp\Client; +use LogicException; +use Psr\SimpleCache\CacheInterface; +use Raygun4php\RaygunClient; +use Raygun4php\Transports\GuzzleAsync; +use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Injector\Factory; use SilverStripe\Core\Environment; -use SilverStripe\Control\Director; -use Raygun4php\RaygunClient; +use SilverStripe\Core\Flushable; +use SilverStripe\Core\Injector\Factory; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Path; -class RaygunClientFactory implements Factory +class RaygunClientFactory implements Factory, Flushable { use CustomAppKeyProvider; @@ -34,19 +41,35 @@ public function create($service, array $params = []) { // extract api key from .env file $apiKey = $this->getCustomRaygunAppKey() ?? (string) Environment::getEnv(self::RAYGUN_APP_KEY_NAME); + + // log error to warn user that exceptions will not be logged to Raygun + if (empty($apiKey) && !Director::isDev()) { + $name = self::RAYGUN_APP_KEY_NAME; + user_error("You need to set the {$name} environment variable in order to log to Raygun.", E_USER_WARNING); + } + + // check if user tracking is enabled $disableTracking = Config::inst()->get( RaygunClient::class, 'disable_user_tracking' ); $disableTracking = is_bool($disableTracking) ? $disableTracking : false; - // log error to warn user that exceptions will not be logged to Raygun - if (empty($apiKey) && !Director::isDev()) { - $name = self::RAYGUN_APP_KEY_NAME; - user_error("You need to set the {$name} environment variable in order to log to Raygun.", E_USER_WARNING); + // Setup new client in the way that is best for the current SDK version. + if (substr(ltrim(static::getSdkVersion(), 'v'), 0, 2) === '1.') { + $this->createForV1($apiKey, $disableTracking, $params); + } else { + $this->createForV2($apiKey, $disableTracking, $params); } - // setup new client + $this->filterSensitiveData(); + + return $this->client; + } + + protected function createForV1($apiKey, $disableTracking, $params) + { + // Instantiate actual client $this->client = new RaygunClient( $apiKey, true, @@ -54,7 +77,7 @@ public function create($service, array $params = []) $disableTracking ); - // set proxy + // Set proxy if (!empty($params['proxyHost'])) { $proxy = $params['proxyHost']; if (!empty($params['proxyPort'])) { @@ -62,10 +85,36 @@ public function create($service, array $params = []) } $this->client->setProxy($proxy); } + } - $this->filterSensitiveData(); + protected function createForV2($apiKey, $disableTracking, $params) + { + // Prepare transport config. + $transportConfig = [ + 'base_uri' => 'https://api.raygun.com', + 'timeout' => 2.0, + 'headers' => [ + 'X-ApiKey' => $apiKey, + ], + ]; - return $this->client; + // Set proxy + if (!empty($params['proxyHost'])) { + $proxy = $params['proxyHost']; + if (!empty($params['proxyPort'])) { + $proxy .= ':' . $params['proxyPort']; + } + $transportConfig['proxy'] = $proxy; + } + + // Create raygun client using async transport. + $transport = new GuzzleAsync( + new Client($transportConfig) + ); + $this->client = new RaygunClient($transport, $disableTracking); + + // Ensure asynchronous requests are given time to finish. + register_shutdown_function([$transport, 'wait']); } protected function filterSensitiveData() @@ -86,4 +135,42 @@ protected function filterSensitiveData() 'Cookie' => true, ]); } + + /** + * Get the currently installed version of the raygun4php package according to composer.lock + * + * @return string + */ + public static function getSdkVersion() + { + $cache = Injector::inst()->get(CacheInterface::class . '.raygunCache'); + // If the SDK version isn't cached, get it from the composer.lock file. + // Note that this is called before flushing has occurred - if we're flushing, bypass the cache for now. + if (Director::isManifestFlushed() || !$version = $cache->get('raygun4phpVersion')) { + $composerLockRaw = file_get_contents(Path::join(Director::baseFolder(), 'composer.lock')); + if (!$composerLockRaw) { + throw new LogicException('composer.lock file is missing.'); + } + $packageList = json_decode($composerLockRaw, true)['packages']; + foreach ($packageList as $package) { + if ($package['name'] === 'mindscape/raygun4php') { + $version = $package['version']; + break; + } + } + if (!$version) { + throw new LogicException('mindscape/raygun4php not found in composer.lock'); + } + // Cache the SDK version so we don't have to do this every request. + $cache->set('raygun4phpVersion', $version); + } + + return $version; + } + + public static function flush() + { + $cache = Injector::inst()->get(CacheInterface::class . '.raygunCache'); + $cache->clear(); + } } diff --git a/src/RaygunFormatter.php b/src/RaygunFormatter.php new file mode 100644 index 0000000..c326671 --- /dev/null +++ b/src/RaygunFormatter.php @@ -0,0 +1,73 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @see http://github.com/graze/MonologExtensions/blob/master/LICENSE + * @link http://github.com/graze/MonologExtensions + */ + +namespace SilverStripe\Raygun; + +use Monolog\Formatter\NormalizerFormatter; + +class RaygunFormatter extends NormalizerFormatter +{ + /** + * {@inheritdoc} + * + * @param array $record A record to format + * + * @return mixed The formatted record + */ + public function format(array $record) + { + $record = parent::format($record); + + $record['tags'] = []; + $record['custom_data'] = []; + $record['timestamp'] = null; + + foreach (['extra', 'context'] as $source) { + if (array_key_exists('tags', $record[$source]) && is_array($record[$source]['tags'])) { + $record['tags'] = array_merge($record['tags'], $record[$source]['tags']); + } + if (array_key_exists('timestamp', $record[$source]) && is_numeric($record[$source]['timestamp'])) { + $record['timestamp'] = $record[$source]['timestamp']; + } + unset($record[$source]['tags'], $record[$source]['timestamp']); + } + + $record['custom_data'] = $record['extra']; + $record['extra'] = []; + foreach ($record['context'] as $key => $item) { + if (!in_array($key, ['file', 'line', 'exception'])) { + $record['custom_data'][$key] = $item; + unset($record['context'][$key]); + } + } + + return $record; + } +} diff --git a/src/RaygunHandler.php b/src/RaygunHandler.php index 8043045..ceea3d4 100644 --- a/src/RaygunHandler.php +++ b/src/RaygunHandler.php @@ -1,14 +1,43 @@ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @see http://github.com/graze/MonologExtensions/blob/master/LICENSE + * @link http://github.com/graze/MonologExtensions + */ namespace SilverStripe\Raygun; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; use Raygun4php\RaygunClient; use SilverStripe\Core\Config\Config; -use Graze\Monolog\Handler\RaygunHandler as MonologRaygunHandler; use SilverStripe\Core\Config\Configurable; use SilverStripe\Security\Security; -class RaygunHandler extends MonologRaygunHandler +class RaygunHandler extends AbstractProcessingHandler { use Configurable; @@ -19,21 +48,42 @@ class RaygunHandler extends MonologRaygunHandler private static $user_include_fullname = false; private static $user_include_email = false; - + private static $enabled = true; + /** + * @var RaygunClient + */ + protected $client; + + /** + * @param RaygunClient $client + * @param int $level + * @param bool $bubble + */ + public function __construct(RaygunClient $client, $level = Logger::DEBUG, $bubble = true) + { + $this->client = $client; + + parent::__construct($level, $bubble); + } + + /** + * @param array $record + */ protected function write(array $record) { + // If not enabled, don't write anything. if (!(bool)$this->config()->get('enabled')) { return; } + // Set user tracking and data. $disableTracking = Config::inst()->get( RaygunClient::class, 'disable_user_tracking' ); $disableTracking = is_bool($disableTracking) ? $disableTracking : false; - if (!$disableTracking) { $user = Security::getCurrentUser(); if ($user) { @@ -47,6 +97,67 @@ protected function write(array $record) } } - parent::write($record); + // Write exceptions and errors appropriately. + $context = $record['context']; + if (isset($context['exception']) + && ( + $context['exception'] instanceof \Exception + || (PHP_VERSION_ID > 70000 && $context['exception'] instanceof \Throwable) + ) + ) { + $this->writeException( + $record, + $record['formatted']['tags'], + $record['formatted']['custom_data'], + $record['formatted']['timestamp'] + ); + } elseif (isset($context['file']) && isset($context['line'])) { + $this->writeError( + $record['formatted'], + $record['formatted']['tags'], + $record['formatted']['custom_data'], + $record['formatted']['timestamp'] + ); + } + // do nothing if its not an exception or an error + } + + /** + * @param array $record + * @param array $tags + * @param array $customData + * @param int|float $timestamp + */ + protected function writeError(array $record, array $tags = [], array $customData = [], $timestamp = null) + { + $context = $record['context']; + $this->client->SendError( + 0, + $record['message'], + $context['file'], + $context['line'], + $tags, + $customData, + $timestamp + ); + } + + /** + * @param array $record + * @param array $tags + * @param array $customData + * @param int|float $timestamp + */ + protected function writeException(array $record, array $tags = [], array $customData = [], $timestamp = null) + { + $this->client->SendException($record['context']['exception'], $tags, $customData, $timestamp); + } + + /** + * @return \Monolog\Formatter\FormatterInterface + */ + protected function getDefaultFormatter() + { + return new RaygunFormatter(); } }