gitlab_quality-test_tooling 2.19.1 → 2.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +30 -28
  5. data/exe/epic-readiness-notification +58 -0
  6. data/lib/gitlab_quality/test_tooling/click_house/client.rb +85 -0
  7. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  8. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  9. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  10. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  11. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  12. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -1
  13. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +3 -3
  14. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  15. data/lib/gitlab_quality/test_tooling/runtime/env.rb +5 -3
  16. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  17. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  18. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  19. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  20. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +2 -2
  21. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +88 -15
  22. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +71 -34
  23. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +105 -80
  24. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  25. metadata +58 -55
  26. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  27. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  28. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module GitlabClient
6
+ class GroupLabelsClient < GitlabClient
7
+ def initialize(token:, group:, endpoint: nil, **_kwargs)
8
+ @token = token
9
+ @group = group
10
+ @endpoint = endpoint
11
+ end
12
+
13
+ def group_labels(options: {})
14
+ client.group_labels(group, options)
15
+ end
16
+
17
+ def create_group_label(name:, color: '#428BCA', description: nil)
18
+ client.create_group_label(group, name, color, description: description)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :group, :token, :endpoint
24
+
25
+ def client
26
+ @client ||= Gitlab.client(
27
+ endpoint: endpoint || ENV['GITLAB_API_BASE'] || Runtime::Env.gitlab_api_base,
28
+ private_token: token
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -31,7 +31,7 @@ module GitlabQuality
31
31
  tests = tests.select { |test| pipeline_stages.include? test.report["stage"] } unless pipeline_stages.empty?
32
32
 
33
33
  issue = gitlab.create_issue(
34
- title: "#{Time.now.strftime('%Y-%m-%d')} Test session report | #{Runtime::Env.qa_run_type}",
34
+ title: "#{Time.now.to_date.iso8601} Test session report | #{Runtime::Env.qa_run_type}",
35
35
  description: generate_description(tests),
36
36
  labels: ['automation:bot-authored', 'E2E', 'triage report', pipeline_name_label, 'suppress-contributor-links'],
37
37
  confidential: confidential
@@ -15,7 +15,6 @@ module GitlabQuality
15
15
  class HealthProblemReporter < ReportAsIssue
16
16
  include Concerns::GroupAndCategoryLabels
17
17
  include Concerns::IssueReports
18
- include TestMetricsExporter::Support::GcsTools
19
18
 
20
19
  BASE_SEARCH_LABELS = ['test'].freeze
21
20
  FOUND_IN_MR_LABEL = '~"found:in MR"'
@@ -151,7 +150,7 @@ module GitlabQuality
151
150
  def push_test_to_gcs(tests_data, test_results_filename)
152
151
  Runtime::Logger.info "will push the test data to GCS"
153
152
 
154
- gcs_client(project_id: gcs_project_id, credentials: gcs_credentials, dry_run: dry_run).put_object(
153
+ GcsTools.gcs_client(project_id: gcs_project_id, credentials: gcs_credentials, dry_run: dry_run).put_object(
155
154
  gcs_bucket,
156
155
  gcs_metrics_file_name(test_results_filename),
157
156
  tests_data.to_json,
@@ -163,7 +162,7 @@ module GitlabQuality
163
162
  end
164
163
 
165
164
  def gcs_metrics_file_name(test_results_filename)
166
- today = Time.now.strftime('%Y-%m-%d')
165
+ today = Time.now.to_date.iso8601
167
166
 
168
167
  "#{today}-#{test_results_filename}"
169
168
  end
@@ -253,6 +252,7 @@ module GitlabQuality
253
252
  issue_url: issues.first&.web_url,
254
253
  job_id: Runtime::Env.ci_job_id,
255
254
  job_web_url: test.ci_job_url,
255
+ job_status: Runtime::Env.ci_job_status,
256
256
  pipeline_id: Runtime::Env.ci_pipeline_id,
257
257
  pipeline_ref: Runtime::Env.ci_commit_ref_name,
258
258
  pipeline_web_url: Runtime::Env.ci_pipeline_url,
@@ -78,13 +78,9 @@ module GitlabQuality
78
78
  end
79
79
 
80
80
  def slow_test_rows(slow_test)
81
- rows = []
82
-
83
- slow_test.each do |test|
84
- rows << slow_test_table_row(test)
81
+ slow_test.map do |test|
82
+ slow_test_table_row(test)
85
83
  end
86
-
87
- rows
88
84
  end
89
85
 
90
86
  def build_note(slow_test)
@@ -15,16 +15,18 @@ module GitlabQuality
15
15
  'CI_JOB_ID' => :ci_job_id,
16
16
  'CI_JOB_NAME' => :ci_job_name,
17
17
  'CI_JOB_URL' => :ci_job_url,
18
+ 'CI_JOB_STATUS' => :ci_job_status,
18
19
  'CI_PIPELINE_ID' => :ci_pipeline_id,
19
20
  'CI_PIPELINE_NAME' => :ci_pipeline_name,
20
21
  'CI_PIPELINE_URL' => :ci_pipeline_url,
21
22
  'CI_PROJECT_ID' => :ci_project_id,
22
23
  'CI_PROJECT_NAME' => :ci_project_name,
23
24
  'CI_PROJECT_PATH' => :ci_project_path,
25
+ 'CI_PIPELINE_CREATED_AT' => :ci_pipeline_created_at,
24
26
  'DEPLOY_VERSION' => :deploy_version,
25
27
  'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
26
28
  'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token,
27
- 'SLACK_QA_CHANNEL' => :slack_qa_channel
29
+ 'SLACK_ALERTS_CHANNEL' => :slack_alerts_channel
28
30
  }.freeze
29
31
 
30
32
  ENV_VARIABLES.each do |env_name, method_name|
@@ -111,12 +113,12 @@ module GitlabQuality
111
113
  end
112
114
 
113
115
  def env_var_value_if_defined(variable)
114
- return ENV.fetch(variable) if env_var_value_valid?(variable)
116
+ ENV.fetch(variable) if env_var_value_valid?(variable)
115
117
  end
116
118
 
117
119
  def env_var_name_if_defined(variable)
118
120
  # Pass through the variables if they are defined and not empty in the environment
119
- return "$#{variable}" if env_var_value_valid?(variable)
121
+ "$#{variable}" if env_var_value_valid?(variable)
120
122
  end
121
123
  end
122
124
  end
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ApiLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/api_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ApplicationLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/application_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ExceptionLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/exceptions_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class GraphqlLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/graphql_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -10,7 +10,7 @@ module GitlabQuality
10
10
 
11
11
  attr_reader :project, :ref, :report_issue, :processed_commits, :token, :specs_file, :dry_run, :processor
12
12
 
13
- TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
13
+ TEST_TOOLING_ALERTS_SLACK_CHANNEL_ID = 'C09HQ5BN07J' # test-tooling-alerts
14
14
 
15
15
  def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
16
16
  @specs_file = specs_file
@@ -294,7 +294,7 @@ module GitlabQuality
294
294
  # @param [String] message the message to post
295
295
  # @return [HTTP::Response]
296
296
  def post_message_on_slack(message)
297
- channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
297
+ channel = ENV.fetch('SLACK_ALERTS_CHANNEL', nil) || TEST_TOOLING_ALERTS_SLACK_CHANNEL_ID
298
298
  slack_options = {
299
299
  slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
300
300
  channel: channel,
@@ -8,6 +8,34 @@ module GitlabQuality
8
8
  class Config
9
9
  include Singleton
10
10
 
11
+ # GCS client configuration object used to push metrics as json file to gcs bucket
12
+ #
13
+ class GCS
14
+ def initialize(project_id:, credentials:, bucket_name:, metrics_file_name:, dry_run: false)
15
+ @project_id = project_id
16
+ @credentials = credentials
17
+ @bucket_name = bucket_name
18
+ @metrics_file_name = metrics_file_name
19
+ @dry_run = dry_run
20
+ end
21
+
22
+ attr_reader :project_id, :credentials, :bucket_name, :metrics_file_name, :dry_run
23
+ end
24
+
25
+ # ClickHouse client configuration object
26
+ #
27
+ class ClickHouse
28
+ def initialize(url:, database:, table_name:, username:, password:)
29
+ @url = url
30
+ @database = database
31
+ @table_name = table_name
32
+ @username = username
33
+ @password = password
34
+ end
35
+
36
+ attr_reader :url, :database, :table_name, :username, :password
37
+ end
38
+
11
39
  class << self
12
40
  def configuration
13
41
  Config.instance
@@ -18,25 +46,70 @@ module GitlabQuality
18
46
  end
19
47
  end
20
48
 
21
- attr_accessor :influxdb_url,
22
- :influxdb_token,
23
- :influxdb_bucket,
24
- :gcs_bucket,
25
- :gcs_project_id,
26
- :gcs_credentials,
27
- :gcs_metrics_file_name,
28
- :test_metric_file_name,
29
- :run_type
49
+ attr_reader :gcs_config, :clickhouse_config
50
+ attr_accessor :run_type
51
+ attr_writer :extra_rspec_metadata_keys,
52
+ :skip_record_proc,
53
+ :test_retried_proc,
54
+ :custom_metrics_proc,
55
+ :logger
30
56
 
31
- attr_writer :custom_keys_tags,
32
- :custom_keys_fields
57
+ # rubocop:disable Style/TrivialAccessors -- allows documenting that setting config enables the export as well as document input class type
58
+
59
+ # Enable metrics export to gcs bucket by setting configuration object
60
+ #
61
+ # @param config [Config::GCS]
62
+ # @return [GCS]
63
+ def gcs_config=(config)
64
+ @gcs_config = config
65
+ end
66
+
67
+ # Enable metrics export to clickhouse by setting configuration object
68
+ #
69
+ # @param config [Config::ClickHouse]
70
+ # @return [ClickHouse]
71
+ def clickhouse_config=(config)
72
+ @clickhouse_config = config
73
+ end
74
+
75
+ # rubocop:enable Style/TrivialAccessors
76
+
77
+ # Extra rspec metadata keys to include in exported metrics
78
+ #
79
+ # @return [Array<Symbol>]
80
+ def extra_rspec_metadata_keys
81
+ @extra_rspec_metadata_keys ||= []
82
+ end
83
+
84
+ # A lambda that determines whether to skip recording a test result
85
+ #
86
+ # This is useful when you would want to skip initial failure when retrying specs is set up in a separate process
87
+ # and you want to avoid duplicate records
88
+ #
89
+ # @return [Proc]
90
+ def skip_record_proc
91
+ @skip_record_proc ||= ->(_example) { false }
92
+ end
93
+
94
+ # A lambda that determines whether a test was retried or not
95
+ #
96
+ # @return [Proc]
97
+ def test_retried_proc
98
+ @test_retried_proc ||= ->(_example) { false }
99
+ end
33
100
 
34
- def custom_keys_tags
35
- @custom_keys_tags || []
101
+ # A lambda that return hash with additional custom metrics
102
+ #
103
+ # @return [Proc]
104
+ def custom_metrics_proc
105
+ @custom_metrics_proc ||= ->(_example) { {} }
36
106
  end
37
107
 
38
- def custom_keys_fields
39
- @custom_keys_fields || []
108
+ # Logger instance
109
+ #
110
+ # @return [Logger]
111
+ def logger
112
+ @logger ||= Logger.new($stdout, level: Logger::INFO)
40
113
  end
41
114
  end
42
115
  end
@@ -1,56 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rspec/core/formatters/base_formatter"
4
+
5
+ # rubocop:disable Metrics/AbcSize
3
6
  module GitlabQuality
4
7
  module TestTooling
5
8
  module TestMetricsExporter
6
9
  class Formatter < RSpec::Core::Formatters::BaseFormatter
7
10
  RSpec::Core::Formatters.register(self, :stop)
8
11
 
12
+ LOG_PREFIX = "[MetricsExporter]"
13
+
9
14
  def stop(notification)
10
- setup_test_metrics_exporter(notification.examples)
11
-
12
- log_test_metrics.push_test_metrics(
13
- custom_keys_tags: config.custom_keys_tags,
14
- custom_keys_fields: config.custom_keys_fields
15
- )
16
-
17
- log_test_metrics.save_test_metrics(
18
- file_name: config.test_metric_file_name,
19
- custom_keys_tags: config.custom_keys_tags,
20
- custom_keys_fields: config.custom_keys_fields
21
- )
15
+ data = notification.examples.filter_map do |example|
16
+ next if config.skip_record_proc.call(example)
17
+
18
+ TestMetrics.new(example, time).data
19
+ end
20
+ return logger.warn("#{LOG_PREFIX} No test execution records found, metrics will not be exported!") if data.empty?
21
+
22
+ push_to_gcs(data)
23
+ push_to_clickhouse(data)
22
24
  end
23
25
 
24
26
  private
25
27
 
26
- attr_reader :log_test_metrics
27
-
28
+ # Configuration instance
29
+ #
30
+ # @return [Config]
28
31
  def config
29
32
  Config.configuration
30
33
  end
31
34
 
32
- # rubocop:disable Metrics/AbcSize
33
- def setup_test_metrics_exporter(examples)
34
- @log_test_metrics = LogTestMetrics.new(
35
- examples: examples,
36
- run_type: config.run_type
37
- )
38
-
39
- @log_test_metrics.configure_influxdb_client(
40
- influxdb_url: config.influxdb_url,
41
- influxdb_token: config.influxdb_token,
42
- influxdb_bucket: config.influxdb_bucket
43
- )
44
-
45
- @log_test_metrics.configure_gcs_client(
46
- gcs_bucket: config.gcs_bucket,
47
- gcs_project_id: config.gcs_project_id,
48
- gcs_credentials: config.gcs_credentials,
49
- gcs_metrics_file_name: config.gcs_metrics_file_name
50
- )
35
+ # Logger instance
36
+ #
37
+ # @return [Logger]
38
+ def logger
39
+ config.logger
40
+ end
41
+
42
+ # Single common timestamp for all exported example metrics to keep data points consistently grouped
43
+ #
44
+ # @return [Time]
45
+ def time
46
+ return @time if @time
47
+
48
+ ci_created_at = Runtime::Env.ci_pipeline_created_at
49
+ @time = ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc
50
+ end
51
+
52
+ # Push data to gcs
53
+ #
54
+ # @param data [Array]
55
+ # @return [void]
56
+ def push_to_gcs(data)
57
+ return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless config.gcs_config
58
+
59
+ gcs_config = config.gcs_config
60
+ GcsTools.gcs_client(
61
+ project_id: gcs_config.project_id,
62
+ credentials: gcs_config.credentials,
63
+ dry_run: gcs_config.dry_run
64
+ ).put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
65
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to GCS bucket!")
66
+ rescue StandardError => e
67
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to GCS: #{e.message}")
68
+ end
69
+
70
+ # Push data to clickhouse
71
+ #
72
+ # @param data [Array<Hash>]
73
+ # @return [void]
74
+ def push_to_clickhouse(data)
75
+ return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless config.clickhouse_config
76
+
77
+ clickhouse_config = config.clickhouse_config
78
+ ClickHouse::Client.new(
79
+ url: clickhouse_config.url,
80
+ database: clickhouse_config.database,
81
+ username: clickhouse_config.username,
82
+ password: clickhouse_config.password,
83
+ logger: logger
84
+ ).insert_json_data(clickhouse_config.table_name, data)
85
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
86
+ rescue StandardError => e
87
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
51
88
  end
52
- # rubocop:enable Metrics/AbcSize
53
89
  end
54
90
  end
55
91
  end
56
92
  end
93
+ # rubocop:enable Metrics/AbcSize