gitlab-exporter 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.gitlab-ci.yml +18 -0
  4. data/.rubocop.yml +34 -0
  5. data/CONTRIBUTING.md +651 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +75 -0
  8. data/LICENSE +25 -0
  9. data/README.md +126 -0
  10. data/bin/gitlab-exporter +17 -0
  11. data/config/gitlab-exporter.yml.example +111 -0
  12. data/gitlab-exporter.gemspec +33 -0
  13. data/lib/gitlab_exporter/cli.rb +342 -0
  14. data/lib/gitlab_exporter/database/base.rb +44 -0
  15. data/lib/gitlab_exporter/database/bloat.rb +74 -0
  16. data/lib/gitlab_exporter/database/bloat_btree.sql +84 -0
  17. data/lib/gitlab_exporter/database/bloat_table.sql +63 -0
  18. data/lib/gitlab_exporter/database/ci_builds.rb +527 -0
  19. data/lib/gitlab_exporter/database/remote_mirrors.rb +74 -0
  20. data/lib/gitlab_exporter/database/row_count.rb +164 -0
  21. data/lib/gitlab_exporter/database/tuple_stats.rb +53 -0
  22. data/lib/gitlab_exporter/database.rb +13 -0
  23. data/lib/gitlab_exporter/git.rb +144 -0
  24. data/lib/gitlab_exporter/memstats/mapping.rb +91 -0
  25. data/lib/gitlab_exporter/memstats.rb +98 -0
  26. data/lib/gitlab_exporter/prober.rb +40 -0
  27. data/lib/gitlab_exporter/process.rb +122 -0
  28. data/lib/gitlab_exporter/prometheus.rb +64 -0
  29. data/lib/gitlab_exporter/sidekiq.rb +171 -0
  30. data/lib/gitlab_exporter/sidekiq_queue_job_stats.lua +42 -0
  31. data/lib/gitlab_exporter/util.rb +83 -0
  32. data/lib/gitlab_exporter/version.rb +5 -0
  33. data/lib/gitlab_exporter/web_exporter.rb +77 -0
  34. data/lib/gitlab_exporter.rb +18 -0
  35. data/spec/cli_spec.rb +31 -0
  36. data/spec/database/bloat_spec.rb +99 -0
  37. data/spec/database/ci_builds_spec.rb +421 -0
  38. data/spec/database/row_count_spec.rb +37 -0
  39. data/spec/fixtures/smaps/sample.txt +10108 -0
  40. data/spec/git_process_proper_spec.rb +27 -0
  41. data/spec/git_spec.rb +52 -0
  42. data/spec/memstats_spec.rb +28 -0
  43. data/spec/prometheus_metrics_spec.rb +17 -0
  44. data/spec/spec_helper.rb +63 -0
  45. data/spec/util_spec.rb +15 -0
  46. metadata +224 -0
@@ -0,0 +1,42 @@
1
+ --
2
+ -- Adapted from https://github.com/mperham/sidekiq/blob/2f9258e4fe77991c526f7a65c92bcf792eef8338/lib/sidekiq/api.rb#L231
3
+ --
4
+ local queue_name = KEYS[1]
5
+ local initial_size = redis.call('llen', queue_name)
6
+ local deleted_size = 0
7
+ local page = 0
8
+ local page_size = 2000
9
+ local temp_job_stats = {}
10
+ local final_job_stats = {}
11
+
12
+ while true do
13
+ local range_start = page * page_size - deleted_size
14
+ local range_end = range_start + page_size - 1
15
+ local entries = redis.call('lrange', queue_name, range_start, range_end)
16
+
17
+ if #entries == 0 then
18
+ break
19
+ end
20
+
21
+ page = page + 1
22
+
23
+ for index, entry in next, entries do
24
+ local class = cjson.decode(entry)['class']
25
+ if class ~= nil then
26
+ if temp_job_stats[class] ~= nil then
27
+ temp_job_stats[class] = temp_job_stats[class] + 1
28
+ else
29
+ temp_job_stats[class] = 1
30
+ end
31
+ end
32
+ end
33
+
34
+ deleted_size = initial_size - redis.call('llen', queue_name)
35
+ end
36
+
37
+ for class, count in next, temp_job_stats do
38
+ local stat_entry = {class, count}
39
+ table.insert(final_job_stats, stat_entry)
40
+ end
41
+
42
+ return final_job_stats
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitLab
4
+ module Exporter
5
+ # Simple time wrapper that provides a to_i and wraps the execution result
6
+ TrackedResult = Struct.new(:result, :time) do
7
+ def to_i
8
+ time
9
+ end
10
+ end
11
+
12
+ # Time tracking object
13
+ #
14
+ # Provides a simple time tracking, and returns back the result plus the tracked time
15
+ # wraped in a TrackedResult struct
16
+ class TimeTracker
17
+ def track
18
+ @start = Time.now.to_f
19
+ result = yield
20
+ TrackedResult.new(result, Time.now.to_f - @start)
21
+ end
22
+ end
23
+
24
+ # Helper methods, some stuff was copied from ActiveSupport
25
+ module Utils
26
+ def camel_case_string(str)
27
+ str.gsub(/(?:_|^)([a-z\d]*)/i) { $1.capitalize } # rubocop:disable PerlBackrefs
28
+ end
29
+ module_function :camel_case_string
30
+
31
+ def deep_symbolize_hash_keys(hash)
32
+ deep_transform_keys_in_object(hash, &:to_sym)
33
+ end
34
+ module_function :deep_symbolize_hash_keys
35
+
36
+ def deep_transform_keys_in_object(object, &block)
37
+ case object
38
+ when Hash
39
+ object.keys.each do |key|
40
+ value = object.delete(key)
41
+ object[yield(key)] = deep_transform_keys_in_object(value, &block)
42
+ end
43
+ object
44
+ when Array
45
+ object.map! { |e| deep_transform_keys_in_object(e, &block) }
46
+ else
47
+ object
48
+ end
49
+ end
50
+ module_function :deep_transform_keys_in_object
51
+
52
+ def pgrep(pattern)
53
+ # pgrep will include the PID of the shell, so strip that out
54
+ exec_pgrep(pattern).split("\n").each_with_object([]) do |line, arr|
55
+ pid, name = line.split(" ")
56
+ arr << pid if name != "sh"
57
+ end
58
+ end
59
+ module_function :pgrep
60
+
61
+ def exec_pgrep(pattern)
62
+ `pgrep -fl "#{pattern}"`
63
+ end
64
+ module_function :exec_pgrep
65
+
66
+ def system_uptime
67
+ File.read("/proc/uptime").split(" ")[0].to_f
68
+ end
69
+ module_function :system_uptime
70
+
71
+ def wrap_in_array(object)
72
+ if object.nil?
73
+ []
74
+ elsif object.respond_to?(:to_ary)
75
+ object.to_ary || [object]
76
+ else
77
+ [object]
78
+ end
79
+ end
80
+ module_function :wrap_in_array
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module GitLab
2
+ module Exporter
3
+ VERSION = "5.0.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,77 @@
1
+ require "sinatra/base"
2
+ require "English"
3
+
4
+ module GitLab
5
+ module Exporter
6
+ # Metrics web exporter
7
+ class WebExporter < Sinatra::Base
8
+ # A middleware to kill the process if we exceeded a certain threshold
9
+ class MemoryKillerMiddleware
10
+ def initialize(app, memory_threshold)
11
+ @app = app
12
+ @memory_threshold = memory_threshold.to_i * 1024
13
+ end
14
+
15
+ def call(env)
16
+ if memory_usage > @memory_threshold
17
+ puts "Memory usage of #{memory_usage} exceeded threshold of #{@memory_threshold}, signalling KILL"
18
+ Process.kill("KILL", $PID)
19
+ end
20
+
21
+ @app.call(env)
22
+ end
23
+
24
+ private
25
+
26
+ def memory_usage
27
+ io = IO.popen(%W(ps -o rss= -p #{$PID}))
28
+
29
+ mem = io.read
30
+ io.close
31
+
32
+ return 0 unless $CHILD_STATUS.to_i.zero?
33
+
34
+ mem.to_i
35
+ end
36
+ end
37
+
38
+ class << self
39
+ def setup(config)
40
+ setup_server(config[:server])
41
+ setup_probes(config[:probes])
42
+
43
+ memory_threshold = (config[:server] && config[:server][:memory_threshold]) || 1024
44
+ use MemoryKillerMiddleware, memory_threshold
45
+ end
46
+
47
+ def setup_server(config)
48
+ config ||= {}
49
+
50
+ set(:bind, config.fetch(:listen_address, "0.0.0.0"))
51
+ set(:port, config.fetch(:listen_port, 9168))
52
+ end
53
+
54
+ def setup_probes(config)
55
+ (config || {}).each do |probe_name, params|
56
+ opts =
57
+ if params.delete(:multiple)
58
+ params
59
+ else
60
+ { probe_name => params }
61
+ end
62
+
63
+ get "/#{probe_name}" do
64
+ content_type "text/plain; version=0.0.4"
65
+ prober = Prober.new(opts, metrics: PrometheusMetrics.new(include_timestamp: false))
66
+
67
+ prober.probe_all
68
+ prober.write_to(response)
69
+
70
+ response
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,18 @@
1
+ module GitLab
2
+ # GitLab Monitoring
3
+ module Exporter
4
+ autoload :CLI, "gitlab_exporter/cli"
5
+ autoload :TimeTracker, "gitlab_exporter/util"
6
+ autoload :Utils, "gitlab_exporter/util"
7
+ autoload :PrometheusMetrics, "gitlab_exporter/prometheus"
8
+ autoload :Utils, "gitlab_exporter/util"
9
+ autoload :Git, "gitlab_exporter/git"
10
+ autoload :GitProber, "gitlab_exporter/git"
11
+ autoload :GitProcessProber, "gitlab_exporter/git"
12
+ autoload :Database, "gitlab_exporter/database"
13
+ autoload :ProcessProber, "gitlab_exporter/process"
14
+ autoload :WebExporter, "gitlab_exporter/web_exporter"
15
+ autoload :Prober, "gitlab_exporter/prober"
16
+ autoload :SidekiqProber, "gitlab_exporter/sidekiq"
17
+ end
18
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+ require "gitlab_exporter/cli"
3
+
4
+ context "With valid pair of repositories" do
5
+ let(:repos) { GitRepoBuilder.new }
6
+
7
+ after do
8
+ repos.cleanup
9
+ end
10
+
11
+ describe GitLab::Exporter::CLI do
12
+ it "returns the rigth parser" do
13
+ expect(GitLab::Exporter::CLI.for("git")).to be(GitLab::Exporter::CLI::GIT)
14
+ end
15
+
16
+ it "returns a null parser if it is not found" do
17
+ expect(GitLab::Exporter::CLI.for("invalid")).to be(GitLab::Exporter::CLI::NullRunner)
18
+ end
19
+ end
20
+
21
+ describe GitLab::Exporter::CLI::GIT do
22
+ let(:output) { StringIO.new }
23
+ it "works end to end" do
24
+ args = CLIArgs.new([repos.cloned_repo, output])
25
+ ssh = GitLab::Exporter::CLI::GIT.new(args)
26
+ ssh.run
27
+ output.rewind
28
+ expect(output.read).to match(/git_push_time_milliseconds \d+ \d+/)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ require "spec_helper"
2
+ require "gitlab_exporter/database/bloat"
3
+
4
+ describe GitLab::Exporter::Database::BloatCollector do
5
+ let(:connection_pool) { double("connection pool") }
6
+ let(:connection) { double("connection") }
7
+
8
+ subject { described_class.new(connection_string: "").run(type) }
9
+ let(:type) { :btree }
10
+ let(:query) { "select something" }
11
+
12
+ before do
13
+ allow_any_instance_of(described_class).to receive(:connection_pool).and_return(connection_pool)
14
+ allow(connection_pool).to receive(:with).and_yield(connection)
15
+ end
16
+
17
+ it "converts query results into a hash" do
18
+ row1 = { "object_name" => "o", "more_stuff" => 1 }
19
+ row2 = { "object_name" => "a", "more_stuff" => 2 }
20
+
21
+ expect(File).to receive(:read).and_return(query)
22
+ expect(connection).to receive(:exec).with(query).and_return([row1, row2])
23
+
24
+ expect(subject).to eq("o" => row1, "a" => row2)
25
+ end
26
+ end
27
+
28
+ describe GitLab::Exporter::Database::BloatProber do
29
+ let(:opts) { { bloat_types: %i(btree table) } }
30
+ let(:metrics) { double("PrometheusMetrics", add: nil) }
31
+ let(:collector) { double("BloatCollector", run: data) }
32
+
33
+ let(:data) do
34
+ {
35
+ "object" => {
36
+ "object_name" => "object",
37
+ "bloat_ratio" => 1,
38
+ "bloat_size" => 2,
39
+ "extra_size" => 3,
40
+ "real_size" => 4
41
+ }
42
+ }
43
+ end
44
+
45
+ describe "#probe_db" do
46
+ subject { described_class.new(opts, metrics: metrics, collector: collector).probe_db }
47
+
48
+ it "invokes the collector for each bloat type" do
49
+ expect(collector).to receive(:run).with(:btree)
50
+ expect(collector).to receive(:run).with(:table)
51
+
52
+ subject
53
+ end
54
+
55
+ it "adds bloat_ratio metric" do
56
+ opts[:bloat_types].each do |type|
57
+ expect(metrics).to receive(:add).with("gitlab_database_bloat_#{type}_bloat_ratio", 1, query_name: "object")
58
+ end
59
+
60
+ subject
61
+ end
62
+
63
+ it "adds bloat_size metric" do
64
+ opts[:bloat_types].each do |type|
65
+ expect(metrics).to receive(:add).with("gitlab_database_bloat_#{type}_bloat_size", 2, query_name: "object")
66
+ end
67
+
68
+ subject
69
+ end
70
+
71
+ it "adds extra_size metric" do
72
+ opts[:bloat_types].each do |type|
73
+ expect(metrics).to receive(:add).with("gitlab_database_bloat_#{type}_extra_size", 3, query_name: "object")
74
+ end
75
+
76
+ subject
77
+ end
78
+
79
+ it "adds real_size metric" do
80
+ opts[:bloat_types].each do |type|
81
+ expect(metrics).to receive(:add).with("gitlab_database_bloat_#{type}_real_size", 4, query_name: "object")
82
+ end
83
+
84
+ subject
85
+ end
86
+ end
87
+
88
+ describe "#write_to" do
89
+ let(:target) { double }
90
+ let(:metrics) { double("PrometheusMetrics", to_s: double) }
91
+ subject { described_class.new(opts, metrics: metrics).write_to(target) }
92
+
93
+ it "writes to given target" do
94
+ expect(target).to receive(:write).with(metrics.to_s)
95
+
96
+ subject
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,421 @@
1
+ require "spec_helper"
2
+ require "gitlab_exporter/database/ci_builds"
3
+
4
+ # rubocop:disable Metrics/LineLength
5
+ describe GitLab::Exporter::Database do
6
+ let(:set_random_page_cost_query) { "SET random_page_cost" }
7
+ let(:builds_query_ee) { "SELECT BUILDS EE" }
8
+ let(:builds_query_ce) { "SELECT BUILDS CE" }
9
+ let(:stale_builds_query) { "SELECT NOT UPDATED RUNNING" }
10
+ let(:per_runner_query_ee) { "SELECT ALL RUNNING PER RUNNER EE" }
11
+ let(:per_runner_query_ce) { "SELECT ALL RUNNING PER RUNNER CE" }
12
+ let(:mirror_column_query) { "SELECT DOES MIRROR COLUMN EXISTS" }
13
+ let(:repeated_commands_query_ee) { "SELECT EE REPEATED COMNANDS %d" }
14
+ let(:repeated_commands_query_ce) { "SELECT CE REPEATED COMNANDS %d" }
15
+ let(:unarchived_traces_query) { "SELECT UNARCHIVED TRACES %s LIST" }
16
+ let(:connection_pool) { double("connection pool") }
17
+ let(:connection) { double("connection") }
18
+ let(:allowed_repeated_commands_count) { 5 }
19
+ let(:created_builds_counting_disabled) { true }
20
+ let(:time_now) { Time.new(2019, 4, 9, 6, 30, 0) }
21
+ let(:unarchived_traces_query_time) { "2019-04-09 05:30:00" }
22
+ let(:unarchived_traces_offset_minutes) { 60 }
23
+
24
+ def stub_ee
25
+ allow(connection).to receive(:exec).with(mirror_column_query).and_return([{ "exists" => "t" }])
26
+ end
27
+
28
+ def stub_ce
29
+ allow(connection).to receive(:exec).with(mirror_column_query).and_return([{ "exists" => "f" }])
30
+ end
31
+
32
+ def builds_query_row_ee(shared_runners_enabled, status, namespace_id, has_minutes, count)
33
+ row = builds_query_row_ce(shared_runners_enabled, status, namespace_id, count)
34
+ row["has_minutes"] = has_minutes
35
+ row
36
+ end
37
+
38
+ def builds_query_row_ce(shared_runners_enabled, status, namespace_id, count)
39
+ { "shared_runners_enabled" => shared_runners_enabled,
40
+ "status" => status,
41
+ "namespace_id" => namespace_id,
42
+ "count" => count }
43
+ end
44
+
45
+ # rubocop:disable Metrics/ParameterLists
46
+ def per_runner_query_row_ee(runner_id, is_shared, namespace_id, mirror, mirror_trigger_builds, pipeline_schedule_id, trigger_request_id, has_minutes, count)
47
+ row = per_runner_query_row_ce(runner_id, is_shared, namespace_id, pipeline_schedule_id, trigger_request_id, count)
48
+ row["mirror"] = mirror
49
+ row["mirror_trigger_builds"] = mirror_trigger_builds
50
+ row["has_minutes"] = has_minutes
51
+ row
52
+ end
53
+ # rubocop:enable Metrics/ParameterLists
54
+
55
+ # rubocop:disable Metrics/ParameterLists
56
+ def per_runner_query_row_ce(runner_id, is_shared, namespace_id, pipeline_schedule_id, trigger_request_id, count)
57
+ { "runner_id" => runner_id,
58
+ "is_shared" => is_shared,
59
+ "namespace_id" => namespace_id,
60
+ "pipeline_schedule_id" => pipeline_schedule_id,
61
+ "trigger_request_id" => trigger_request_id,
62
+ "count" => count }
63
+ end
64
+ # rubocop:enable Metrics/ParameterLists
65
+
66
+ # rubocop:disable Metrics/ParameterLists
67
+ def repeated_commands_query_row_ee(namespace_id, shared_runners_enabled, project_id, status, has_minutes, count)
68
+ row = repeated_commands_query_row_ce(namespace_id, shared_runners_enabled, project_id, status, count)
69
+ row["has_minutes"] = has_minutes
70
+ row
71
+ end
72
+ # rubocop:enable Metrics/ParameterLists
73
+
74
+ def repeated_commands_query_row_ce(namespace_id, shared_runners_enabled, project_id, status, count)
75
+ { "namespace_id" => namespace_id,
76
+ "shared_runners_enabled" => shared_runners_enabled,
77
+ "project_id" => project_id,
78
+ "status" => status,
79
+ "count" => count }
80
+ end
81
+
82
+ before do
83
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::SET_RANDOM_PAGE_COST", set_random_page_cost_query)
84
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::BUILDS_QUERY_EE", builds_query_ee)
85
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::BUILDS_QUERY_CE", builds_query_ce)
86
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::STALE_BUILDS_QUERY", stale_builds_query)
87
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::PER_RUNNER_QUERY_EE", per_runner_query_ee)
88
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::PER_RUNNER_QUERY_CE", per_runner_query_ce)
89
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::MIRROR_COLUMN_QUERY", mirror_column_query)
90
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::REPEATED_COMMANDS_QUERY_EE", repeated_commands_query_ee)
91
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::REPEATED_COMMANDS_QUERY_CE", repeated_commands_query_ce)
92
+ stub_const("GitLab::Exporter::Database::CiBuildsCollector::UNARCHIVED_TRACES_QUERY", unarchived_traces_query)
93
+
94
+ allow_any_instance_of(GitLab::Exporter::Database::CiBuildsCollector).to receive(:connection_pool).and_return(connection_pool)
95
+ allow(connection_pool).to receive(:with).and_yield(connection)
96
+
97
+ allow(connection).to receive(:transaction).and_yield(connection)
98
+ allow(connection).to receive(:exec).with(set_random_page_cost_query)
99
+
100
+ allow(Time).to receive(:now).and_return(time_now)
101
+
102
+ allow(connection).to receive(:exec).with(builds_query_ee)
103
+ .and_return([builds_query_row_ee("f", "created", "1", "f", 10),
104
+ builds_query_row_ee("t", "pending", "1", "t", 30),
105
+ builds_query_row_ee("f", "created", "2", "f", 20),
106
+ builds_query_row_ee("t", "pending", "2", "t", 50),
107
+ builds_query_row_ee("t", "pending", "3", "f", 1),
108
+ builds_query_row_ee("t", "pending", "4", "t", 2),
109
+ builds_query_row_ee("f", "pending", "5", "f", 2)])
110
+ allow(connection).to receive(:exec).with(builds_query_ce)
111
+ .and_return([builds_query_row_ce("f", "created", "1", 10),
112
+ builds_query_row_ce("t", "pending", "1", 30),
113
+ builds_query_row_ce("f", "created", "2", 20),
114
+ builds_query_row_ce("t", "pending", "2", 50),
115
+ builds_query_row_ce("t", "pending", "3", 1),
116
+ builds_query_row_ce("t", "pending", "4", 2),
117
+ builds_query_row_ce("f", "pending", "5", 2)])
118
+
119
+ allow(connection).to receive(:exec).with(stale_builds_query).and_return([{ "count" => 2 }])
120
+
121
+ allow(connection).to receive(:exec).with(per_runner_query_ee)
122
+ .and_return([per_runner_query_row_ee(1, "t", 1, "f", "f", 1, nil, "t", 15),
123
+ per_runner_query_row_ee(2, "f", 2, "t", "t", nil, 3, "f", 5),
124
+ per_runner_query_row_ee(2, "f", 3, "t", "t", nil, 3, "t", 5),
125
+ per_runner_query_row_ee(3, "f", 4, "t", "t", nil, 3, "f", 5)])
126
+
127
+ allow(connection).to receive(:exec).with(per_runner_query_ce)
128
+ .and_return([per_runner_query_row_ce(1, "t", 1, 1, nil, 15),
129
+ per_runner_query_row_ce(2, "f", 2, nil, 3, 5),
130
+ per_runner_query_row_ce(2, "f", 3, nil, 3, 5),
131
+ per_runner_query_row_ce(3, "f", 4, nil, 3, 5)])
132
+
133
+ # rubocop:disable Style/FormatString
134
+ repeated_commands_query_ee_with_limit = repeated_commands_query_ee % [allowed_repeated_commands_count]
135
+ repeated_commands_query_ce_with_limit = repeated_commands_query_ce % [allowed_repeated_commands_count]
136
+ # rubocop:enable Style/FormatString
137
+
138
+ allow(connection).to receive(:exec)
139
+ .with(repeated_commands_query_ee_with_limit)
140
+ .and_return([repeated_commands_query_row_ee(1, "t", 1, "pending", "t", 10),
141
+ repeated_commands_query_row_ee(2, "f", 2, "running", "f", 20),
142
+ repeated_commands_query_row_ee(1, "f", 3, "pending", "t", 30),
143
+ repeated_commands_query_row_ee(2, "t", 4, "running", "f", 40)])
144
+ allow(connection).to receive(:exec)
145
+ .with(repeated_commands_query_ce_with_limit)
146
+ .and_return([repeated_commands_query_row_ce(1, "t", 1, "pending", 10),
147
+ repeated_commands_query_row_ce(2, "f", 2, "running", 20),
148
+ repeated_commands_query_row_ce(1, "f", 3, "pending", 30),
149
+ repeated_commands_query_row_ce(2, "t", 4, "running", 40)])
150
+
151
+ unarchived_traces_query_with_time = unarchived_traces_query % [unarchived_traces_query_time] # rubocop:disable Style/FormatString
152
+
153
+ allow(connection).to receive(:exec).with(unarchived_traces_query_with_time).and_return([{ "count" => 10 }])
154
+ end
155
+
156
+ describe GitLab::Exporter::Database::CiBuildsCollector do
157
+ let(:collector) do
158
+ described_class.new(connection_string: "host=localhost",
159
+ allowed_repeated_commands_count: allowed_repeated_commands_count,
160
+ created_builds_counting_disabled: created_builds_counting_disabled,
161
+ unarchived_traces_offset_minutes: unarchived_traces_offset_minutes)
162
+ end
163
+ let(:expected_stale_builds) { 2 }
164
+ let(:expected_unarchived_traces) { 10 }
165
+
166
+ shared_examples "data collector" do
167
+ subject { collector.run }
168
+
169
+ it "returns raw per_runner data" do
170
+ expect(subject[:per_runner]).to include(*expected_per_runner)
171
+ end
172
+
173
+ it "returns raw pending_builds data" do
174
+ expect(subject[:pending_builds]).to include(*expected_pending_builds)
175
+ end
176
+
177
+ context "when created_builds_counting_disabled is set to false" do
178
+ let(:created_builds_counting_disabled) { false }
179
+
180
+ it "returns raw created_builds data" do
181
+ expect(subject).to have_key(:created_builds)
182
+ expect(subject[:created_builds]).to include(*expected_created_builds)
183
+ end
184
+ end
185
+
186
+ context "when created_builds_counting_disabled is set to true" do
187
+ let(:created_builds_counting_disabled) { true }
188
+
189
+ it "doesn't return raw created_builds data" do
190
+ expect(subject).not_to have_key(:created_builds)
191
+ end
192
+ end
193
+
194
+ it "returns raw stale_builds data" do
195
+ expect(subject[:stale_builds]).to eq(expected_stale_builds)
196
+ end
197
+
198
+ it "returns raw repeated_commands data" do
199
+ expect(subject[:repeated_commands]).to include(*expected_repeated_commands)
200
+ end
201
+
202
+ it "returns raw unarchived_traces data" do
203
+ expect(subject[:unarchived_traces]).to eq(expected_unarchived_traces)
204
+ end
205
+ end
206
+
207
+ context "when executed on EE" do
208
+ let(:expected_pending_builds) do
209
+ [{ namespace: "1", shared_runners: "yes", has_minutes: "yes", value: 30 },
210
+ { namespace: "2", shared_runners: "yes", has_minutes: "yes", value: 50 },
211
+ { namespace: "3", shared_runners: "yes", has_minutes: "no", value: 1 },
212
+ { namespace: "4", shared_runners: "yes", has_minutes: "yes", value: 2 },
213
+ { namespace: "5", shared_runners: "no", has_minutes: "no", value: 2 }]
214
+ end
215
+ let(:expected_created_builds) do
216
+ [{ namespace: "1", shared_runners: "no", has_minutes: "no", value: 10 },
217
+ { namespace: "2", shared_runners: "no", has_minutes: "no", value: 20 }]
218
+ end
219
+ let(:expected_per_runner) do
220
+ [{ runner: "1", shared_runner: "yes", namespace: "1", mirror: "no", mirror_trigger_builds: "no", scheduled: "yes", triggered: "no", has_minutes: "yes", value: 15 },
221
+ { runner: "2", shared_runner: "no", namespace: "2", mirror: "yes", mirror_trigger_builds: "yes", scheduled: "no", triggered: "yes", has_minutes: "no", value: 5 },
222
+ { runner: "2", shared_runner: "no", namespace: "3", mirror: "yes", mirror_trigger_builds: "yes", scheduled: "no", triggered: "yes", has_minutes: "yes", value: 5 },
223
+ { runner: "3", shared_runner: "no", namespace: "4", mirror: "yes", mirror_trigger_builds: "yes", scheduled: "no", triggered: "yes", has_minutes: "no", value: 5 }]
224
+ end
225
+ let(:expected_repeated_commands) do
226
+ [{ namespace: "1", project: "1", shared_runners: "yes", status: "pending", has_minutes: "yes", value: 10 },
227
+ { namespace: "2", project: "2", shared_runners: "no", status: "running", has_minutes: "no", value: 20 },
228
+ { namespace: "1", project: "3", shared_runners: "no", status: "pending", has_minutes: "yes", value: 30 },
229
+ { namespace: "2", project: "4", shared_runners: "yes", status: "running", has_minutes: "no", value: 40 }]
230
+ end
231
+
232
+ before do
233
+ stub_ee
234
+ end
235
+
236
+ it_behaves_like "data collector"
237
+ end
238
+
239
+ context "when executed on CE" do
240
+ let(:expected_pending_builds) do
241
+ [{ namespace: "1", shared_runners: "yes", value: 30 },
242
+ { namespace: "2", shared_runners: "yes", value: 50 },
243
+ { namespace: "3", shared_runners: "yes", value: 1 },
244
+ { namespace: "4", shared_runners: "yes", value: 2 },
245
+ { namespace: "5", shared_runners: "no", value: 2 }]
246
+ end
247
+ let(:expected_created_builds) do
248
+ [{ namespace: "1", shared_runners: "no", value: 10 },
249
+ { namespace: "2", shared_runners: "no", value: 20 }]
250
+ end
251
+ let(:expected_per_runner) do
252
+ [{ runner: "1", shared_runner: "yes", namespace: "1", scheduled: "yes", triggered: "no", value: 15 },
253
+ { runner: "2", shared_runner: "no", namespace: "2", scheduled: "no", triggered: "yes", value: 5 },
254
+ { runner: "2", shared_runner: "no", namespace: "3", scheduled: "no", triggered: "yes", value: 5 },
255
+ { runner: "3", shared_runner: "no", namespace: "4", scheduled: "no", triggered: "yes", value: 5 }]
256
+ end
257
+ let(:expected_repeated_commands) do
258
+ [{ namespace: "1", project: "1", shared_runners: "yes", status: "pending", value: 10 },
259
+ { namespace: "2", project: "2", shared_runners: "no", status: "running", value: 20 },
260
+ { namespace: "1", project: "3", shared_runners: "no", status: "pending", value: 30 },
261
+ { namespace: "2", project: "4", shared_runners: "yes", status: "running", value: 40 }]
262
+ end
263
+
264
+ before do
265
+ stub_ce
266
+ end
267
+
268
+ it_behaves_like "data collector"
269
+ end
270
+ end
271
+
272
+ describe GitLab::Exporter::Database::CiBuildsProber do
273
+ let(:writer) { StringIO.new }
274
+ let(:prober) do
275
+ opts = { connection_string: "host=localhost",
276
+ allowed_repeated_commands_count: allowed_repeated_commands_count,
277
+ created_builds_counting_disabled: created_builds_counting_disabled,
278
+ unarchived_traces_offset_minutes: unarchived_traces_offset_minutes }
279
+ described_class.new(opts,
280
+ metrics: GitLab::Exporter::PrometheusMetrics.new(include_timestamp: false))
281
+ end
282
+
283
+ before do
284
+ allow_any_instance_of(GitLab::Exporter::Database::CiBuildsCollector).to receive(:connected?).and_return(true)
285
+ end
286
+
287
+ shared_examples "metrics server" do
288
+ subject do
289
+ prober.probe_db
290
+ prober.write_to(writer)
291
+ writer.string
292
+ end
293
+
294
+ context "when PG exceptions aren't raised" do
295
+ context "when created_builds_counting_disabled is set to false" do
296
+ let(:created_builds_counting_disabled) { false }
297
+
298
+ it "responds with created builds Prometheus metrics" do
299
+ ci_created_builds_expected_lines.each do |expected_line|
300
+ expect(subject).to match(Regexp.new("^#{expected_line}$", Regexp::MULTILINE))
301
+ end
302
+ end
303
+ end
304
+
305
+ context "when created_builds_counting_disabled is set to true" do
306
+ let(:created_builds_counting_disabled) { true }
307
+
308
+ it "doesn't respond with created builds Prometheus metrics" do
309
+ ci_created_builds_expected_lines.each do |expected_line|
310
+ expect(subject).not_to match(Regexp.new("^#{expected_line}$", Regexp::MULTILINE))
311
+ end
312
+ end
313
+ end
314
+
315
+ it "responds with pending builds Prometheus metrics" do
316
+ ci_pending_builds_expected_lines.each do |expected_line|
317
+ expect(subject).to match(Regexp.new("^#{expected_line}$", Regexp::MULTILINE))
318
+ end
319
+ end
320
+
321
+ it "responds with running builds Prometheus metrics" do
322
+ ci_running_builds_expected_lines.each do |expected_line|
323
+ expect(subject).to match(Regexp.new("^#{expected_line}$", Regexp::MULTILINE))
324
+ end
325
+ end
326
+
327
+ it "responds with repeated commands Prometheus metrics" do
328
+ ci_repeated_commands_builds_lines.each do |expected_line|
329
+ expect(subject).to match(Regexp.new("^#{expected_line}$", Regexp::MULTILINE))
330
+ end
331
+ end
332
+
333
+ it "responds with stale builds Prometheus metrics" do
334
+ expect(subject).to match(/^ci_stale_builds 2$/m)
335
+ end
336
+
337
+ it "responds with unarchived traces Prometheus metrics" do
338
+ expect(subject).to match(/^ci_unarchived_traces 10$/m)
339
+ end
340
+ end
341
+
342
+ context "when PG exceptions are raised" do
343
+ before do
344
+ allow(connection).to receive(:exec).and_raise(PG::UndefinedColumn)
345
+ end
346
+
347
+ it "responds with Prometheus metrics" do
348
+ prober.probe_db
349
+ prober.write_to(writer)
350
+ output = writer.string
351
+
352
+ expect(output).to match(/^ci_stale_builds 0$/m)
353
+ end
354
+ end
355
+ end
356
+
357
+ context "when executed on EE" do
358
+ let(:ci_created_builds_expected_lines) do
359
+ ['ci_created_builds\{has_minutes="no",namespace="1",shared_runners="no"\} 10',
360
+ 'ci_created_builds\{has_minutes="no",namespace="2",shared_runners="no"\} 20']
361
+ end
362
+ let(:ci_pending_builds_expected_lines) do
363
+ ['ci_pending_builds\{has_minutes="yes",namespace="1",shared_runners="yes"\} 30',
364
+ 'ci_pending_builds\{has_minutes="yes",namespace="2",shared_runners="yes"\} 50',
365
+ 'ci_pending_builds\{has_minutes="no",namespace="",shared_runners="yes"\} 1',
366
+ 'ci_pending_builds\{has_minutes="yes",namespace="",shared_runners="yes"\} 2',
367
+ 'ci_pending_builds\{has_minutes="no",namespace="",shared_runners="no"\} 2']
368
+ end
369
+ let(:ci_running_builds_expected_lines) do
370
+ ['ci_running_builds\{has_minutes="yes",mirror="no",mirror_trigger_builds="no",namespace="1",runner="1",scheduled="yes",shared_runner="yes",triggered="no"\} 15',
371
+ 'ci_running_builds\{has_minutes="no",mirror="yes",mirror_trigger_builds="yes",namespace="",runner="2",scheduled="no",shared_runner="no",triggered="yes"\} 5',
372
+ 'ci_running_builds\{has_minutes="yes",mirror="yes",mirror_trigger_builds="yes",namespace="",runner="2",scheduled="no",shared_runner="no",triggered="yes"\} 5',
373
+ 'ci_running_builds\{has_minutes="no",mirror="yes",mirror_trigger_builds="yes",namespace="",runner="3",scheduled="no",shared_runner="no",triggered="yes"\} 5']
374
+ end
375
+ let(:ci_repeated_commands_builds_lines) do
376
+ ['ci_repeated_commands_builds\{namespace="1",project="1",shared_runners="yes",status="pending",has_minutes="yes"\} 10',
377
+ 'ci_repeated_commands_builds\{namespace="2",project="2",shared_runners="no",status="running",has_minutes="no"\} 20',
378
+ 'ci_repeated_commands_builds\{namespace="1",project="3",shared_runners="no",status="pending",has_minutes="yes"\} 30',
379
+ 'ci_repeated_commands_builds\{namespace="2",project="4",shared_runners="yes",status="running",has_minutes="no"\} 40']
380
+ end
381
+ let(:namespace_out_of_limit) { 2 }
382
+
383
+ before do
384
+ stub_ee
385
+ end
386
+
387
+ it_behaves_like "metrics server"
388
+ end
389
+
390
+ context "when executed on CE" do
391
+ let(:ci_created_builds_expected_lines) do
392
+ ['ci_created_builds\{namespace="1",shared_runners="no"\} 10',
393
+ 'ci_created_builds\{namespace="2",shared_runners="no"\} 20']
394
+ end
395
+ let(:ci_pending_builds_expected_lines) do
396
+ ['ci_pending_builds\{namespace="1",shared_runners="yes"\} 30',
397
+ 'ci_pending_builds\{namespace="2",shared_runners="yes"\} 50',
398
+ 'ci_pending_builds\{namespace="",shared_runners="yes"\} 3',
399
+ 'ci_pending_builds\{namespace="",shared_runners="no"\} 2']
400
+ end
401
+ let(:ci_running_builds_expected_lines) do
402
+ ['ci_running_builds\{namespace="1",runner="1",scheduled="yes",shared_runner="yes",triggered="no"\} 15',
403
+ 'ci_running_builds\{namespace="",runner="2",scheduled="no",shared_runner="no",triggered="yes"\} 10',
404
+ 'ci_running_builds\{namespace="",runner="3",scheduled="no",shared_runner="no",triggered="yes"\} 5']
405
+ end
406
+ let(:ci_repeated_commands_builds_lines) do
407
+ ['ci_repeated_commands_builds\{namespace="1",project="1",shared_runners="yes",status="pending"\} 10',
408
+ 'ci_repeated_commands_builds\{namespace="2",project="2",shared_runners="no",status="running"\} 20',
409
+ 'ci_repeated_commands_builds\{namespace="1",project="3",shared_runners="no",status="pending"\} 30',
410
+ 'ci_repeated_commands_builds\{namespace="2",project="4",shared_runners="yes",status="running"\} 40']
411
+ end
412
+ let(:namespace_out_of_limit) { 0 }
413
+
414
+ before do
415
+ stub_ce
416
+ end
417
+
418
+ it_behaves_like "metrics server"
419
+ end
420
+ end
421
+ end