gitlab-triage 1.51.1 → 1.52.0
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/gitlab/triage/api_observability.rb +73 -0
- data/lib/gitlab/triage/engine.rb +39 -17
- data/lib/gitlab/triage/graphql_network.rb +3 -2
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +3 -2
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +6 -2
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +10 -1
- data/lib/gitlab/triage/option_parser.rb +8 -0
- data/lib/gitlab/triage/options.rb +3 -1
- data/lib/gitlab/triage/rest_api_network.rb +2 -1
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0188aec696ec4f1821f6e9d4244a2323527d464e9c51da0b7e2b54053003a2c
|
|
4
|
+
data.tar.gz: d432a7b181c290420afdd883c01a55f686a1e30d1797f736fbbf3171459f36ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 797365b9ad72539cf3027e7e00130cbf88172e11ec46162c7c38e25d7eebbaca39cb6d5518a033ac7b820e127c15b0ef8a3d64c8011a0dcd45f4d9ef985a13c6
|
|
7
|
+
data.tar.gz: 46c14ed3a8916402aefe35aaa6b5f80c8b6b647ee4f6e1ab7bbf138ac4c0c9fd0326a672be08abc1043bfdcc5280d74456554a71f2f390439f1b9858828401f7
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Gitlab
|
|
6
|
+
module Triage
|
|
7
|
+
module APIObservability
|
|
8
|
+
class Tracker
|
|
9
|
+
PAGINATION_PARAMS = %w[page per_page].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :calls, :resources_fetched
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@calls = Hash.new(0)
|
|
15
|
+
@resources_fetched = 0
|
|
16
|
+
@rate_limit_waits = 0
|
|
17
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def record(method, url, resource_count: 0)
|
|
21
|
+
@calls["#{method} #{normalize_endpoint(url)}"] += 1
|
|
22
|
+
@resources_fetched += resource_count
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_rate_limit_wait
|
|
26
|
+
@rate_limit_waits += 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def total
|
|
30
|
+
@calls.values.sum
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def print_summary
|
|
34
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
|
35
|
+
|
|
36
|
+
puts "\n"
|
|
37
|
+
puts '=' * 70
|
|
38
|
+
puts ' API Call Summary'
|
|
39
|
+
puts '=' * 70
|
|
40
|
+
puts " Total HTTP requests: #{total}"
|
|
41
|
+
puts " Resources fetched: #{@resources_fetched}"
|
|
42
|
+
puts " Rate limit waits: #{@rate_limit_waits}"
|
|
43
|
+
puts " Elapsed time: #{elapsed.round(1)}s"
|
|
44
|
+
puts '-' * 70
|
|
45
|
+
|
|
46
|
+
@calls.sort_by { |_, count| -count }.each do |endpoint, count|
|
|
47
|
+
puts " #{count.to_s.rjust(5)}x #{endpoint}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts '=' * 70
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def normalize_endpoint(url)
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
path = uri.path
|
|
58
|
+
.sub(%r{/api/v\d+}, '')
|
|
59
|
+
.sub(%r{/projects/[^/]+}, '/projects/:id')
|
|
60
|
+
.sub(%r{/groups/[^/]+}, '/groups/:id')
|
|
61
|
+
.gsub(%r{/\d+(/|\z)}, '/:id\1')
|
|
62
|
+
|
|
63
|
+
query_keys = (URI.decode_www_form(uri.query || '').map(&:first) - PAGINATION_PARAMS).sort
|
|
64
|
+
|
|
65
|
+
path += "?#{query_keys.join('&')}" if query_keys.any?
|
|
66
|
+
path
|
|
67
|
+
rescue URI::InvalidURIError
|
|
68
|
+
url
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -32,6 +32,7 @@ require_relative 'network_adapters/httparty_adapter'
|
|
|
32
32
|
require_relative 'network_adapters/graphql_adapter'
|
|
33
33
|
require_relative 'graphql_queries/query_builder'
|
|
34
34
|
require_relative 'ui'
|
|
35
|
+
require_relative 'api_observability'
|
|
35
36
|
|
|
36
37
|
module Gitlab
|
|
37
38
|
module Triage
|
|
@@ -90,20 +91,26 @@ module Gitlab
|
|
|
90
91
|
def perform
|
|
91
92
|
puts "Performing a dry run.\n\n" if options.dry_run
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
unless options.quiet
|
|
95
|
+
puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
|
|
96
|
+
puts
|
|
97
|
+
end
|
|
95
98
|
|
|
96
99
|
resource_rules.each do |resource_type, policy_definition|
|
|
97
100
|
next unless right_resource_type_for_resource_option?(resource_type)
|
|
98
101
|
|
|
99
102
|
assert_epic_rule!(resource_type)
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
unless options.quiet
|
|
105
|
+
puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
|
|
106
|
+
puts
|
|
107
|
+
end
|
|
103
108
|
|
|
104
109
|
process_summaries(resource_type, policy_definition[:summaries])
|
|
105
110
|
process_rules(resource_type, policy_definition[:rules])
|
|
106
111
|
end
|
|
112
|
+
|
|
113
|
+
tracker.print_summary if options.summary
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
def network
|
|
@@ -204,12 +211,16 @@ module Gitlab
|
|
|
204
211
|
@resource_rules ||= policies.delete(:resource_rules) { {} }
|
|
205
212
|
end
|
|
206
213
|
|
|
214
|
+
def tracker
|
|
215
|
+
@tracker ||= options.summary ? APIObservability::Tracker.new : nil
|
|
216
|
+
end
|
|
217
|
+
|
|
207
218
|
def network_adapter
|
|
208
|
-
@network_adapter ||= @network_adapter_class.new(options)
|
|
219
|
+
@network_adapter ||= @network_adapter_class.new(options, tracker: tracker)
|
|
209
220
|
end
|
|
210
221
|
|
|
211
222
|
def graphql_network_adapter
|
|
212
|
-
@graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
|
|
223
|
+
@graphql_network_adapter ||= @graphql_network_adapter_class.new(options, tracker: tracker)
|
|
213
224
|
end
|
|
214
225
|
|
|
215
226
|
def rule_conditions(rule)
|
|
@@ -310,8 +321,10 @@ module Gitlab
|
|
|
310
321
|
#
|
|
311
322
|
# @return [nil]
|
|
312
323
|
def process_summary(resource_type, summary_definition)
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
unless options.quiet
|
|
325
|
+
puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
|
|
326
|
+
puts
|
|
327
|
+
end
|
|
315
328
|
|
|
316
329
|
summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
|
|
317
330
|
policy = Policies::SummaryPolicy.new(
|
|
@@ -365,7 +378,7 @@ module Gitlab
|
|
|
365
378
|
#
|
|
366
379
|
# @return [nil]
|
|
367
380
|
def resources_for_rule(resource_type, rule_definition, in_summary: false)
|
|
368
|
-
puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
|
|
381
|
+
puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-') unless options.quiet
|
|
369
382
|
|
|
370
383
|
# Skip resource fetching for custom type rules
|
|
371
384
|
if rule_definition[:type] == 'custom'
|
|
@@ -373,7 +386,7 @@ module Gitlab
|
|
|
373
386
|
raise ArgumentError, "type: custom can only be used in summary rules, not in top-level rules (rule: #{rule_definition[:name]})"
|
|
374
387
|
end
|
|
375
388
|
|
|
376
|
-
puts "\n* Skipping resource fetch for custom type rule"
|
|
389
|
+
puts "\n* Skipping resource fetch for custom type rule" unless options.quiet
|
|
377
390
|
yield(PoliciesResources::RuleResources.new([]), {})
|
|
378
391
|
return
|
|
379
392
|
end
|
|
@@ -391,14 +404,23 @@ module Gitlab
|
|
|
391
404
|
# In some filters/actions we want to know which resource type it is
|
|
392
405
|
attach_resource_type(resources, resource_type)
|
|
393
406
|
|
|
394
|
-
|
|
395
|
-
|
|
407
|
+
unless options.quiet
|
|
408
|
+
puts "\n\n* Found #{resources.count} resources..."
|
|
409
|
+
print "* Filtering resources..."
|
|
410
|
+
end
|
|
411
|
+
|
|
396
412
|
resources = filter_resources(resources, expanded_conditions)
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
|
|
414
|
+
unless options.quiet
|
|
415
|
+
puts "\n* Total after filtering: #{resources.count} resources"
|
|
416
|
+
print "* Limiting resources..."
|
|
417
|
+
end
|
|
418
|
+
|
|
399
419
|
resources = limit_resources(resources, rule_limits(rule_definition))
|
|
400
|
-
|
|
401
|
-
|
|
420
|
+
unless options.quiet
|
|
421
|
+
puts "\n* Total after limiting: #{resources.count} resources"
|
|
422
|
+
puts
|
|
423
|
+
end
|
|
402
424
|
|
|
403
425
|
resources = sanitize_resources(resources)
|
|
404
426
|
|
|
@@ -470,7 +492,7 @@ module Gitlab
|
|
|
470
492
|
policy: policy,
|
|
471
493
|
network: network,
|
|
472
494
|
dry: options.dry_run)
|
|
473
|
-
puts
|
|
495
|
+
puts unless options.quiet
|
|
474
496
|
end
|
|
475
497
|
|
|
476
498
|
def filter_resources(resources, conditions)
|
|
@@ -29,7 +29,7 @@ module Gitlab
|
|
|
29
29
|
parsed_graphql_query = adapter.parse(graphql_query.query)
|
|
30
30
|
|
|
31
31
|
begin
|
|
32
|
-
print '.'
|
|
32
|
+
print '.' unless options.quiet
|
|
33
33
|
|
|
34
34
|
response = adapter.query(
|
|
35
35
|
parsed_graphql_query,
|
|
@@ -52,7 +52,7 @@ module Gitlab
|
|
|
52
52
|
def mutate(graphql_mutation, variables = {})
|
|
53
53
|
return if graphql_mutation.blank?
|
|
54
54
|
|
|
55
|
-
print '.'
|
|
55
|
+
print '.' unless options.quiet
|
|
56
56
|
|
|
57
57
|
parsed_mutation = adapter.parse(graphql_mutation)
|
|
58
58
|
|
|
@@ -99,6 +99,7 @@ module Gitlab
|
|
|
99
99
|
return unless reset_at
|
|
100
100
|
|
|
101
101
|
puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{reset_at - Time.now} seconds" if options.debug
|
|
102
|
+
adapter.tracker&.record_rate_limit_wait
|
|
102
103
|
sleep(1) until Time.now >= reset_at
|
|
103
104
|
end
|
|
104
105
|
end
|
|
@@ -8,10 +8,11 @@ module Gitlab
|
|
|
8
8
|
class BaseAdapter
|
|
9
9
|
USER_AGENT = "GitLab Triage #{Gitlab::Triage::VERSION}".freeze
|
|
10
10
|
|
|
11
|
-
attr_reader :options
|
|
11
|
+
attr_reader :options, :tracker
|
|
12
12
|
|
|
13
|
-
def initialize(options)
|
|
13
|
+
def initialize(options, tracker: nil)
|
|
14
14
|
@options = options
|
|
15
|
+
@tracker = tracker
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -138,8 +138,11 @@ module Gitlab
|
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
def http_client
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
graphql_tracker = tracker
|
|
142
|
+
graphql_url = "#{options.host_url}/api/graphql"
|
|
143
|
+
|
|
144
|
+
Client::HTTP.new(graphql_url) do
|
|
145
|
+
define_method(:execute) do |document:, operation_name: nil, variables: {}, context: {}|
|
|
143
146
|
body = {}
|
|
144
147
|
body['query'] = document.to_query_string
|
|
145
148
|
body['variables'] = variables if variables.any?
|
|
@@ -157,6 +160,7 @@ module Gitlab
|
|
|
157
160
|
|
|
158
161
|
case response.code
|
|
159
162
|
when 200, 400
|
|
163
|
+
graphql_tracker&.record('POST', graphql_url)
|
|
160
164
|
JSON.parse(response.body).merge('extensions' => { 'headers' => response.headers })
|
|
161
165
|
else
|
|
162
166
|
{ 'errors' => [{ 'message' => "#{response.code} #{response.message}" }] }
|
|
@@ -24,13 +24,18 @@ module Gitlab
|
|
|
24
24
|
raise_on_internal_server_error!(response)
|
|
25
25
|
raise_on_too_many_requests!(response)
|
|
26
26
|
|
|
27
|
-
{
|
|
27
|
+
result = {
|
|
28
28
|
more_pages: (response.headers["x-next-page"].to_s != ""),
|
|
29
29
|
next_page_url: next_page_url(url, response),
|
|
30
30
|
results: response.parsed_response,
|
|
31
31
|
ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
|
|
32
32
|
ratelimit_reset_at: Time.at(response.headers["ratelimit-reset"].to_i)
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
resource_count = result[:results].is_a?(Array) ? result[:results].size : 0
|
|
36
|
+
tracker&.record('GET', url, resource_count: resource_count)
|
|
37
|
+
|
|
38
|
+
result
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def post(token, url, body)
|
|
@@ -48,6 +53,8 @@ module Gitlab
|
|
|
48
53
|
raise_on_internal_server_error!(response)
|
|
49
54
|
raise_on_too_many_requests!(response)
|
|
50
55
|
|
|
56
|
+
tracker&.record('POST', url)
|
|
57
|
+
|
|
51
58
|
{
|
|
52
59
|
results: response.parsed_response,
|
|
53
60
|
ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
|
|
@@ -68,6 +75,8 @@ module Gitlab
|
|
|
68
75
|
raise_on_internal_server_error!(response)
|
|
69
76
|
raise_on_too_many_requests!(response)
|
|
70
77
|
|
|
78
|
+
tracker&.record('DELETE', url)
|
|
79
|
+
|
|
71
80
|
{
|
|
72
81
|
results: response.parsed_response,
|
|
73
82
|
ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
|
|
@@ -65,6 +65,14 @@ module Gitlab
|
|
|
65
65
|
options.debug = value
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
opts.on('--summary', 'Print API call summary at end of execution') do |value|
|
|
69
|
+
options.summary = value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
opts.on('--quiet', 'Suppress pagination dots and section headers') do |value|
|
|
73
|
+
options.quiet = value
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
opts.on('--cache-dir [string]', String,
|
|
69
77
|
'Directory containing pre-fetched API responses. ' \
|
|
70
78
|
'Files must follow the naming convention: <source>_<id>_<resource>.json ' \
|
|
@@ -35,7 +35,7 @@ module Gitlab
|
|
|
35
35
|
resources = []
|
|
36
36
|
|
|
37
37
|
begin
|
|
38
|
-
print '.'
|
|
38
|
+
print '.' unless options.quiet
|
|
39
39
|
url = response.fetch(:next_page_url) { url }
|
|
40
40
|
|
|
41
41
|
response = execute_with_retry(
|
|
@@ -165,6 +165,7 @@ module Gitlab
|
|
|
165
165
|
return unless response.delete(:ratelimit_remaining) < MINIMUM_RATE_LIMIT
|
|
166
166
|
|
|
167
167
|
puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
|
|
168
|
+
@adapter.tracker&.record_rate_limit_wait
|
|
168
169
|
sleep(1) until Time.now >= response[:ratelimit_reset_at]
|
|
169
170
|
end
|
|
170
171
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-triage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.52.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -240,6 +240,7 @@ files:
|
|
|
240
240
|
- lib/gitlab/triage/action/issue.rb
|
|
241
241
|
- lib/gitlab/triage/action/summarize.rb
|
|
242
242
|
- lib/gitlab/triage/action/work_item_status.rb
|
|
243
|
+
- lib/gitlab/triage/api_observability.rb
|
|
243
244
|
- lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
|
|
244
245
|
- lib/gitlab/triage/api_query_builders/date_query_param_builder.rb
|
|
245
246
|
- lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb
|