Source of file SentryLogger.php
Size: 11,033 Bytes - Last Modified: 2021-12-23T10:09:59+00:00
/var/www/docs.ssmods.com/process/src/src/Log/SentryLogger.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 | <?php /** * Class: SentryLogger. * * @author Russell Michell 2017-2021 <russ@theruss.com> * @package phptek/sentry */ namespace PhpTek\Sentry\Log; use Composer\InstalledVersions; use Monolog\Logger; use Sentry\Frame; use Sentry\Client; use SilverStripe\Control\Director; use SilverStripe\Control\Middleware\TrustedProxyMiddleware; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Backtrace; use SilverStripe\Security\Member; use SilverStripe\Security\Security; use SilverStripe\Core\Config\Configurable; use PhpTek\Sentry\Adaptor\SentryAdaptor; /** * The SentryLogger class is a bridge between {@link SentryAdaptor} and * SilverStripe's use of Monolog. */ class SentryLogger { use Configurable; /** * @var SentryAdaptor */ public $adaptor = null; /** * Stipulates what gets shown in the Sentry UI, should some metric not be * available for any reason. * * @var string */ const SLW_NOOP = 'Unavailable'; /** * Default text to show if in self-generated stacktraces, we're unable to discern data. * * @var string */ public const DEFAULT_FRAME_VAL = 'Unknown'; /** * Factory, consumed by {@link SentryHandler}. * * @param Client $client * @param array $config An array of optional additional configuration for * passing custom information to Sentry. See the README * for more detail. * @return SentryLogger */ public static function factory(Client $client, array $config = []): SentryLogger { $logger = new static(); $env = $logger->defaultEnv(); $tags = $logger->defaultTags(); $extra = $logger->defaultExtra(); $user = []; $level = Logger::DEBUG; if ($config) { $env = $config['env'] ?? $env; $tags = array_merge($tags, $config['tags'] ?? []); $extra = array_merge($extra, $config['extra'] ?? []); $user = $config['user'] ?? $user; $level = $config['level'] ?? $level; } $adaptor = (new SentryAdaptor($client)) ->setContext('env', $env) ->setContext('tags', $tags) ->setContext('extra', $extra) ->setContext('user', $user); return $logger->setAdaptor($adaptor); } /** * @return SentryAdaptor */ public function getAdaptor(): SentryAdaptor { return $this->adaptor; } /** * @param SentryAdaptor $adaptor * @return SentryLogger */ public function setAdaptor(SentryAdaptor $adaptor): SentryLogger { $this->adaptor = $adaptor; return $this; } /** * Returns a default environment when one isn't passed to the factory() * method. * * @return string */ public function defaultEnv(): string { return Director::get_environment_type(); } /** * Returns a default set of additional "tags" we wish to send to Sentry. * By default, Sentry reports on several mertrics, and we're already sending * {@link Member} data. But there are additional data that would be useful * for debugging via the Sentry UI. * * These data can augment that which is sent to Sentry at setup * time in _config.php. See the README for more detail. * * N.b. Tags can be used to group messages within the Sentry UI itself, so there * should only be "static" data being sent, not something that can drastically * or minutely change, such as memory usage for example. * * @return array */ public function defaultTags(): array { return [ 'request.method'=> self::get_req_method(), 'request.type' => self::get_req_type(), 'php.sapi' => self::get_sapi(), 'silverstripe.framework.version' => self::get_package_info('silverstripe/framework'), 'phptek.sentry.version' => self::get_package_info('phptek/sentry'), 'app' => self::get_app_info(), ]; } /** * Returns a default set of extra data to show upon selecting a message for * analysis in the Sentry UI. This can augment the data sent to Sentry at setup * time in _config.php as well as at runtime when calling SS_Log itself. * See the README for more detail. * * @return array */ public function defaultExtra(): array { return [ 'PHP Peak Memory' => self::get_peak_memory(), ]; } /** * What sort of request is this? (A harder question to answer than you might * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request) * * @return string */ public static function get_req_type(): string { $isCLI = self::get_sapi() !== 'cli'; $isAjax = Director::is_ajax(); return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax'; } /** * Return peak memory usage. * * @return string */ public static function get_peak_memory(): string { $peak = memory_get_peak_usage(true) / 1024 / 1024; return (string) round($peak, 2) . 'Mb'; } /** * Basic User-Agent check and return. * * @return string */ public function getUserAgent(): string { return $_SERVER['HTTP_USER_AGENT'] ?? self::SLW_NOOP; } /** * Basic request method check and return. * * @return string */ public static function get_req_method(): string { return $_SERVER['REQUEST_METHOD'] ?? self::SLW_NOOP; } /** * @return string */ public static function get_sapi(): string { return php_sapi_name(); } /** * @param string $package * @return string */ public static function get_package_info(string $package): string { return InstalledVersions::getVersion(trim($package)) ?? self::SLW_NOOP; } /** * Format and return a string of metadata about the app in which this module is installed. * * @return string */ public static function get_app_info(): string { $meta = InstalledVersions::getRootPackage(); $data = [ 'project' => $meta['name'] ?? self::SLW_NOOP, 'branch' => $meta['version'] ?? self::SLW_NOOP, 'commit' => $meta['reference'] ?? self::SLW_NOOP, ]; // If we have nothing to show, then show a meaningful default $filtered = array_filter($data, function ($item): bool { return $item == self::SLW_NOOP; }); if (count($filtered) === count($data)) { return self::SLW_NOOP; } return sprintf('%s: %s (%s)', $data['project'], $data['branch'], $data['commit']); } /** * Returns the client IP address which originated this request. * Lifted and modified from SilverStripe 3's SS_HTTPRequest. * * @return string */ public static function get_ip(): string { $headerOverrideIP = null; if (defined('TRUSTED_PROXY')) { $headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ? [SS_TRUSTED_PROXY_IP_HEADER] : null; if (!$headers) { // Backwards compatible defaults $headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR']; } foreach ($headers as $header) { if (!empty($_SERVER[$header])) { $headerOverrideIP = $_SERVER[$header]; break; } } } $proxy = Injector::inst()->create(TrustedProxyMiddleware::class); if ($headerOverrideIP) { return $proxy->getIPFromHeaderValue($headerOverrideIP); } if (isset($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; } return ''; } /** * Returns a default set of additional data specific to the user's part in * the request. * * @param mixed Member|null $member * @return array */ public static function user_data(Member $member = null): array { if (!$member) { $member = Security::getCurrentUser(); } return [ 'IP Address' => self::get_ip() ?: self::SLW_NOOP, 'ID' => $member ? $member->getField('ID') : self::SLW_NOOP, 'Email' => $member ? $member->getField('Email') : self::SLW_NOOP, ]; } /** * Manually extract or generate a suitable backtrace. This is especially useful * in non-exception reports such as those that use Sentry\Client::captureMessage(). * * Note: Calling logic only makes use of this if the "custom_stacktrace" option is enabled. * As it is, it requires a little more work to make it on-par with Sentry's defaults (which is this * module's default also). * * @param array $record * @return array An array of filtered Sentry\Frame objects. */ public static function backtrace(array $record): array { if (!empty($record['context']['trace'])) { // Provided trace $bt = $record['context']['trace']; } else if (isset($record['context']['exception'])) { // Generate trace from exception $bt = $record['context']['exception']->getTrace(); } else { // Failover: build custom trace $bt = debug_backtrace(); // Push current line into context array_unshift($bt, [ 'file' => $bt['file'] ?? self::DEFAULT_FRAME_VAL, 'line' => (int) ($bt['line'] ?? 0), 'function' => '', 'class' => '', 'type' => '', 'args' => [], ]); } // Regardless of where it came from, filter the exception $filtered = Backtrace::filter_backtrace($bt, [ '', 'Monolog\\Handler\\AbstractProcessingHandler->handle', 'Monolog\\Logger->addRecord', 'Monolog\\Logger->error', 'Monolog\\Logger->log', 'Monolog\\Logger->warn', 'PhpTek\\Sentry\\Handler\\SentryHandler', 'PhpTek\\Sentry\\Handler\\SentryHandler::write', 'PhpTek\\Sentry\\Logger\\SentryLogger::backtrace', ]); return array_map(function($item) { return new Frame( $item['function'] ?? self::DEFAULT_FRAME_VAL, $item['file'] ?? self::DEFAULT_FRAME_VAL, $item['line'] ?? 0, $item['raw_function'] ?? self::DEFAULT_FRAME_VAL, $item['abs_filepath'] ?? self::DEFAULT_FRAME_VAL, $item['vars'] ?? [], $item['in_app'] ?? true ); }, $filtered); } } |