gitlab-org--gitlab-foss/lib/gitlab/error_tracking.rb

205 lines
7.4 KiB
Ruby

# frozen_string_literal: true
module Gitlab
module ErrorTracking
# Exceptions in this group will receive custom Sentry fingerprinting
CUSTOM_FINGERPRINTING = %w[
Acme::Client::Error::BadNonce
Acme::Client::Error::NotFound
Acme::Client::Error::RateLimited
Acme::Client::Error::Timeout
Acme::Client::Error::UnsupportedOperation
ActiveRecord::ConnectionTimeoutError
Gitlab::RequestContext::RequestDeadlineExceeded
GRPC::DeadlineExceeded
JIRA::HTTPError
Rack::Timeout::RequestTimeoutException
].freeze
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)
config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor
# Sanitize authentication headers
config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = extra_tags_from_env.merge(program: Gitlab.process_name)
config.before_send = method(:before_send)
yield config if block_given?
end
end
def with_context(current_user = nil)
last_user_context = Raven.context.user
user_context = {
id: current_user&.id,
email: current_user&.email,
username: current_user&.username
}.compact
Raven.tags_context(default_tags)
Raven.user_context(user_context)
yield
ensure
Raven.user_context(last_user_context)
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)
raise exception
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)
raise exception if should_raise_for_dev?
end
# 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
# 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 before_send(event, hint)
event = add_context_from_exception_type(event, hint)
event = custom_fingerprinting(event, hint)
event
end
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
extra = sanitize_request_parameters(extra)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
end
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::ErrorTracking::Logger.error(log_hash)
end
end
def sanitize_request_parameters(parameters)
filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
filter.filter(parameters)
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
# Static tags that are set on application start
def extra_tags_from_env
Gitlab::Json.parse(ENV.fetch('GITLAB_SENTRY_EXTRA_TAGS', '{}')).to_hash
rescue => e
Gitlab::AppLogger.debug("GITLAB_SENTRY_EXTRA_TAGS could not be parsed as JSON: #{e.class.name}: #{e.message}")
{}
end
# Debugging for https://gitlab.com/gitlab-org/gitlab-foss/issues/57727
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)
.transform_values { |v| v.map(&:first) }
event.extra.merge!(columns_hash)
end
event
end
# Group common, mostly non-actionable exceptions by type and message,
# rather than cause
def custom_fingerprinting(event, hint)
ex = hint[:exception]
return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)
event.fingerprint = [ex.class.name, ex.message]
event
end
end
end
end