You are here

class Raven in Raven: Sentry Integration 3.x

Same name in this branch
  1. 3.x src/Logger/Raven.php \Drupal\raven\Logger\Raven
  2. 3.x src/Plugin/CspReportingHandler/Raven.php \Drupal\raven\Plugin\CspReportingHandler\Raven
Same name and namespace in other branches
  1. 8.2 src/Logger/Raven.php \Drupal\raven\Logger\Raven
  2. 8 src/Logger/Raven.php \Drupal\raven\Logger\Raven

Logs events to Sentry.

Hierarchy

Expanded class hierarchy of Raven

3 files declare their use of Raven
ProxyConfigTest.php in tests/src/Unit/ProxyConfigTest.php
RavenCommands.php in src/Commands/RavenCommands.php
RequestSubscriber.php in src/EventSubscriber/RequestSubscriber.php
1 string reference to 'Raven'
raven.services.yml in ./raven.services.yml
raven.services.yml
1 service uses Raven
logger.raven in ./raven.services.yml
Drupal\raven\Logger\Raven

File

src/Logger/Raven.php, line 45

Namespace

Drupal\raven\Logger
View source
class Raven implements LoggerInterface {
  use DependencySerializationTrait;
  use RfcLoggerTrait;

  /**
   * Config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Current user.
   *
   * @var \Drupal\Core\Session\AccountInterface|null
   */
  protected $currentUser;

  /**
   * Environment.
   *
   * @var string
   */
  protected $environment;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The message's placeholders parser.
   *
   * @var \Drupal\Core\Logger\LogMessageParserInterface
   */
  protected $parser;

  /**
   * Request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack|null
   */
  protected $requestStack;

  /**
   * The settings array.
   *
   * @var \Drupal\Core\Site\Settings
   */
  protected $settings;

  /**
   * Constructs a Raven log object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory object.
   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
   *   The parser to use when extracting message variables.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param string $environment
   *   The kernel.environment parameter.
   * @param \Drupal\Core\Session\AccountInterface|null $current_user
   *   The current user (optional).
   * @param \Symfony\Component\HttpFoundation\RequestStack|null $request_stack
   *   The request stack (optional).
   * @param \Drupal\Core\Site\Settings $settings
   *   The settings array (optional).
   */
  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();

    // We cannot lazily initialize Sentry, because we want the scope to be
    // immediately available for adding context, etc.
    $this
      ->getClient();

    // Add Drush console error event listener.
    if (function_exists('drush_main') && $config
      ->get('drush_error_handler') && method_exists(Drush::class, 'service')) {
      Drush::service('eventDispatcher')
        ->addListener(ConsoleEvents::ERROR, [
        $this,
        'onConsoleError',
      ]);
    }
  }

  /**
   * Returns existing or new Sentry client, or NULL if it could not be created.
   */
  public function getClient($force_new = FALSE) : ?ClientInterface {

    // Gracefully handle missing or wrong version of Sentry SDK.
    if (!class_exists(SentrySdk::class) || !method_exists(Event::class, 'createEvent')) {
      return NULL;
    }

    // Is the client already initialized?
    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');
    }

    // Proxy configuration (DSN is null before install).
    $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'] : [];

        // No need to configure proxy if Sentry host is on proxy bypass list.
        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;
    }

    // Set default user context.
    \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();
  }

  /**
   * {@inheritdoc}
   */
  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);

        // Remove any logger stack frames.
        $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);
    }

    // Allow modules to alter or ignore this message.
    $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);
      }
    }

    // Record a breadcrumb.
    $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']));
    }
  }

  /**
   * Sends all unsent events.
   *
   * Call this method periodically if you have a long-running script or are
   * processing a large set of data which may generate errors.
   */
  public function flush() {
    if ($client = $this
      ->getClient()) {
      $client
        ->flush();
    }
  }

  /**
   * Captures console error events.
   */
  public function onConsoleError(ConsoleErrorEvent $event) {
    if ($this
      ->getClient()) {
      \Sentry\captureException($event
        ->getError());
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
Raven::$configFactory protected property Config factory.
Raven::$currentUser protected property Current user.
Raven::$environment protected property Environment.
Raven::$moduleHandler protected property The module handler.
Raven::$parser protected property The message's placeholders parser.
Raven::$requestStack protected property Request stack.
Raven::$settings protected property The settings array.
Raven::flush public function Sends all unsent events.
Raven::getClient public function Returns existing or new Sentry client, or NULL if it could not be created.
Raven::log public function Overrides RfcLoggerTrait::log
Raven::onConsoleError public function Captures console error events.
Raven::__construct public function Constructs a Raven log object.
RfcLoggerTrait::alert public function
RfcLoggerTrait::critical public function
RfcLoggerTrait::debug public function
RfcLoggerTrait::emergency public function
RfcLoggerTrait::error public function
RfcLoggerTrait::info public function
RfcLoggerTrait::notice public function
RfcLoggerTrait::warning public function