View source
<?php
namespace Drupal\raven\Logger;
use Drupal\Component\ClassFinder\ClassFinder;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LogMessageParserInterface;
use Drupal\Core\Logger\LoggerChannel;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\raven\Exception\RateLimitException;
use Drupal\raven\Integration\RemoveExceptionFrameVarsIntegration;
use Drupal\raven\Integration\SanitizeIntegration;
use Drush\Drush;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;
use Sentry\Breadcrumb;
use Sentry\ClientInterface;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\Integration\EnvironmentIntegration;
use Sentry\Integration\FatalErrorListenerIntegration;
use Sentry\Integration\FrameContextifierIntegration;
use Sentry\Integration\RequestIntegration;
use Sentry\Integration\TransactionIntegration;
use Sentry\SentrySdk;
use Sentry\Serializer\RepresentationSerializer;
use Sentry\Severity;
use Sentry\StacktraceBuilder;
use Sentry\State\Scope;
use Sentry\Tracing\SpanContext;
use Sentry\UserDataBag;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\HttpFoundation\RequestStack;
class Raven implements LoggerInterface {
use DependencySerializationTrait;
use RfcLoggerTrait;
protected $configFactory;
protected $currentUser;
protected $environment;
protected $moduleHandler;
protected $parser;
protected $requestStack;
protected $settings;
public function __construct(ConfigFactoryInterface $config_factory, LogMessageParserInterface $parser, ModuleHandlerInterface $module_handler, $environment, AccountInterface $current_user = NULL, RequestStack $request_stack = NULL, Settings $settings = NULL) {
$this->configFactory = $config_factory;
$config = $this->configFactory
->get('raven.settings');
$this->currentUser = $current_user;
$this->requestStack = $request_stack;
$this->moduleHandler = $module_handler;
$this->parser = $parser;
$this->environment = $config
->get('environment') ?: $environment;
$this->settings = $settings ?: Settings::getInstance();
$this
->getClient();
if (function_exists('drush_main') && $config
->get('drush_error_handler') && method_exists(Drush::class, 'service')) {
Drush::service('eventDispatcher')
->addListener(ConsoleEvents::ERROR, [
$this,
'onConsoleError',
]);
}
}
public function getClient($force_new = FALSE) : ?ClientInterface {
if (!class_exists(SentrySdk::class) || !method_exists(Event::class, 'createEvent')) {
return NULL;
}
if (!$force_new && ($client = SentrySdk::getCurrentHub()
->getClient())) {
return $client;
}
$config = $this->configFactory
->get('raven.settings');
$options = [
'default_integrations' => FALSE,
'dsn' => empty($_SERVER['SENTRY_DSN']) ? $config
->get('client_key') : $_SERVER['SENTRY_DSN'],
'environment' => empty($_SERVER['SENTRY_ENVIRONMENT']) ? $this->environment : $_SERVER['SENTRY_ENVIRONMENT'],
];
if ($config
->get('stack')) {
$options['attach_stacktrace'] = TRUE;
}
if ($config
->get('fatal_error_handler')) {
$options['integrations'][] = new FatalErrorListenerIntegration();
}
$options['integrations'][] = new RequestIntegration();
$options['integrations'][] = new TransactionIntegration();
$options['integrations'][] = new FrameContextifierIntegration();
$options['integrations'][] = new EnvironmentIntegration();
$options['integrations'][] = new SanitizeIntegration();
if (!$config
->get('trace')) {
$options['integrations'][] = new RemoveExceptionFrameVarsIntegration();
}
if (!empty($_SERVER['SENTRY_RELEASE'])) {
$options['release'] = $_SERVER['SENTRY_RELEASE'];
}
elseif (!empty($config
->get('release'))) {
$options['release'] = $config
->get('release');
}
if (!$config
->get('send_request_body')) {
$options['max_request_body_size'] = 'none';
}
if ($config
->get('traces_sample_rate')) {
$options['traces_sample_rate'] = $config
->get('traces_sample_rate');
}
$parsed_dsn = parse_url($options['dsn'] ?? '');
if (!empty($parsed_dsn['host']) && !empty($parsed_dsn['scheme'])) {
$http_client_config = $this->settings
->get('http_client_config', []);
if (!empty($http_client_config['proxy'][$parsed_dsn['scheme']])) {
$no_proxy = isset($http_client_config['proxy']['no']) ? $http_client_config['proxy']['no'] : [];
if (!in_array($parsed_dsn['host'], $no_proxy, TRUE)) {
$options['http_proxy'] = $http_client_config['proxy'][$parsed_dsn['scheme']];
}
}
}
$this->moduleHandler
->alter('raven_options', $options);
try {
\Sentry\init($options);
} catch (\InvalidArgumentException $e) {
return NULL;
}
\Sentry\configureScope(function (Scope $scope) use ($config) : void {
$user = [
'id' => $this->currentUser ? $this->currentUser
->id() : 0,
];
if ($this->requestStack && ($request = $this->requestStack
->getCurrentRequest())) {
$user['ip_address'] = $request
->getClientIp();
}
if ($this->currentUser && $config
->get('send_user_data')) {
$user['email'] = $this->currentUser
->getEmail();
$user['username'] = $this->currentUser
->getAccountName();
}
$scope
->setUser($user);
});
return SentrySdk::getCurrentHub()
->getClient();
}
public function log($level, $message, array $context = []) {
static $counter = 0;
$client = $this
->getClient();
if (!$client) {
return;
}
$config = $this->configFactory
->get('raven.settings');
$event = Event::createEvent();
$levels = [
RfcLogLevel::EMERGENCY => Severity::FATAL,
RfcLogLevel::ALERT => Severity::FATAL,
RfcLogLevel::CRITICAL => Severity::FATAL,
RfcLogLevel::ERROR => Severity::ERROR,
RfcLogLevel::WARNING => Severity::WARNING,
RfcLogLevel::NOTICE => Severity::INFO,
RfcLogLevel::INFO => Severity::INFO,
RfcLogLevel::DEBUG => Severity::DEBUG,
];
$event
->setLevel(new Severity($levels[$level]));
$message_placeholders = $this->parser
->parseMessagePlaceholders($message, $context);
$formatted_message = empty($message_placeholders) ? $message : strtr($message, $message_placeholders);
$event
->setMessage($message, $message_placeholders, $formatted_message);
$event
->setTimestamp($context['timestamp']);
$event
->setLogger($context['channel']);
$extra = [
'request_uri' => $context['request_uri'],
];
if ($context['referer']) {
$extra['referer'] = $context['referer'];
}
if ($context['link']) {
$extra['link'] = MailFormatHelper::htmlToText($context['link']);
}
$event
->setExtra($extra);
$user = UserDataBag::createFromUserIdentifier($context['uid']);
$user
->setIpAddress($context['ip']);
if ($this->currentUser && $this->currentUser
->id() == $context['uid'] && $config
->get('send_user_data')) {
$user
->setEmail($this->currentUser
->getEmail());
$user
->setUsername($this->currentUser
->getAccountName());
}
$event
->setUser($user);
if ($client
->getOptions()
->shouldAttachStacktrace()) {
if (isset($context['backtrace'])) {
$backtrace = $context['backtrace'];
if (!$config
->get('trace')) {
foreach ($backtrace as &$frame) {
unset($frame['args']);
}
}
}
else {
$backtrace = debug_backtrace($config
->get('trace') ? 0 : DEBUG_BACKTRACE_IGNORE_ARGS);
$finder = new ClassFinder();
if ($backtrace[0]['file'] === realpath($finder
->findFile(LoggerChannel::class))) {
array_shift($backtrace);
if ($backtrace[0]['file'] === realpath($finder
->findFile(LoggerTrait::class))) {
array_shift($backtrace);
}
}
}
$stacktraceBuilder = new StacktraceBuilder($client
->getOptions(), new RepresentationSerializer($client
->getOptions()));
$stacktrace = $stacktraceBuilder
->buildFromBacktrace($backtrace, '', 0);
$stacktrace
->removeFrame(count($stacktrace
->getFrames()) - 1);
$event
->setStacktrace($stacktrace);
}
$filter = [
'level' => $level,
'message' => $message,
'context' => $context,
'event' => $event,
'client' => $client,
'process' => !empty($config
->get('log_levels')[$level + 1]),
];
if (in_array($context['channel'], $config
->get('ignored_channels') ?: [])) {
$filter['process'] = FALSE;
}
$this->moduleHandler
->alter('raven_filter', $filter);
if (!empty($filter['process'])) {
$eventHint['extra'] = [
'level' => $level,
'message' => $message,
'context' => $context,
];
if (isset($stacktrace)) {
$eventHint['stacktrace'] = $stacktrace;
}
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$eventHint['exception'] = $context['exception'];
}
$start = microtime(TRUE);
$rateLimit = $config
->get('rate_limit');
if (!$rateLimit || $counter < $rateLimit) {
\Sentry\captureEvent($event, EventHint::fromArray($eventHint));
}
elseif ($counter == $rateLimit) {
\Sentry\captureException(new RateLimitException('Log event discarded due to rate limit exceeded; future log events will not be captured by Sentry.'));
}
$counter++;
if ($parent = SentrySdk::getCurrentHub()
->getSpan()) {
$span = new SpanContext();
$span
->setOp('sentry.capture');
$span
->setDescription($context['channel'] . ': ' . $formatted_message);
$span
->setStartTimestamp($start);
$span
->setEndTimestamp(microtime(TRUE));
$parent
->startChild($span);
}
}
$breadcrumb = [
'level' => $level,
'message' => $message,
'context' => $context,
'process' => TRUE,
'breadcrumb' => [
'category' => $context['channel'],
'message' => isset($formatted_message) ? (string) $formatted_message : NULL,
'level' => $levels[$level],
],
];
foreach ([
'%line',
'%file',
'%type',
'%function',
] as $key) {
if (isset($context[$key])) {
$breadcrumb['breadcrumb']['data'][substr($key, 1)] = $context[$key];
}
}
$this->moduleHandler
->alter('raven_breadcrumb', $breadcrumb);
if (!empty($breadcrumb['process'])) {
\Sentry\addBreadcrumb(Breadcrumb::fromArray($breadcrumb['breadcrumb']));
}
}
public function flush() {
if ($client = $this
->getClient()) {
$client
->flush();
}
}
public function onConsoleError(ConsoleErrorEvent $event) {
if ($this
->getClient()) {
\Sentry\captureException($event
->getError());
}
}
}