diff options
Diffstat (limited to 'lib/gitlab/sentry.rb')
-rw-r--r-- | lib/gitlab/sentry.rb | 193 |
1 files changed, 135 insertions, 58 deletions
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 005cb3112b8..aa8f6fa12b1 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -2,76 +2,153 @@ module Gitlab module Sentry - def self.enabled? - (Rails.env.production? || Rails.env.development?) && - Gitlab.config.sentry.enabled - end + class << self + def configure + Raven.configure do |config| + config.dsn = sentry_dsn + config.release = Gitlab.revision + config.current_environment = Gitlab.config.sentry.environment + + # Sanitize fields based on those sanitized from Rails. + config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) + # Sanitize authentication headers + config.sanitize_http_headers = %w[Authorization Private-Token] + config.tags = { program: Gitlab.process_name } + # Debugging for https://gitlab.com/gitlab-org/gitlab-foss/issues/57727 + config.before_send = method(:add_context_from_exception_type) + end + end + + def with_context(current_user = nil) + last_user_context = Raven.context.user - def self.context(current_user = nil) - return unless enabled? + user_context = { + id: current_user&.id, + email: current_user&.email, + username: current_user&.username + }.compact - Raven.tags_context(default_tags) + Raven.tags_context(default_tags) + Raven.user_context(user_context) - if current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username - ) + yield + ensure + Raven.user_context(last_user_context) end - end - # This can be used for investigating exceptions that can be recovered from in - # code. The exception will still be raised in development and test - # environments. - # - # That way we can track down these exceptions with as much information as we - # need to resolve them. - # - # Provide an issue URL for follow up. - def self.track_exception(exception, issue_url: nil, extra: {}) - track_acceptable_exception(exception, issue_url: issue_url, extra: extra) - - raise exception if should_raise_for_dev? - end + # This should be used when you want to passthrough exception handling: + # rescue and raise to be catched in upper layers of the application. + # + # If the exception implements the method `sentry_extra_data` and that method + # returns a Hash, then the return value of that method will be merged into + # `extra`. Exceptions can use this mechanism to provide structured data + # to sentry in addition to their message and back-trace. + def track_and_raise_exception(exception, extra = {}) + process_exception(exception, sentry: true, extra: extra) - # This should be used when you do not want to raise an exception in - # development and test. If you need development and test to behave - # just the same as production you can use this instead of - # track_exception. - # - # If the exception implements the method `sentry_extra_data` and that method - # returns a Hash, then the return value of that method will be merged into - # `extra`. Exceptions can use this mechanism to provide structured data - # to sentry in addition to their message and back-trace. - def self.track_acceptable_exception(exception, issue_url: nil, extra: {}) - if enabled? - extra = build_extra_data(exception, issue_url, extra) - context # Make sure we've set everything we know in the context - - Raven.capture_exception(exception, tags: default_tags, extra: extra) + raise exception end - end - def self.should_raise_for_dev? - Rails.env.development? || Rails.env.test? - end + # This can be used for investigating exceptions that can be recovered from in + # code. The exception will still be raised in development and test + # environments. + # + # That way we can track down these exceptions with as much information as we + # need to resolve them. + # + # If the exception implements the method `sentry_extra_data` and that method + # returns a Hash, then the return value of that method will be merged into + # `extra`. Exceptions can use this mechanism to provide structured data + # to sentry in addition to their message and back-trace. + # + # Provide an issue URL for follow up. + # as `issue_url: 'http://gitlab.com/gitlab-org/gitlab/issues/111'` + def track_and_raise_for_dev_exception(exception, extra = {}) + process_exception(exception, sentry: true, extra: extra) - def self.default_tags - { - Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id, - locale: I18n.locale - } - end + raise exception if should_raise_for_dev? + end - def self.build_extra_data(exception, issue_url, extra) - exception.try(:sentry_extra_data)&.tap do |data| - extra.merge!(data) if data.is_a?(Hash) + # This should be used when you only want to track the exception. + # + # If the exception implements the method `sentry_extra_data` and that method + # returns a Hash, then the return value of that method will be merged into + # `extra`. Exceptions can use this mechanism to provide structured data + # to sentry in addition to their message and back-trace. + def track_exception(exception, extra = {}) + process_exception(exception, sentry: true, extra: extra) end - extra.merge({ issue_url: issue_url }.compact) - end + # This should be used when you only want to log the exception, + # but not send it to Sentry. + # + # If the exception implements the method `sentry_extra_data` and that method + # returns a Hash, then the return value of that method will be merged into + # `extra`. Exceptions can use this mechanism to provide structured data + # to sentry in addition to their message and back-trace. + def log_exception(exception, extra = {}) + process_exception(exception, extra: extra) + end + + private + + def process_exception(exception, sentry: false, logging: true, extra:) + exception.try(:sentry_extra_data)&.tap do |data| + extra = extra.merge(data) if data.is_a?(Hash) + end + + if sentry && Raven.configuration.server + Raven.capture_exception(exception, tags: default_tags, extra: extra) + end - private_class_method :build_extra_data + if logging + # TODO: this logic could migrate into `Gitlab::ExceptionLogFormatter` + # and we could also flatten deep nested hashes if required for search + # (e.g. if `extra` includes hash of hashes). + # In the current implementation, we don't flatten multi-level folded hashes. + log_hash = {} + Raven.context.tags.each { |name, value| log_hash["tags.#{name}"] = value } + Raven.context.user.each { |name, value| log_hash["user.#{name}"] = value } + Raven.context.extra.merge(extra).each { |name, value| log_hash["extra.#{name}"] = value } + + Gitlab::ExceptionLogFormatter.format!(exception, log_hash) + + Gitlab::Sentry::Logger.error(log_hash) + end + end + + def sentry_dsn + return unless Rails.env.production? || Rails.env.development? + return unless Gitlab.config.sentry.enabled + + Gitlab.config.sentry.dsn + end + + def should_raise_for_dev? + Rails.env.development? || Rails.env.test? + end + + def default_tags + { + Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id, + locale: I18n.locale + } + end + + def add_context_from_exception_type(event, hint) + if ActiveModel::MissingAttributeError === hint[:exception] + columns_hash = ActiveRecord::Base + .connection + .schema_cache + .instance_variable_get(:@columns_hash) + .map { |k, v| [k, v.map(&:first)] } + .to_h + + event.extra.merge!(columns_hash) + end + + event + end + end end end |