diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /lib/gitlab/error_tracking | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'lib/gitlab/error_tracking')
4 files changed, 329 insertions, 4 deletions
diff --git a/lib/gitlab/error_tracking/error_repository.rb b/lib/gitlab/error_tracking/error_repository.rb index 4ec636703d9..fd2467add20 100644 --- a/lib/gitlab/error_tracking/error_repository.rb +++ b/lib/gitlab/error_tracking/error_repository.rb @@ -15,7 +15,12 @@ module Gitlab # # @return [self] def self.build(project) - strategy = ActiveRecordStrategy.new(project) + strategy = + if Feature.enabled?(:use_click_house_database_for_error_tracking, project) + OpenApiStrategy.new(project) + else + ActiveRecordStrategy.new(project) + end new(strategy) end @@ -72,14 +77,15 @@ module Gitlab # @param sort [String] order list by 'first_seen', 'last_seen', or 'frequency' # @param filters [Hash<Symbol, String>] filter list by # @option filters [String] :status error status + # @params query [String, nil] free text search # @param limit [Integer, String] limit result # @param cursor [Hash] pagination information # # @return [Array<Array<Gitlab::ErrorTracking::Error>, Pagination>] - def list_errors(sort: 'last_seen', filters: {}, limit: 20, cursor: {}) + def list_errors(sort: 'last_seen', filters: {}, query: nil, limit: 20, cursor: {}) limit = [limit.to_i, 100].min - strategy.list_errors(filters: filters, sort: sort, limit: limit, cursor: cursor) + strategy.list_errors(filters: filters, query: query, sort: sort, limit: limit, cursor: cursor) end # Fetches last event for error +id+. @@ -105,6 +111,10 @@ module Gitlab strategy.update_error(id, status: status) end + def dsn_url(public_key) + strategy.dsn_url(public_key) + end + private attr_reader :strategy diff --git a/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb b/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb index e5b532ee0f0..01e7fbda384 100644 --- a/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb +++ b/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb @@ -39,11 +39,12 @@ module Gitlab handle_exceptions(e) end - def list_errors(filters:, sort:, limit:, cursor:) + def list_errors(filters:, query:, sort:, limit:, cursor:) errors = project_errors errors = filter_by_status(errors, filters[:status]) errors = sort(errors, sort) errors = errors.keyset_paginate(cursor: cursor, per_page: limit) + # query is not supported pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page) @@ -60,6 +61,24 @@ module Gitlab project_error(id).update(attributes) end + def dsn_url(public_key) + gitlab = Settings.gitlab + + custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}" + + base_url = [ + gitlab.protocol, + "://", + public_key, + '@', + gitlab.host, + custom_port, + gitlab.relative_url_root + ].join('') + + "#{base_url}/api/v4/error_tracking/collector/#{project.id}" + end + private attr_reader :project diff --git a/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb new file mode 100644 index 00000000000..e3eae20c520 --- /dev/null +++ b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class ErrorRepository + class OpenApiStrategy + def initialize(project) + @project = project + + api_url = configured_api_url + + open_api.configure do |config| + config.scheme = api_url.scheme + config.host = [api_url.host, api_url.port].compact.join(':') + config.server_index = nil + config.logger = Gitlab::AppLogger + end + end + + def report_error( + name:, description:, actor:, platform:, + environment:, level:, occurred_at:, payload: + ) + raise NotImplementedError, 'Use ingestion endpoint' + end + + def find_error(id) + api = open_api::ErrorsApi.new + error = api.get_error(project_id, id) + + to_sentry_detailed_error(error) + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def list_errors(filters:, query:, sort:, limit:, cursor:) + opts = { + sort: "#{sort}_desc", + status: filters[:status], + query: query, + cursor: cursor, + limit: limit + }.compact + + api = open_api::ErrorsApi.new + errors, _status, headers = api.list_errors_with_http_info(project_id, opts) + pagination = pagination_from_headers(headers) + + if errors.size < limit + # Don't show next link if amount of errors is less then requested. + # This a workaround until the Golang backend returns link cursor + # only if there is a next page. + pagination.next = nil + end + + [errors.map { to_sentry_error(_1) }, pagination] + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + [[], ErrorRepository::Pagination.new] + end + + def last_event_for(id) + event = newest_event_for(id) + return unless event + + api = open_api::ErrorsApi.new + error = api.get_error(project_id, id) + return unless error + + to_sentry_error_event(event, error) + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def update_error(id, **attributes) + opts = attributes.slice(:status) + + body = open_api::ErrorUpdatePayload.new(opts) + + api = open_api::ErrorsApi.new + api.update_error(project_id, id, body) + + true + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + false + end + + def dsn_url(public_key) + config = open_api::Configuration.default + + base_url = [ + config.scheme, + "://", + public_key, + '@', + config.host, + config.base_path + ].join('') + + "#{base_url}/projects/api/#{project_id}" + end + + private + + def event_for(id, sort:) + opts = { sort: sort, limit: 1 } + + api = open_api::ErrorsApi.new + api.list_events(project_id, id, opts).first + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def newest_event_for(id) + event_for(id, sort: 'occurred_at_desc') + end + + def oldest_event_for(id) + event_for(id, sort: 'occurred_at_asc') + end + + def to_sentry_error(error) + Gitlab::ErrorTracking::Error.new( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at, + last_seen: error.last_seen_at, + status: error.status, + count: error.event_count, + user_count: error.approximated_user_count + ) + end + + def to_sentry_detailed_error(error) + Gitlab::ErrorTracking::DetailedError.new( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at.to_s, + last_seen: error.last_seen_at.to_s, + count: error.event_count, + user_count: error.approximated_user_count, + project_id: error.project_id, + status: error.status, + tags: { level: nil, logger: nil }, + external_url: external_url(error.fingerprint), + external_base_url: external_base_url, + integrated: true, + first_release_version: release_from(oldest_event_for(error.fingerprint)), + last_release_version: release_from(newest_event_for(error.fingerprint)) + ) + end + + def to_sentry_error_event(event, error) + Gitlab::ErrorTracking::ErrorEvent.new( + issue_id: event.fingerprint.to_s, + date_received: error.last_seen_at, + stack_trace_entries: build_stacktrace(event) + ) + end + + def pagination_from_headers(headers) + links = headers['link'].to_s.split(', ') + + pagination_hash = links.map { parse_pagination_link(_1) }.compact.to_h + + ErrorRepository::Pagination.new(pagination_hash['next'], pagination_hash['prev']) + end + + LINK_PATTERN = %r{cursor=(?<cursor>[^&]+).*; rel="(?<direction>\w+)"}.freeze + + def parse_pagination_link(content) + match = LINK_PATTERN.match(content) + return unless match + + [match['direction'], CGI.unescape(match['cursor'])] + end + + def build_stacktrace(event) + payload = parse_json(event.payload) + return [] unless payload + + ::ErrorTracking::StacktraceBuilder.new(payload).stacktrace + end + + def parse_json(payload) + Gitlab::Json.parse(payload) + rescue JSON::ParserError + end + + def release_from(event) + return unless event + + payload = parse_json(event.payload) + return unless payload + + payload['release'] + end + + def project_id + @project.id + end + + def open_api + ErrorTrackingOpenAPI + end + + # For compatibility with sentry integration + def external_url(id) + Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url( + namespace_id: @project.namespace, + project_id: @project, + issue_id: id) + end + + # For compatibility with sentry integration + def external_base_url + Gitlab::Routing.url_helpers.project_url(@project) + end + + def configured_api_url + url = Gitlab::CurrentSettings.current_application_settings.error_tracking_api_url || + 'http://localhost:8080' + + Gitlab::UrlBlocker.validate!(url, schemes: %w[http https], allow_localhost: true) + + URI(url) + end + + def log_exception(exception) + params = { + http_code: exception.code, + response_body: exception.response_body&.truncate(100) + } + + Gitlab::AppLogger.error(Gitlab::Utils::InlineHash.merge_keys(params, prefix: 'open_api')) + end + end + end + end +end diff --git a/lib/gitlab/error_tracking/processor/sanitizer_processor.rb b/lib/gitlab/error_tracking/processor/sanitizer_processor.rb new file mode 100644 index 00000000000..e6114f8e206 --- /dev/null +++ b/lib/gitlab/error_tracking/processor/sanitizer_processor.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + module Processor + module SanitizerProcessor + SANITIZED_HTTP_HEADERS = %w[Authorization Private-Token Job-Token].freeze + SANITIZED_ATTRIBUTES = %i[user contexts extra tags].freeze + + # This processor removes sensitive fields or headers from the event + # before sending. Sentry versions above 4.0 don't support + # sanitized_fields and sanitized_http_headers anymore. The official + # document recommends using before_send instead. + # + # For more information, please visit: + # https://docs.sentry.io/platforms/ruby/guides/rails/configuration/filtering/#using-beforesend + def self.call(event) + # Raven::Event instances don't need this processing. + return event unless event.is_a?(Sentry::Event) + + if event.request.present? + event.request.cookies = {} + event.request.data = {} + end + + if event.request.present? && event.request.headers.is_a?(Hash) + header_filter = ActiveSupport::ParameterFilter.new(SANITIZED_HTTP_HEADERS) + event.request.headers = header_filter.filter(event.request.headers) + end + + attribute_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters) + SANITIZED_ATTRIBUTES.each do |attribute| + event.send("#{attribute}=", attribute_filter.filter(event.send(attribute))) # rubocop:disable GitlabSecurity/PublicSend + end + + if event.request.present? && event.request.query_string.present? + query = Rack::Utils.parse_nested_query(event.request.query_string) + query = attribute_filter.filter(query) + query = Rack::Utils.build_nested_query(query) + event.request.query_string = query + end + + event + end + end + end + end +end |