gitlab-monitor 4.0.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 +7 -0
- data/.gitignore +2 -0
- data/.gitlab-ci.yml +18 -0
- data/.rubocop.yml +34 -0
- data/CONTRIBUTING.md +651 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +75 -0
- data/LICENSE +25 -0
- data/README.md +110 -0
- data/bin/gitlab-mon +17 -0
- data/config/gitlab-monitor.yml.example +112 -0
- data/gitlab-monitor.gemspec +33 -0
- data/lib/gitlab_monitor.rb +18 -0
- data/lib/gitlab_monitor/cli.rb +341 -0
- data/lib/gitlab_monitor/database.rb +13 -0
- data/lib/gitlab_monitor/database/base.rb +44 -0
- data/lib/gitlab_monitor/database/bloat.rb +74 -0
- data/lib/gitlab_monitor/database/bloat_btree.sql +84 -0
- data/lib/gitlab_monitor/database/bloat_table.sql +63 -0
- data/lib/gitlab_monitor/database/ci_builds.rb +527 -0
- data/lib/gitlab_monitor/database/remote_mirrors.rb +74 -0
- data/lib/gitlab_monitor/database/row_count.rb +164 -0
- data/lib/gitlab_monitor/database/tuple_stats.rb +53 -0
- data/lib/gitlab_monitor/git.rb +144 -0
- data/lib/gitlab_monitor/memstats.rb +98 -0
- data/lib/gitlab_monitor/memstats/mapping.rb +91 -0
- data/lib/gitlab_monitor/prober.rb +40 -0
- data/lib/gitlab_monitor/process.rb +122 -0
- data/lib/gitlab_monitor/prometheus.rb +64 -0
- data/lib/gitlab_monitor/sidekiq.rb +149 -0
- data/lib/gitlab_monitor/sidekiq_queue_job_stats.lua +42 -0
- data/lib/gitlab_monitor/util.rb +83 -0
- data/lib/gitlab_monitor/version.rb +5 -0
- data/lib/gitlab_monitor/web_exporter.rb +77 -0
- data/spec/cli_spec.rb +31 -0
- data/spec/database/bloat_spec.rb +99 -0
- data/spec/database/ci_builds_spec.rb +421 -0
- data/spec/database/row_count_spec.rb +37 -0
- data/spec/fixtures/smaps/sample.txt +10108 -0
- data/spec/git_process_proper_spec.rb +27 -0
- data/spec/git_spec.rb +52 -0
- data/spec/memstats_spec.rb +28 -0
- data/spec/prometheus_metrics_spec.rb +17 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/util_spec.rb +15 -0
- metadata +225 -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 Monitor
|
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,77 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "English"
|
3
|
+
|
4
|
+
module GitLab
|
5
|
+
module Monitor
|
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
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "gitlab_monitor/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::Monitor::CLI do
|
12
|
+
it "returns the rigth parser" do
|
13
|
+
expect(GitLab::Monitor::CLI.for("git")).to be(GitLab::Monitor::CLI::GIT)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns a null parser if it is not found" do
|
17
|
+
expect(GitLab::Monitor::CLI.for("invalid")).to be(GitLab::Monitor::CLI::NullRunner)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe GitLab::Monitor::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::Monitor::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_monitor/database/bloat"
|
3
|
+
|
4
|
+
describe GitLab::Monitor::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::Monitor::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_monitor/database/ci_builds"
|
3
|
+
|
4
|
+
# rubocop:disable Metrics/LineLength
|
5
|
+
describe GitLab::Monitor::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::Monitor::Database::CiBuildsCollector::SET_RANDOM_PAGE_COST", set_random_page_cost_query)
|
84
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::BUILDS_QUERY_EE", builds_query_ee)
|
85
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::BUILDS_QUERY_CE", builds_query_ce)
|
86
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::STALE_BUILDS_QUERY", stale_builds_query)
|
87
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::PER_RUNNER_QUERY_EE", per_runner_query_ee)
|
88
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::PER_RUNNER_QUERY_CE", per_runner_query_ce)
|
89
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::MIRROR_COLUMN_QUERY", mirror_column_query)
|
90
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::REPEATED_COMMANDS_QUERY_EE", repeated_commands_query_ee)
|
91
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::REPEATED_COMMANDS_QUERY_CE", repeated_commands_query_ce)
|
92
|
+
stub_const("GitLab::Monitor::Database::CiBuildsCollector::UNARCHIVED_TRACES_QUERY", unarchived_traces_query)
|
93
|
+
|
94
|
+
allow_any_instance_of(GitLab::Monitor::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::Monitor::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::Monitor::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::Monitor::PrometheusMetrics.new(include_timestamp: false))
|
281
|
+
end
|
282
|
+
|
283
|
+
before do
|
284
|
+
allow_any_instance_of(GitLab::Monitor::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
|