gitlab-exporter 17.0.1 → 17.1.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 +6 -6
- data/README.md +59 -2
- data/config/gitlab-exporter.yml.example +3 -0
- data/gitlab-exporter.gemspec +1 -1
- data/lib/gitlab_exporter/database/ci_builds.rb +41 -0
- data/lib/gitlab_exporter/database/geo_node_status.rb +116 -0
- data/lib/gitlab_exporter/database.rb +1 -0
- data/lib/gitlab_exporter/version.rb +1 -1
- data/spec/database/ci_builds_spec.rb +52 -0
- data/spec/database/geo_node_status_spec.rb +210 -0
- data/spec/prometheus_metrics_spec.rb +20 -6
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df145039780458bab34b436ca149dce37c4c4f4a08e1707b606f70dc4aa021a4
|
|
4
|
+
data.tar.gz: 272f4e78ffb5901954f086e58a158087328d0478deb18114fb748504e067796b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a223ec3d899fcc85eeb391e100cce3942aeb050264cc03c6877ad6387f09234461408f45f1a8a82128c657b240f4ce2768c35a1d58857d4c3fb212f42090f28c
|
|
7
|
+
data.tar.gz: 992f12485329239412a793b67c556c7226535765d3ecfea83271e055c679c75d7d1d0af8b0be71e83408a0aacc77ceb0e3d9079281725d26b04f86955e01baf7
|
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab-exporter (17.0
|
|
4
|
+
gitlab-exporter (17.1.0)
|
|
5
5
|
base64 (= 0.3.0)
|
|
6
6
|
connection_pool (= 2.5.5)
|
|
7
7
|
deep_merge (~> 1.2.2)
|
|
8
|
-
faraday (= 2.14.
|
|
8
|
+
faraday (= 2.14.4)
|
|
9
9
|
pg (= 1.6.3)
|
|
10
10
|
quantile (= 0.2.1)
|
|
11
11
|
redis (= 4.8.1)
|
|
@@ -21,7 +21,7 @@ GEM
|
|
|
21
21
|
connection_pool (2.5.5)
|
|
22
22
|
deep_merge (1.2.2)
|
|
23
23
|
diff-lcs (1.5.0)
|
|
24
|
-
faraday (2.14.
|
|
24
|
+
faraday (2.14.4)
|
|
25
25
|
faraday-net_http (>= 2.0, < 3.5)
|
|
26
26
|
json
|
|
27
27
|
logger
|
|
@@ -57,8 +57,8 @@ GEM
|
|
|
57
57
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
58
58
|
rspec-support (~> 3.12.0)
|
|
59
59
|
rspec-support (3.12.0)
|
|
60
|
-
rubocop (1.
|
|
61
|
-
json (
|
|
60
|
+
rubocop (1.91.0)
|
|
61
|
+
json (>= 2.3)
|
|
62
62
|
language_server-protocol (~> 3.17.0.2)
|
|
63
63
|
lint_roller (~> 1.1.0)
|
|
64
64
|
parallel (>= 1.10)
|
|
@@ -91,4 +91,4 @@ DEPENDENCIES
|
|
|
91
91
|
rubocop (~> 1.0)
|
|
92
92
|
|
|
93
93
|
BUNDLED WITH
|
|
94
|
-
4.0.
|
|
94
|
+
4.0.18
|
data/README.md
CHANGED
|
@@ -23,14 +23,20 @@ metrics.
|
|
|
23
23
|
`gitlab_pg_sequences_min_value`, `gitlab_pg_sequences_max_value`, `gitlab_pg_sequences_current_value`
|
|
24
24
|
* [Row count queries](lib/gitlab_exporter/database/row_count.rb) --
|
|
25
25
|
`gitlab_database_rows`
|
|
26
|
-
* [CI builds](lib/gitlab_exporter/database/ci_builds.rb) --
|
|
26
|
+
* [CI builds](lib/gitlab_exporter/database/ci_builds.rb) (**deprecated**) --
|
|
27
27
|
`ci_pending_builds`, `ci_created_builds`, `ci_stale_builds`,
|
|
28
|
-
`ci_running_builds`
|
|
28
|
+
`ci_running_builds`, `ci_unarchived_traces`
|
|
29
|
+
(see [Deprecated: CI builds metrics](#deprecated-ci-builds-metrics))
|
|
29
30
|
* [Bloat](lib/gitlab_exporter/database/bloat.rb) --
|
|
30
31
|
`gitlab_database_bloat_$type_$key` with type `btree` (index bloat) or `table`
|
|
31
32
|
(table bloat) and keys `bloat_ratio bloat_size extra_size real_size` (see below)
|
|
32
33
|
* [Remote mirrors](lib/gitlab_exporter/database/remote_mirrors.rb) --
|
|
33
34
|
`project_remote_mirror_last_successful_update_time_seconds`, `project_remote_mirror_last_update_time_seconds`
|
|
35
|
+
* [Geo node status](lib/gitlab_exporter/database/geo_node_status.rb) -- Geo metrics from
|
|
36
|
+
`geo_node_statuses`, emitted as `geo_<status_field>` gauges with trailing `_count` and
|
|
37
|
+
`_total` suffixes removed, matching Rails' exported metric fields and `name`/`url` labels.
|
|
38
|
+
Usage-data counters are excluded. Enable the `geo_node_status` probe in the `database`
|
|
39
|
+
endpoint configuration.
|
|
34
40
|
* [Zoekt](lib/gitlab_exporter/database/zoekt.rb) --
|
|
35
41
|
`search_zoekt_task_processing_queue_size`
|
|
36
42
|
1. Git
|
|
@@ -133,6 +139,57 @@ Also see the [original documentation](https://github.com/ioguix/pgsql-bloat-esti
|
|
|
133
139
|
Note that all metrics returned are estimates without an upper bound for
|
|
134
140
|
the error.
|
|
135
141
|
|
|
142
|
+
### Deprecated: CI builds metrics
|
|
143
|
+
|
|
144
|
+
The [CI builds probe](lib/gitlab_exporter/database/ci_builds.rb) is deprecated and
|
|
145
|
+
will be removed in a future major version. It collects its metrics by querying
|
|
146
|
+
`p_ci_builds` directly, which puts avoidable load on tables that job scheduling
|
|
147
|
+
already loads heavily, and it has had almost no maintenance since 2021. GitLab
|
|
148
|
+
Rails and GitLab Runner instrument most of the same information by counting
|
|
149
|
+
events as they happen; GitLab.com stopped using this probe for CI state years
|
|
150
|
+
ago.
|
|
151
|
+
|
|
152
|
+
The probe prints a deprecation notice on every run, and each of its metrics
|
|
153
|
+
carries `(deprecated, ...)` in its `# HELP` line, so the warning reaches
|
|
154
|
+
Prometheus and Grafana and not only the exporter's log.
|
|
155
|
+
|
|
156
|
+
Replacements, where one exists:
|
|
157
|
+
|
|
158
|
+
| Deprecated metric | Replacement |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `ci_pending_builds` | `gitlab_ci_current_queue_size` (Rails, gauge, by `runner_type`) |
|
|
161
|
+
| `ci_running_builds` | `gitlab_runner_jobs` (Runner, gauge, by `runner`, `state`, `stage`) |
|
|
162
|
+
| `ci_created_builds` | None. A `created` build is not queued yet, so nothing counts it. |
|
|
163
|
+
| `ci_stale_builds` | None. The nearest signal is `gitlab_ci_queue_operations_total{operation="build_status_stale"}` (Rails, counter), which counts stale rows met while queueing rather than how many are stale now. |
|
|
164
|
+
| `ci_unarchived_traces` | None. |
|
|
165
|
+
|
|
166
|
+
The replacements are not a like-for-like relabel: the Rails queue metrics are
|
|
167
|
+
keyed by `runner_type` and the Runner metrics by runner, so neither carries the
|
|
168
|
+
`namespace` label this probe reports. Queue health is better served by the Rails
|
|
169
|
+
metrics that have no exporter equivalent at all -- `job_queue_duration_seconds`
|
|
170
|
+
(how long jobs wait), `job_register_attempts_total` and
|
|
171
|
+
`job_register_attempts_failed_total`, and `gitlab_ci_queue_operations_total`.
|
|
172
|
+
Those four are always instrumented.
|
|
173
|
+
|
|
174
|
+
**`gitlab_ci_current_queue_size`, and every other `gitlab_ci_queue_*` metric, is
|
|
175
|
+
behind the `gitlab_ci_builds_queuing_metrics` ops feature flag, which is disabled
|
|
176
|
+
by default.** Enable it before pointing a dashboard at them, or the replacement
|
|
177
|
+
panels stay empty:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
Feature.enable(:gitlab_ci_builds_queuing_metrics)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Metric definitions live in
|
|
184
|
+
[`lib/gitlab/ci/queue/metrics.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/queue/metrics.rb)
|
|
185
|
+
(Rails) and
|
|
186
|
+
[`commands/builds_helper.go`](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/main/commands/builds_helper.go)
|
|
187
|
+
(Runner). See
|
|
188
|
+
[GitLab Prometheus metrics](https://docs.gitlab.com/administration/monitoring/prometheus/gitlab_metrics/)
|
|
189
|
+
for enabling the Rails exporter, and
|
|
190
|
+
[Monitoring runners](https://docs.gitlab.com/runner/monitoring/) for the
|
|
191
|
+
Runner's.
|
|
192
|
+
|
|
136
193
|
## Contributing
|
|
137
194
|
|
|
138
195
|
gitlab-exporter is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
|
data/gitlab-exporter.gemspec
CHANGED
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
|
|
28
28
|
s.add_runtime_dependency "base64", "0.3.0"
|
|
29
29
|
s.add_runtime_dependency "connection_pool", "2.5.5"
|
|
30
30
|
s.add_runtime_dependency "deep_merge", "~> 1.2.2"
|
|
31
|
-
s.add_runtime_dependency "faraday", "2.14.
|
|
31
|
+
s.add_runtime_dependency "faraday", "2.14.4"
|
|
32
32
|
s.add_runtime_dependency "pg", "1.6.3"
|
|
33
33
|
s.add_runtime_dependency "quantile", "0.2.1"
|
|
34
34
|
s.add_runtime_dependency "redis", "4.8.1"
|
|
@@ -294,12 +294,53 @@ module GitLab
|
|
|
294
294
|
|
|
295
295
|
# The prober which is called when gathering metrics
|
|
296
296
|
class CiBuildsProber
|
|
297
|
+
# The stdout notice in #probe_db reaches whoever reads the exporter's log,
|
|
298
|
+
# which is not who reads the metrics. HELP is the one channel that travels
|
|
299
|
+
# with the sample to the scraper, so the deprecation rides there too: it
|
|
300
|
+
# survives into Prometheus and shows up in Grafana's metric browser.
|
|
301
|
+
DEPRECATED = "(deprecated, to be removed in a future major version)".freeze
|
|
302
|
+
REPLACEMENT = "See the CI builds entry in README.md for replacements.".freeze
|
|
303
|
+
|
|
304
|
+
PrometheusMetrics.describe(
|
|
305
|
+
"ci_created_builds",
|
|
306
|
+
"#{DEPRECATED} Number of CI builds in the created state, by namespace. #{REPLACEMENT}",
|
|
307
|
+
"gauge"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
PrometheusMetrics.describe(
|
|
311
|
+
"ci_pending_builds",
|
|
312
|
+
"#{DEPRECATED} Number of CI builds in the pending state, by namespace. #{REPLACEMENT}",
|
|
313
|
+
"gauge"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
PrometheusMetrics.describe(
|
|
317
|
+
"ci_stale_builds",
|
|
318
|
+
"#{DEPRECATED} Number of running CI builds not updated for over an hour. #{REPLACEMENT}",
|
|
319
|
+
"gauge"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
PrometheusMetrics.describe(
|
|
323
|
+
"ci_running_builds",
|
|
324
|
+
"#{DEPRECATED} Number of running CI builds, by runner and namespace. #{REPLACEMENT}",
|
|
325
|
+
"gauge"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
PrometheusMetrics.describe(
|
|
329
|
+
"ci_unarchived_traces",
|
|
330
|
+
"#{DEPRECATED} Number of finished CI builds whose trace is not archived. #{REPLACEMENT}",
|
|
331
|
+
"gauge"
|
|
332
|
+
)
|
|
333
|
+
|
|
297
334
|
def initialize(metrics: PrometheusMetrics.new, **opts)
|
|
298
335
|
@metrics = metrics
|
|
299
336
|
@collector = CiBuildsCollector.new(**opts)
|
|
300
337
|
end
|
|
301
338
|
|
|
302
339
|
def probe_db
|
|
340
|
+
puts "[DEPRECATED] probe_db and CiBuildsProber are now considered obsolete"\
|
|
341
|
+
" and will be removed in a future major version,"\
|
|
342
|
+
" please use the CI/CD queuing metrics emitted by GitLab Rails instead"
|
|
343
|
+
|
|
303
344
|
@results = @collector.run
|
|
304
345
|
|
|
305
346
|
ci_builds_metrics(@results[:created_builds], "ci_created_builds") if @results[:created_builds]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module GitLab
|
|
2
|
+
module Exporter
|
|
3
|
+
module Database
|
|
4
|
+
# A helper class to collect Geo node status metrics.
|
|
5
|
+
class GeoNodeStatusCollector < Base
|
|
6
|
+
QUERY = <<~SQL.freeze
|
|
7
|
+
SELECT
|
|
8
|
+
geo_nodes.name AS geo_node_name,
|
|
9
|
+
geo_node_statuses.db_replication_lag_seconds,
|
|
10
|
+
geo_node_statuses.replication_slots_count,
|
|
11
|
+
geo_node_statuses.replication_slots_used_count,
|
|
12
|
+
geo_node_statuses.replication_slots_max_retained_wal_bytes,
|
|
13
|
+
geo_node_statuses.last_event_id,
|
|
14
|
+
geo_node_statuses.cursor_last_event_id,
|
|
15
|
+
geo_node_statuses.status,
|
|
16
|
+
EXTRACT(EPOCH FROM geo_node_statuses.last_event_date)::bigint AS last_event_timestamp,
|
|
17
|
+
EXTRACT(EPOCH FROM geo_node_statuses.cursor_last_event_date)::bigint AS cursor_last_event_timestamp,
|
|
18
|
+
EXTRACT(EPOCH FROM geo_node_statuses.last_successful_status_check_at)::bigint
|
|
19
|
+
AS last_successful_status_check_timestamp
|
|
20
|
+
FROM geo_node_statuses
|
|
21
|
+
INNER JOIN geo_nodes ON geo_nodes.id = geo_node_statuses.geo_node_id
|
|
22
|
+
WHERE geo_nodes.enabled = TRUE
|
|
23
|
+
SQL
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
with_connection_pool do |conn|
|
|
27
|
+
conn.exec(QUERY)
|
|
28
|
+
end
|
|
29
|
+
rescue PG::UndefinedTable, PG::UndefinedColumn
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The prober which is called when gathering metrics.
|
|
35
|
+
class GeoNodeStatusProber
|
|
36
|
+
METRIC_PREFIX = "geo_".freeze
|
|
37
|
+
COUNT_SUFFIX = /_(count|total)\z/
|
|
38
|
+
METRIC_ALIASES = { "project_repositories_count" => "repositories_count" }.freeze
|
|
39
|
+
COLUMN_METRICS = %w[
|
|
40
|
+
db_replication_lag_seconds
|
|
41
|
+
replication_slots_count
|
|
42
|
+
replication_slots_used_count
|
|
43
|
+
replication_slots_max_retained_wal_bytes
|
|
44
|
+
last_event_id
|
|
45
|
+
cursor_last_event_id
|
|
46
|
+
].freeze
|
|
47
|
+
TIMESTAMP_METRICS = %w[
|
|
48
|
+
last_event_timestamp
|
|
49
|
+
cursor_last_event_timestamp
|
|
50
|
+
last_successful_status_check_timestamp
|
|
51
|
+
].freeze
|
|
52
|
+
REPLICATOR_METRIC_SUFFIXES = %w[
|
|
53
|
+
_count
|
|
54
|
+
_checksum_total_count
|
|
55
|
+
_checksummed_count
|
|
56
|
+
_checksum_failed_count
|
|
57
|
+
_synced_count
|
|
58
|
+
_failed_count
|
|
59
|
+
_registry_count
|
|
60
|
+
_verification_total_count
|
|
61
|
+
_verified_count
|
|
62
|
+
_verification_failed_count
|
|
63
|
+
_oldest_unsynced_time
|
|
64
|
+
].freeze
|
|
65
|
+
NON_PROMETHEUS_STATUS_FIELDS = %w[projects_count].freeze
|
|
66
|
+
|
|
67
|
+
def initialize(metrics: PrometheusMetrics.new, **opts)
|
|
68
|
+
@metrics = metrics
|
|
69
|
+
@collector = opts[:collector] || GeoNodeStatusCollector.new(**opts)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def probe_db
|
|
73
|
+
@collector.run.to_a.each do |row|
|
|
74
|
+
labels = {
|
|
75
|
+
name: row["geo_node_name"],
|
|
76
|
+
url: row["geo_node_name"]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
row.fetch("status", {}).to_h.each do |name, value|
|
|
80
|
+
add_metric(name, value, labels) if value.is_a?(Integer) && prometheus_metric?(name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
(COLUMN_METRICS + TIMESTAMP_METRICS).each do |name|
|
|
84
|
+
add_metric(name, row[name], labels) if row[name].is_a?(Integer)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
self
|
|
89
|
+
rescue PG::ConnectionBad
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_to(target)
|
|
94
|
+
target.write(@metrics.to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def prometheus_metric?(name)
|
|
100
|
+
name = name.to_s
|
|
101
|
+
return false if NON_PROMETHEUS_STATUS_FIELDS.include?(name)
|
|
102
|
+
return false if COLUMN_METRICS.include?(name)
|
|
103
|
+
|
|
104
|
+
REPLICATOR_METRIC_SUFFIXES.any? { |suffix| name.end_with?(suffix) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_metric(name, value, labels)
|
|
108
|
+
name = METRIC_ALIASES.fetch(name.to_s, name.to_s)
|
|
109
|
+
metric_name = "#{METRIC_PREFIX}#{name.sub(COUNT_SUFFIX, '')}"
|
|
110
|
+
PrometheusMetrics.describe(metric_name, "Geo node status metric", "gauge")
|
|
111
|
+
@metrics.add(metric_name, value, **labels)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -8,6 +8,7 @@ module GitLab
|
|
|
8
8
|
autoload :RowCountProber, "gitlab_exporter/database/row_count"
|
|
9
9
|
autoload :BloatProber, "gitlab_exporter/database/bloat"
|
|
10
10
|
autoload :RemoteMirrorsProber, "gitlab_exporter/database/remote_mirrors"
|
|
11
|
+
autoload :GeoNodeStatusProber, "gitlab_exporter/database/geo_node_status"
|
|
11
12
|
autoload :PgSequencesProber, "gitlab_exporter/database/pg_sequences"
|
|
12
13
|
autoload :ZoektProber, "gitlab_exporter/database/zoekt"
|
|
13
14
|
end
|
|
@@ -361,3 +361,55 @@ describe GitLab::Exporter::Database do
|
|
|
361
361
|
end
|
|
362
362
|
end
|
|
363
363
|
end
|
|
364
|
+
|
|
365
|
+
# Asserting on the notice keeps a later refactor of #probe_db from dropping it
|
|
366
|
+
# silently, which would leave operators unwarned before the probe goes (issue #119).
|
|
367
|
+
describe GitLab::Exporter::Database::CiBuildsProber do
|
|
368
|
+
let(:prober) do
|
|
369
|
+
described_class.new(connection_string: "host=localhost",
|
|
370
|
+
metrics: GitLab::Exporter::PrometheusMetrics.new(include_timestamp: false))
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
let(:metric_names) do
|
|
374
|
+
%w[ci_created_builds ci_pending_builds ci_stale_builds ci_running_builds ci_unarchived_traces]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it "warns that the probe is deprecated and names the replacement" do
|
|
378
|
+
allow_any_instance_of(GitLab::Exporter::Database::CiBuildsCollector).to receive(:run)
|
|
379
|
+
.and_return(pending_builds: [], stale_builds: 0, per_runner: [], unarchived_traces: 0)
|
|
380
|
+
|
|
381
|
+
expect { prober.probe_db }.to output(/\[DEPRECATED\].*CiBuildsProber.*GitLab Rails/m).to_stdout
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# The stdout notice never reaches the scraper. HELP does, so every metric the
|
|
385
|
+
# probe can emit has to carry the deprecation, not just the ones with a
|
|
386
|
+
# replacement.
|
|
387
|
+
it "describes every metric it can emit as deprecated" do
|
|
388
|
+
metric_names.each do |name|
|
|
389
|
+
expect(GitLab::Exporter::PrometheusMetrics.description(name))
|
|
390
|
+
.to start_with("(deprecated"), "#{name} is emitted without a deprecated HELP line"
|
|
391
|
+
expect(GitLab::Exporter::PrometheusMetrics.metric_type(name)).to eq("gauge")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it "renders the deprecation into the scrape output" do
|
|
396
|
+
allow_any_instance_of(GitLab::Exporter::Database::CiBuildsCollector).to receive(:run)
|
|
397
|
+
.and_return(pending_builds: [{ namespace: "1", shared_runners: "yes", value: 12 }],
|
|
398
|
+
stale_builds: 3, per_runner: [], unarchived_traces: 0)
|
|
399
|
+
|
|
400
|
+
output = StringIO.new
|
|
401
|
+
capture_stdout do prober.probe_db.write_to(output) end
|
|
402
|
+
|
|
403
|
+
expect(output.string).to include("# HELP ci_pending_builds (deprecated")
|
|
404
|
+
expect(output.string).to include("# TYPE ci_pending_builds gauge")
|
|
405
|
+
expect(output.string).to include(%(ci_pending_builds{namespace="1",shared_runners="yes"} 12.0))
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def capture_stdout
|
|
409
|
+
original = $stdout
|
|
410
|
+
$stdout = StringIO.new
|
|
411
|
+
yield
|
|
412
|
+
ensure
|
|
413
|
+
$stdout = original
|
|
414
|
+
end
|
|
415
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "gitlab_exporter/database/geo_node_status"
|
|
3
|
+
|
|
4
|
+
describe GitLab::Exporter::Database::GeoNodeStatusCollector do
|
|
5
|
+
let(:connection_pool) { double("connection pool") }
|
|
6
|
+
let(:connection) { double("connection") }
|
|
7
|
+
let(:results) { [{ "geo_node_name" => "secondary", "status" => { "projects_count" => 12 } }] }
|
|
8
|
+
|
|
9
|
+
subject(:collector) { described_class.new(connection_string: "host=localhost") }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
allow(collector).to receive(:connection_pool).and_return(connection_pool)
|
|
13
|
+
allow(connection_pool).to receive(:with).and_yield(connection)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "QUERY" do
|
|
17
|
+
it "selects all required status columns, timestamps, and node name" do
|
|
18
|
+
expect(described_class::QUERY).to include("geo_nodes.name AS geo_node_name")
|
|
19
|
+
expect(described_class::QUERY).to include("geo_node_statuses.db_replication_lag_seconds")
|
|
20
|
+
expect(described_class::QUERY).to include("geo_node_statuses.replication_slots_count")
|
|
21
|
+
expect(described_class::QUERY).to include("geo_node_statuses.replication_slots_used_count")
|
|
22
|
+
expect(described_class::QUERY).to include("geo_node_statuses.replication_slots_max_retained_wal_bytes")
|
|
23
|
+
expect(described_class::QUERY).to include("geo_node_statuses.last_event_id")
|
|
24
|
+
expect(described_class::QUERY).to include("geo_node_statuses.cursor_last_event_id")
|
|
25
|
+
expect(described_class::QUERY).to include("geo_node_statuses.status")
|
|
26
|
+
expect(described_class::QUERY).to include(
|
|
27
|
+
"EXTRACT(EPOCH FROM geo_node_statuses.last_event_date)::bigint AS last_event_timestamp"
|
|
28
|
+
)
|
|
29
|
+
expect(described_class::QUERY).to include(
|
|
30
|
+
"EXTRACT(EPOCH FROM geo_node_statuses.cursor_last_event_date)::bigint AS cursor_last_event_timestamp"
|
|
31
|
+
)
|
|
32
|
+
expect(described_class::QUERY).to include(
|
|
33
|
+
"EXTRACT(EPOCH FROM geo_node_statuses.last_successful_status_check_at)::bigint"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "#run" do
|
|
39
|
+
it "queries enabled Geo nodes and returns their status data" do
|
|
40
|
+
expect(connection).to receive(:exec).with(described_class::QUERY).and_return(results)
|
|
41
|
+
|
|
42
|
+
expect(collector.run).to eq(results)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "returns nil when Geo tables are unavailable" do
|
|
46
|
+
allow(connection).to receive(:exec).and_raise(PG::UndefinedTable)
|
|
47
|
+
|
|
48
|
+
expect(collector.run).to be_nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe GitLab::Exporter::Database::GeoNodeStatusProber do
|
|
54
|
+
let(:writer) { StringIO.new }
|
|
55
|
+
let(:metrics) { GitLab::Exporter::PrometheusMetrics.new(include_timestamp: false) }
|
|
56
|
+
let(:collector) { double(GitLab::Exporter::Database::GeoNodeStatusCollector, run: results) }
|
|
57
|
+
let(:results) do
|
|
58
|
+
[
|
|
59
|
+
{
|
|
60
|
+
"geo_node_id" => 7,
|
|
61
|
+
"geo_node_name" => "secondary",
|
|
62
|
+
"db_replication_lag_seconds" => 4,
|
|
63
|
+
"replication_slots_count" => 2,
|
|
64
|
+
"replication_slots_used_count" => 1,
|
|
65
|
+
"replication_slots_max_retained_wal_bytes" => 1024,
|
|
66
|
+
"last_event_id" => 100,
|
|
67
|
+
"cursor_last_event_id" => 98,
|
|
68
|
+
"last_event_timestamp" => 1_750_000_100,
|
|
69
|
+
"cursor_last_event_timestamp" => 1_750_000_090,
|
|
70
|
+
"last_successful_status_check_timestamp" => 1_750_000_000,
|
|
71
|
+
"status" => {
|
|
72
|
+
"projects_count" => 12,
|
|
73
|
+
"project_repositories_count" => 21,
|
|
74
|
+
"projects_verification_total_count" => 8,
|
|
75
|
+
"projects_synced_count" => 19,
|
|
76
|
+
"projects_failed_count" => 2,
|
|
77
|
+
"git_fetch_event_count_weekly" => 100,
|
|
78
|
+
"container_repositories_replication_enabled" => true,
|
|
79
|
+
"status_message" => "Healthy"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
subject(:prober) { described_class.new(metrics: metrics, collector: collector) }
|
|
86
|
+
|
|
87
|
+
describe "constants and data sources" do
|
|
88
|
+
it "defines the expected COLUMN_METRICS corresponding to table columns" do
|
|
89
|
+
expect(described_class::COLUMN_METRICS).to contain_exactly(
|
|
90
|
+
"db_replication_lag_seconds",
|
|
91
|
+
"replication_slots_count",
|
|
92
|
+
"replication_slots_used_count",
|
|
93
|
+
"replication_slots_max_retained_wal_bytes",
|
|
94
|
+
"last_event_id",
|
|
95
|
+
"cursor_last_event_id"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "defines the expected TIMESTAMP_METRICS corresponding to epoch extractions" do
|
|
100
|
+
expect(described_class::TIMESTAMP_METRICS).to contain_exactly(
|
|
101
|
+
"last_event_timestamp",
|
|
102
|
+
"cursor_last_event_timestamp",
|
|
103
|
+
"last_successful_status_check_timestamp"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "does not define CORE_STATUS_METRICS" do
|
|
108
|
+
expect(described_class.const_defined?(:CORE_STATUS_METRICS)).to be false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "does not emit column metrics if placed only inside status jsonb" do
|
|
112
|
+
allow(collector).to receive(:run).and_return([
|
|
113
|
+
{
|
|
114
|
+
"geo_node_name" => "secondary",
|
|
115
|
+
"status" => {
|
|
116
|
+
"db_replication_lag_seconds" => 4,
|
|
117
|
+
"replication_slots_count" => 2,
|
|
118
|
+
"replication_slots_used_count" => 1,
|
|
119
|
+
"replication_slots_max_retained_wal_bytes" => 1024,
|
|
120
|
+
"last_event_id" => 100,
|
|
121
|
+
"cursor_last_event_id" => 98
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
prober.probe_db
|
|
127
|
+
prober.write_to(writer)
|
|
128
|
+
|
|
129
|
+
expect(writer.string).not_to include("geo_db_replication_lag_seconds")
|
|
130
|
+
expect(writer.string).not_to include("geo_replication_slots")
|
|
131
|
+
expect(writer.string).not_to include("geo_replication_slots_used")
|
|
132
|
+
expect(writer.string).not_to include("geo_replication_slots_max_retained_wal_bytes")
|
|
133
|
+
expect(writer.string).not_to include("geo_last_event_id")
|
|
134
|
+
expect(writer.string).not_to include("geo_cursor_last_event_id")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "does not emit replicator metrics if placed only in row columns outside status jsonb" do
|
|
138
|
+
allow(collector).to receive(:run).and_return([
|
|
139
|
+
{
|
|
140
|
+
"geo_node_name" => "secondary",
|
|
141
|
+
"project_repositories_count" => 21,
|
|
142
|
+
"projects_synced_count" => 19,
|
|
143
|
+
"status" => {}
|
|
144
|
+
}
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
prober.probe_db
|
|
148
|
+
prober.write_to(writer)
|
|
149
|
+
|
|
150
|
+
expect(writer.string).not_to include("geo_repositories")
|
|
151
|
+
expect(writer.string).not_to include("geo_projects_synced")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe "#probe_db" do
|
|
156
|
+
it "exports integer Geo metrics with Rails-compatible names and site labels" do
|
|
157
|
+
prober.probe_db
|
|
158
|
+
prober.write_to(writer)
|
|
159
|
+
labels = 'name="secondary",url="secondary"'
|
|
160
|
+
|
|
161
|
+
expect(writer.string).to include("geo_db_replication_lag_seconds{#{labels}} 4")
|
|
162
|
+
expect(writer.string).to include("# TYPE geo_db_replication_lag_seconds gauge")
|
|
163
|
+
expect(writer.string).to include("geo_replication_slots{#{labels}} 2")
|
|
164
|
+
expect(writer.string).to include("# TYPE geo_replication_slots gauge")
|
|
165
|
+
expect(writer.string).to include("geo_replication_slots_used{#{labels}} 1")
|
|
166
|
+
expect(writer.string).to include("# TYPE geo_replication_slots_used gauge")
|
|
167
|
+
expect(writer.string).to include("geo_replication_slots_max_retained_wal_bytes{#{labels}} 1024")
|
|
168
|
+
expect(writer.string).to include("# TYPE geo_replication_slots_max_retained_wal_bytes gauge")
|
|
169
|
+
expect(writer.string).to include("geo_last_event_id{#{labels}} 100")
|
|
170
|
+
expect(writer.string).to include("geo_cursor_last_event_id{#{labels}} 98")
|
|
171
|
+
expect(writer.string).to include("geo_last_event_timestamp{#{labels}} 1750000100")
|
|
172
|
+
expect(writer.string).to include("geo_cursor_last_event_timestamp{#{labels}} 1750000090")
|
|
173
|
+
expect(writer.string).to include("geo_last_successful_status_check_timestamp{#{labels}} 1750000000")
|
|
174
|
+
expect(writer.string).to include("geo_repositories{#{labels}} 21")
|
|
175
|
+
expect(writer.string).to include("geo_projects_verification_total{#{labels}} 8")
|
|
176
|
+
expect(writer.string).to include("geo_projects_synced{#{labels}} 19")
|
|
177
|
+
expect(writer.string).to include("geo_projects_failed{#{labels}} 2")
|
|
178
|
+
|
|
179
|
+
expect(writer.string).not_to include("event_log_max_id")
|
|
180
|
+
expect(writer.string).not_to include("geo_projects{")
|
|
181
|
+
expect(writer.string).not_to include("geo_git_fetch_event")
|
|
182
|
+
expect(writer.string).not_to include("container_repositories_replication_enabled")
|
|
183
|
+
expect(writer.string).not_to include("status_message")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "handles nil column values gracefully" do
|
|
187
|
+
allow(collector).to receive(:run).and_return([
|
|
188
|
+
{
|
|
189
|
+
"geo_node_name" => "secondary",
|
|
190
|
+
"db_replication_lag_seconds" => nil,
|
|
191
|
+
"replication_slots_count" => nil,
|
|
192
|
+
"status" => { "project_repositories_count" => 5 }
|
|
193
|
+
}
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
prober.probe_db
|
|
197
|
+
prober.write_to(writer)
|
|
198
|
+
|
|
199
|
+
expect(writer.string).not_to include("geo_db_replication_lag_seconds")
|
|
200
|
+
expect(writer.string).not_to include("geo_replication_slots")
|
|
201
|
+
expect(writer.string).to include('geo_repositories{name="secondary",url="secondary"} 5')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "handles a missing status payload" do
|
|
205
|
+
allow(collector).to receive(:run).and_return([{ "geo_node_name" => "secondary" }])
|
|
206
|
+
|
|
207
|
+
expect { prober.probe_db }.not_to raise_error
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -29,15 +29,29 @@ describe GitLab::Exporter::PrometheusMetrics do
|
|
|
29
29
|
|
|
30
30
|
allow(Time).to receive(:now).and_return(time)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
# Descriptions are class-level state, and probes register theirs when their
|
|
33
|
+
# file loads, which happens once. Describing on a subclass keeps this example
|
|
34
|
+
# out of the shared registry: clearing it here would drop those registrations
|
|
35
|
+
# for every later example, and nothing can put them back.
|
|
36
|
+
metrics_class = Class.new(described_class)
|
|
37
|
+
metrics = metrics_class.new
|
|
38
|
+
|
|
39
|
+
metrics_class.describe("mymetric", "description")
|
|
40
|
+
metrics_class.describe("missingmetric", "otherdescription")
|
|
41
|
+
metrics.add("mymetric", 1.3, mylabel: "x", myotherlabel: "y")
|
|
42
|
+
|
|
43
|
+
expect(metrics.to_s).to eq(<<~METRICS)
|
|
37
44
|
# HELP mymetric description
|
|
38
45
|
mymetric{mylabel="x",myotherlabel="y"} 1.3 #{(time.to_f * 1000).to_i}
|
|
39
46
|
METRICS
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "clears descriptions" do
|
|
50
|
+
metrics_class = Class.new(described_class)
|
|
51
|
+
metrics_class.describe("mymetric", "description", "gauge")
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
expect { metrics_class.clear_descriptions }
|
|
54
|
+
.to change { metrics_class.description("mymetric") }.from("description").to(nil)
|
|
55
|
+
expect(metrics_class.metric_type("mymetric")).to be_nil
|
|
42
56
|
end
|
|
43
57
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-exporter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 17.0
|
|
4
|
+
version: 17.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pablo Carranza
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - '='
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 2.14.
|
|
61
|
+
version: 2.14.4
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - '='
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 2.14.
|
|
68
|
+
version: 2.14.4
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: pg
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -209,6 +209,7 @@ files:
|
|
|
209
209
|
- lib/gitlab_exporter/database/bloat_btree.sql
|
|
210
210
|
- lib/gitlab_exporter/database/bloat_table.sql
|
|
211
211
|
- lib/gitlab_exporter/database/ci_builds.rb
|
|
212
|
+
- lib/gitlab_exporter/database/geo_node_status.rb
|
|
212
213
|
- lib/gitlab_exporter/database/pg_sequences.rb
|
|
213
214
|
- lib/gitlab_exporter/database/remote_mirrors.rb
|
|
214
215
|
- lib/gitlab_exporter/database/row_count.rb
|
|
@@ -230,6 +231,7 @@ files:
|
|
|
230
231
|
- spec/cli_spec.rb
|
|
231
232
|
- spec/database/bloat_spec.rb
|
|
232
233
|
- spec/database/ci_builds_spec.rb
|
|
234
|
+
- spec/database/geo_node_status_spec.rb
|
|
233
235
|
- spec/database/pg_sequences_spec.rb
|
|
234
236
|
- spec/database/row_count_spec.rb
|
|
235
237
|
- spec/database/tuple_stats_spec.rb
|
|
@@ -274,6 +276,7 @@ test_files:
|
|
|
274
276
|
- spec/cli_spec.rb
|
|
275
277
|
- spec/database/bloat_spec.rb
|
|
276
278
|
- spec/database/ci_builds_spec.rb
|
|
279
|
+
- spec/database/geo_node_status_spec.rb
|
|
277
280
|
- spec/database/pg_sequences_spec.rb
|
|
278
281
|
- spec/database/row_count_spec.rb
|
|
279
282
|
- spec/database/tuple_stats_spec.rb
|