Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /lib/gitlab/error_tracking
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'lib/gitlab/error_tracking')
-rw-r--r--lib/gitlab/error_tracking/error_repository.rb16
-rw-r--r--lib/gitlab/error_tracking/error_repository/active_record_strategy.rb21
-rw-r--r--lib/gitlab/error_tracking/error_repository/open_api_strategy.rb248
-rw-r--r--lib/gitlab/error_tracking/processor/sanitizer_processor.rb48
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