From 8cc5f2790908ba9bb8eecba2b78a3c5a88c77b90 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 13 Dec 2019 12:07:41 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- lib/gitlab/bitbucket_import/importer.rb | 9 +- lib/gitlab/bitbucket_server_import/importer.rb | 33 ++-- lib/gitlab/ci/config.rb | 8 +- lib/gitlab/ci/pipeline/chain/config/process.rb | 4 +- lib/gitlab/diff/highlight.rb | 2 +- lib/gitlab/gitaly_client.rb | 3 +- .../importer/pull_request_importer.rb | 10 +- lib/gitlab/gpg.rb | 4 +- lib/gitlab/graphql/calls_gitaly/instrumentation.rb | 2 +- .../graphql/query_analyzers/logger_analyzer.rb | 4 +- lib/gitlab/highlight.rb | 2 +- lib/gitlab/import_export/relation_tree_restorer.rb | 7 +- lib/gitlab/import_export/shared.rb | 6 +- lib/gitlab/profiler.rb | 2 + lib/gitlab/sentry.rb | 193 ++++++++++++++------- lib/gitlab/sentry/logger.rb | 11 ++ lib/gitlab/shell.rb | 6 +- lib/gitlab/usage_data_counters/base_counter.rb | 2 +- 18 files changed, 198 insertions(+), 110 deletions(-) create mode 100644 lib/gitlab/sentry/logger.rb (limited to 'lib/gitlab') diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index e01ffb631ba..5fbf7be2568 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -11,7 +11,6 @@ module Gitlab { title: 'task', color: '#7F8C8D' }].freeze attr_reader :project, :client, :errors, :users - attr_accessor :logger def initialize(project) @project = project @@ -20,7 +19,6 @@ module Gitlab @labels = {} @errors = [] @users = {} - @logger = Gitlab::Import::Logger.build end def execute @@ -47,7 +45,8 @@ module Gitlab backtrace = Gitlab::Profiler.clean_backtrace(ex.backtrace) error = { type: :pull_request, iid: pull_request.iid, errors: ex.message, trace: backtrace, raw_response: pull_request.raw } - log_error(error) + Gitlab::Sentry.log_exception(ex, error) + # Omit the details from the database to avoid blowing up usage in the error column error.delete(:trace) error.delete(:raw_response) @@ -275,10 +274,6 @@ module Gitlab author.to_s + comment.note.to_s end - def log_error(details) - logger.error(log_base_data.merge(details)) - end - def log_base_data { class: self.class.name, diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb index af50a27c47b..1a9ac65e641 100644 --- a/lib/gitlab/bitbucket_server_import/importer.rb +++ b/lib/gitlab/bitbucket_server_import/importer.rb @@ -133,7 +133,10 @@ module Gitlab log_info(stage: 'import_repository', message: 'finished import') rescue Gitlab::Shell::Error => e - log_error(stage: 'import_repository', message: 'failed import', error: e.message) + Gitlab::Sentry.log_exception( + e, + stage: 'import_repository', message: 'failed import', error: e.message + ) # Expire cache to prevent scenarios such as: # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true @@ -164,8 +167,10 @@ module Gitlab batch.each do |pull_request| import_bitbucket_pull_request(pull_request) rescue StandardError => e - backtrace = Gitlab::Profiler.clean_backtrace(e.backtrace) - log_error(stage: 'import_pull_requests', iid: pull_request.iid, error: e.message, backtrace: backtrace) + Gitlab::Sentry.log_exception( + e, + stage: 'import_pull_requests', iid: pull_request.iid, error: e.message + ) errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, backtrace: backtrace.join("\n"), raw_response: pull_request.raw } end @@ -177,7 +182,11 @@ module Gitlab client.delete_branch(project_key, repository_slug, branch.name, branch.sha) project.repository.delete_branch(branch.name) rescue BitbucketServer::Connection::ConnectionError => e - log_error(stage: 'delete_temp_branches', branch: branch.name, error: e.message) + Gitlab::Sentry.log_exception( + e, + stage: 'delete_temp_branches', branch: branch.name, error: e.message + ) + @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message } end end @@ -288,7 +297,11 @@ module Gitlab # a regular note. create_fallback_diff_note(merge_request, comment, position) rescue StandardError => e - log_error(stage: 'create_diff_note', comment_id: comment.id, error: e.message) + Gitlab::Sentry.log_exception( + e, + stage: 'create_diff_note', comment_id: comment.id, error: e.message + ) + errors << { type: :pull_request, id: comment.id, errors: e.message } nil end @@ -325,7 +338,11 @@ module Gitlab merge_request.notes.create!(pull_request_comment_attributes(replies)) end rescue StandardError => e - log_error(stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message) + Gitlab::Sentry.log_exception( + e, + stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message + ) + errors << { type: :pull_request, comment_id: comment.id, errors: e.message } end end @@ -360,10 +377,6 @@ module Gitlab logger.info(log_base_data.merge(details)) end - def log_error(details) - logger.error(log_base_data.merge(details)) - end - def log_warn(details) logger.warn(log_base_data.merge(details)) end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 9c1e6277e95..ef11b8dc530 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -67,11 +67,11 @@ module Gitlab build_config(config) rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e - track_exception(e) + track_and_raise_for_dev_exception(e) raise Config::ConfigError, e.message rescue Gitlab::Ci::Config::External::Context::TimeoutError => e - track_exception(e) + track_and_raise_for_dev_exception(e) raise Config::ConfigError, TIMEOUT_MESSAGE end @@ -94,8 +94,8 @@ module Gitlab user: user) end - def track_exception(error) - Gitlab::Sentry.track_exception(error, extra: @context.sentry_payload) + def track_and_raise_for_dev_exception(error) + Gitlab::Sentry.track_and_raise_for_dev_exception(error, @context.sentry_payload) end # Overriden in EE diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb index 731b0fdb286..feb5b4556e1 100644 --- a/lib/gitlab/ci/pipeline/chain/config/process.rb +++ b/lib/gitlab/ci/pipeline/chain/config/process.rb @@ -21,10 +21,10 @@ module Gitlab rescue Gitlab::Ci::YamlProcessor::ValidationError => ex error(ex.message, config_error: true) rescue => ex - Gitlab::Sentry.track_acceptable_exception(ex, extra: { + Gitlab::Sentry.track_exception(ex, project_id: project.id, sha: @pipeline.sha - }) + ) error("Undefined error (#{Labkit::Correlation::CorrelationId.current_id})", config_error: true) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index ca7974930af..676743dbd59 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -35,7 +35,7 @@ module Gitlab # match the blob, which is a bug. But we shouldn't fail to render # completely in that case, even though we want to report the error. rescue RangeError => e - Gitlab::Sentry.track_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/45441') + Gitlab::Sentry.track_and_raise_for_dev_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/45441') end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 373539f5516..41207e8cbde 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -67,8 +67,7 @@ module Gitlab File.read(cert_file).scan(PEM_REGEX).map do |cert| OpenSSL::X509::Certificate.new(cert).to_pem rescue OpenSSL::OpenSSLError => e - Rails.logger.error "Could not load certificate #{cert_file} #{e}" # rubocop:disable Gitlab/RailsLogger - Gitlab::Sentry.track_exception(e, extra: { cert_file: cert_file }) + Gitlab::Sentry.track_and_raise_for_dev_exception(e, cert_file: cert_file) nil end.compact end.uniq.join("\n") diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index dc15b95505a..d762ea179e3 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -91,12 +91,10 @@ module Gitlab project.repository.add_branch(project.creator, source_branch, pull_request.source_branch_sha) rescue Gitlab::Git::CommandError => e - Gitlab::Sentry.track_acceptable_exception(e, - extra: { - source_branch: source_branch, - project_id: merge_request.project.id, - merge_request_id: merge_request.id - }) + Gitlab::Sentry.track_exception(e, + source_branch: source_branch, + project_id: merge_request.project.id, + merge_request_id: merge_request.id) end end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index abe90bba19c..048534e04de 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -110,9 +110,9 @@ module Gitlab folder_contents = Dir.children(tmp_dir) # This means we left a GPG-agent process hanging. Logging the problem in # sentry will make this more visible. - Gitlab::Sentry.track_exception(e, + Gitlab::Sentry.track_and_raise_for_dev_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918', - extra: { tmp_dir: tmp_dir, contents: folder_contents }) + tmp_dir: tmp_dir, contents: folder_contents) end tmp_keychains_removed.increment unless File.exist?(tmp_dir) diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb index fbd5e348c7d..118748d5a56 100644 --- a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb +++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb @@ -32,7 +32,7 @@ module Gitlab # Will inform you if there needs to be `calls_gitaly: true` as a kwarg in the field declaration # if there is at least 1 Gitaly call involved with the field resolution. error = RuntimeError.new("Gitaly is called for field '#{type_object.name}' on #{type_object.owner.try(:name)} - please either specify a constant complexity or add `calls_gitaly: true` to the field declaration") - Gitlab::Sentry.track_exception(error) + Gitlab::Sentry.track_and_raise_for_dev_exception(error) end end end diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb index 01b55a1667f..a6eb35be427 100644 --- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb +++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb @@ -18,7 +18,7 @@ module Gitlab variables: variables }) rescue => e - Gitlab::Sentry.track_exception(e) + Gitlab::Sentry.track_and_raise_for_dev_exception(e) default_initial_values(query) end @@ -38,7 +38,7 @@ module Gitlab GraphqlLogger.info(memo.except!(:time_started, :query)) rescue => e - Gitlab::Sentry.track_exception(e) + Gitlab::Sentry.track_and_raise_for_dev_exception(e) end private diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 5663b9f20cf..fdb81e62bd7 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -61,7 +61,7 @@ module Gitlab tokens = lexer.lex(text, continue: continue) Timeout.timeout(timeout_time) { @formatter.format(tokens, tag: tag).html_safe } rescue Timeout::Error => e - Gitlab::Sentry.track_exception(e) + Gitlab::Sentry.track_and_raise_for_dev_exception(e) highlight_plain(text) rescue highlight_plain(text) diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb index 8a2b0cab915..d2d0cddfcbb 100644 --- a/lib/gitlab/import_export/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/relation_tree_restorer.rb @@ -82,11 +82,8 @@ module Gitlab end def log_import_failure(relation_key, relation_index, exception) - Gitlab::Sentry.track_acceptable_exception( - exception, - extra: { project_id: @importable.id, - relation_key: relation_key, - relation_index: relation_index }) + Gitlab::Sentry.track_exception(exception, + project_id: @importable.id, relation_key: relation_key, relation_index: relation_index) ImportFailure.create( project: @importable, diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 2539a6828c3..7bdfd928723 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -56,11 +56,7 @@ module Gitlab end def error(error) - error_payload = { message: error.message } - error_payload[:error_backtrace] = Gitlab::Profiler.clean_backtrace(error.backtrace) if error.backtrace - log_error(error_payload) - - Gitlab::Sentry.track_acceptable_exception(error, extra: log_base_data) + Gitlab::Sentry.track_exception(error, log_base_data) add_error_message(error.message) end diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb index 560618bb486..f2f6180c464 100644 --- a/lib/gitlab/profiler.rb +++ b/lib/gitlab/profiler.rb @@ -118,6 +118,8 @@ module Gitlab end def self.clean_backtrace(backtrace) + return unless backtrace + Array(Rails.backtrace_cleaner.clean(backtrace)).reject do |line| line.match(Regexp.union(IGNORE_BACKTRACES)) end 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 diff --git a/lib/gitlab/sentry/logger.rb b/lib/gitlab/sentry/logger.rb new file mode 100644 index 00000000000..fa24b8d17d2 --- /dev/null +++ b/lib/gitlab/sentry/logger.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module Sentry + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'exceptions_json' + end + end + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 28e5d0ba8f5..45de77f77aa 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -126,7 +126,7 @@ module Gitlab true rescue => e - Gitlab::Sentry.track_acceptable_exception(e, extra: { path: path, new_path: new_path, storage: storage }) + Gitlab::Sentry.track_exception(e, path: path, new_path: new_path, storage: storage) false end @@ -158,7 +158,7 @@ module Gitlab true rescue => e Rails.logger.warn("Repository does not exist: #{e} at: #{name}.git") # rubocop:disable Gitlab/RailsLogger - Gitlab::Sentry.track_acceptable_exception(e, extra: { path: name, storage: storage }) + Gitlab::Sentry.track_exception(e, path: name, storage: storage) false end @@ -267,7 +267,7 @@ module Gitlab def mv_namespace(storage, old_name, new_name) Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name) rescue GRPC::InvalidArgument => e - Gitlab::Sentry.track_acceptable_exception(e, extra: { old_name: old_name, new_name: new_name, storage: storage }) + Gitlab::Sentry.track_exception(e, old_name: old_name, new_name: new_name, storage: storage) false end diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb index 2b52571c3cc..461d562a0d4 100644 --- a/lib/gitlab/usage_data_counters/base_counter.rb +++ b/lib/gitlab/usage_data_counters/base_counter.rb @@ -8,7 +8,7 @@ module Gitlab::UsageDataCounters class << self def redis_key(event) - Gitlab::Sentry.track_exception(UnknownEvent, extra: { event: event }) unless known_events.include?(event.to_s) + Gitlab::Sentry.track_and_raise_for_dev_exception(UnknownEvent.new, event: event) unless known_events.include?(event.to_s) "USAGE_#{prefix}_#{event}".upcase end -- cgit v1.2.1