gitlab-exporter 16.7.0 → 16.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 050c12ce9d7b70227e38e416538371674bccb14e2e5f5f1fc256f290ba462d17
4
- data.tar.gz: ea71b320b31953a3926ae1d9f7674cba9081a836aa3e2435a699e65c3a6b5822
3
+ metadata.gz: 77bae663d5dff12133db411a27154cb55c100caf0bec1ecdd4dd4e8102c88994
4
+ data.tar.gz: d98c9128dc327d8757d4bb8d021cf863274f59588767ff877ce5ebe4c3274636
5
5
  SHA512:
6
- metadata.gz: 671f76c854145721fdc0597d4c2ab5b889570568de67c2f96670c4fc9cbb9e03cd6f315aee8c6375d3e2b13ecac11778007292e7f622081f63f3b5bf2a608bb1
7
- data.tar.gz: f4072dec64099fbf6d0a7852b07bb7711db2d887471d570cffa25dfbeaa13736d409f1c06beadb61dfae897479983c0a33f862fc7f2fdc64bb1cb94a389d0b46
6
+ metadata.gz: ff4b8dc86a0765ab98cf1e7ac97ee7016e09e1ec1e0a08db4366bdd9a66ae75dc34cb9d41ad5ed28668a6d279f641db780526ac3619b04b114f4c68aa29d8a74
7
+ data.tar.gz: a76a114f309436e969865d25cb3a0e7aa4b94d8239cb4d7f775958ef971d3a6747380d002adf8999d3a8be545ad3692980ae980dd5ed6e1a8533cc3f7bc87173
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-exporter (16.7.0)
4
+ gitlab-exporter (16.9.0)
5
5
  connection_pool (= 2.5.5)
6
6
  deep_merge (~> 1.2.2)
7
- faraday (= 2.14.1)
7
+ faraday (= 2.14.3)
8
8
  pg (= 1.6.3)
9
- puma (= 7.2.0)
9
+ puma (= 8.0.1)
10
10
  quantile (= 0.2.1)
11
11
  redis (= 4.8.1)
12
12
  redis-namespace (= 1.11.0)
@@ -22,7 +22,7 @@ GEM
22
22
  connection_pool (2.5.5)
23
23
  deep_merge (1.2.2)
24
24
  diff-lcs (1.5.0)
25
- faraday (2.14.1)
25
+ faraday (2.14.3)
26
26
  faraday-net_http (>= 2.0, < 3.5)
27
27
  json
28
28
  logger
@@ -40,7 +40,7 @@ GEM
40
40
  racc
41
41
  pg (1.6.3)
42
42
  prism (1.9.0)
43
- puma (7.2.0)
43
+ puma (8.0.1)
44
44
  nio4r (~> 2.0)
45
45
  quantile (0.2.1)
46
46
  racc (1.8.1)
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
24
24
 
25
25
  s.add_runtime_dependency "connection_pool", "2.5.5"
26
26
  s.add_runtime_dependency "deep_merge", "~> 1.2.2"
27
- s.add_runtime_dependency "faraday", "2.14.1"
27
+ s.add_runtime_dependency "faraday", "2.14.3"
28
28
  s.add_runtime_dependency "pg", "1.6.3"
29
- s.add_runtime_dependency "puma", "7.2.0"
29
+ s.add_runtime_dependency "puma", "8.0.1"
30
30
  s.add_runtime_dependency "quantile", "0.2.1"
31
31
  s.add_runtime_dependency "redis", "4.8.1"
32
32
  s.add_runtime_dependency "redis-namespace", "1.11.0"
@@ -1,7 +1,13 @@
1
1
  -- Originally from: https://github.com/ioguix/pgsql-bloat-estimation/blob/master/btree/btree_bloat.sql
2
2
  -- WARNING: executed with a non-superuser role, the query inspect only index on tables you are granted to read.
3
3
  -- WARNING: rows with is_na = 't' are known to have bad statistics ("name" type is not supported).
4
- -- This query is compatible with PostgreSQL 8.2 and after
4
+ -- This query is compatible with PostgreSQL 12 and after
5
+ -- pg_stats is materialized so the planner sees the true row count before planning the join,
6
+ -- avoiding a catastrophic cardinality underestimate on PostgreSQL 17.
7
+ WITH pg_stats_cached AS MATERIALIZED (
8
+ SELECT schemaname, tablename, attname, null_frac, avg_width
9
+ FROM pg_catalog.pg_stats
10
+ )
5
11
  SELECT current_database(), nspname AS schemaname, tblname, idxname AS object_name, bs*(relpages)::bigint AS real_size,
6
12
  bs*(relpages-est_pages)::bigint AS extra_size,
7
13
  100 * (relpages-est_pages)::float / relpages AS extra_ratio,
@@ -91,9 +97,9 @@ FROM (
91
97
  AND a2.attnum = ic.attpos
92
98
  ) i
93
99
  JOIN pg_catalog.pg_namespace n ON n.oid = i.relnamespace
94
- JOIN pg_catalog.pg_stats s ON s.schemaname = n.nspname
95
- AND s.tablename = i.attrelname
96
- AND s.attname = i.attname
100
+ JOIN pg_stats_cached s ON s.schemaname = n.nspname
101
+ AND s.tablename = i.attrelname
102
+ AND s.attname = i.attname
97
103
  GROUP BY 1,2,3,4,5,6,7,8,9,10,11
98
104
  ) AS rows_data_stats
99
105
  ) AS rows_hdr_pdg_stats
@@ -1,7 +1,14 @@
1
1
  -- Originally from: https://github.com/ioguix/pgsql-bloat-estimation/blob/master/table/table_bloat.sql
2
2
  /* WARNING: executed with a non-superuser role, the query inspect only tables and materialized view (9.3+) you are granted to read.
3
- * This query is compatible with PostgreSQL 9.0 and more
3
+ * This query is compatible with PostgreSQL 12 and later
4
4
  */
5
+ -- pg_stats is materialized so the planner sees the true row count before planning the join,
6
+ -- avoiding a catastrophic cardinality underestimate on PostgreSQL 17.
7
+ WITH pg_stats_cached AS MATERIALIZED (
8
+ SELECT schemaname, tablename, attname, null_frac, avg_width
9
+ FROM pg_stats
10
+ WHERE inherited = false
11
+ )
5
12
  SELECT current_database(), schemaname, tblname AS object_name, bs*tblpages AS real_size,
6
13
  (tblpages-est_tblpages)*bs AS extra_size,
7
14
  CASE WHEN tblpages - est_tblpages > 0
@@ -49,8 +56,8 @@ FROM (
49
56
  FROM pg_attribute AS att
50
57
  JOIN pg_class AS tbl ON att.attrelid = tbl.oid
51
58
  JOIN pg_namespace AS ns ON ns.oid = tbl.relnamespace
52
- LEFT JOIN pg_stats AS s ON s.schemaname=ns.nspname
53
- AND s.tablename = tbl.relname AND s.inherited=false AND s.attname=att.attname
59
+ LEFT JOIN pg_stats_cached AS s ON s.schemaname=ns.nspname
60
+ AND s.tablename = tbl.relname AND s.attname=att.attname
54
61
  LEFT JOIN pg_class AS toast ON tbl.reltoastrelid = toast.oid
55
62
  WHERE NOT att.attisdropped
56
63
  AND tbl.relkind in ('r','m')
@@ -2,6 +2,7 @@ module GitLab
2
2
  module Exporter
3
3
  module Database
4
4
  # A helper class to collect zoekt metrics.
5
+ # rubocop:disable Metrics/ClassLength
5
6
  class ZoektCollector < Base
6
7
  # Query to get processing zoekt_tasks distribution by zoekt_node_id
7
8
  ZOEKT_TASKS_PROCESSING_QUERY = <<~SQL.freeze
@@ -62,13 +63,26 @@ module GitLab
62
63
  LIMIT 1
63
64
  SQL
64
65
 
66
+ # unclaimed_storage_bytes and storage_percent_used are not real columns on
67
+ # zoekt_nodes; they are derived in Search::Zoekt::Node. We compute them in SQL
68
+ # here to avoid loading every row through ActiveRecord.
65
69
  ZOEKT_NODE_STORAGE_QUERY = <<~SQL.freeze
66
70
  SELECT
67
- id,
68
- metadata ->> 'name' AS node_name,
69
- unclaimed_storage_bytes,
70
- storage_percent_used
71
- FROM zoekt_nodes
71
+ zn.id,
72
+ zn.metadata ->> 'name' AS node_name,
73
+ (zn.usable_storage_bytes - COALESCE(zi.reserved_total, 0))::bigint
74
+ AS unclaimed_storage_bytes,
75
+ CASE
76
+ WHEN zn.total_bytes > 0
77
+ THEN zn.used_bytes::float8 / zn.total_bytes::float8
78
+ ELSE 0
79
+ END AS storage_percent_used
80
+ FROM zoekt_nodes zn
81
+ LEFT JOIN (
82
+ SELECT zoekt_node_id, SUM(reserved_storage_bytes) AS reserved_total
83
+ FROM zoekt_indices
84
+ GROUP BY zoekt_node_id
85
+ ) zi ON zi.zoekt_node_id = zn.id
72
86
  SQL
73
87
 
74
88
  ZOEKT_REPOSITORIES_STATE_QUERY = <<~SQL.freeze
@@ -95,6 +109,40 @@ module GitLab
95
109
  zoekt_nodes ON zoekt_indices.zoekt_node_id = zoekt_nodes.id
96
110
  SQL
97
111
 
112
+ ZOEKT_NODE_ENABLED_NAMESPACES_QUERY = <<~SQL.freeze
113
+ SELECT
114
+ zn.id AS node_id,
115
+ zn.metadata ->> 'name' AS node_name,
116
+ COUNT(DISTINCT zi.zoekt_enabled_namespace_id) AS count
117
+ FROM zoekt_nodes zn
118
+ LEFT JOIN zoekt_indices zi ON zi.zoekt_node_id = zn.id
119
+ GROUP BY zn.id, zn.metadata ->> 'name'
120
+ SQL
121
+
122
+ ZOEKT_NODE_TASKS_QUERY = <<~SQL.freeze
123
+ SELECT
124
+ zn.id AS node_id,
125
+ zn.metadata ->> 'name' AS node_name,
126
+ zt.state,
127
+ COUNT(*) AS count
128
+ FROM zoekt_nodes zn
129
+ LEFT JOIN zoekt_tasks zt ON zt.zoekt_node_id = zn.id
130
+ GROUP BY zn.id, zn.metadata ->> 'name', zt.state
131
+ SQL
132
+
133
+ ZOEKT_INDICES_STALE_USED_STORAGE_QUERY = <<~SQL.freeze
134
+ SELECT COUNT(*) AS count FROM zoekt_indices WHERE last_indexed_at >= used_storage_bytes_updated_at
135
+ SQL
136
+
137
+ ZOEKT_TASK_STATES = {
138
+ 0 => "pending",
139
+ 1 => "processing",
140
+ 10 => "done",
141
+ 250 => "skipped",
142
+ 255 => "failed",
143
+ 256 => "orphaned"
144
+ }.freeze
145
+
98
146
  def run
99
147
  return {} unless zoekt_indexing_enabled?
100
148
 
@@ -106,7 +154,10 @@ module GitLab
106
154
  repositories_state_query_result: execute(ZOEKT_REPOSITORIES_STATE_QUERY),
107
155
  indices_state_query_result: execute(ZOEKT_INDICES_STATE_QUERY),
108
156
  indices_watermark_query_result: execute(ZOEKT_INDICES_WATERMARK_QUERY),
109
- indices_storage_query_result: execute(ZOEKT_INDICES_STORAGE_QUERY)
157
+ indices_storage_query_result: execute(ZOEKT_INDICES_STORAGE_QUERY),
158
+ node_enabled_namespaces_query_result: execute(ZOEKT_NODE_ENABLED_NAMESPACES_QUERY),
159
+ node_tasks_query_result: execute(ZOEKT_NODE_TASKS_QUERY),
160
+ indices_stale_used_storage_query_result: execute(ZOEKT_INDICES_STALE_USED_STORAGE_QUERY)
110
161
  }.compact
111
162
  end
112
163
 
@@ -163,10 +214,12 @@ module GitLab
163
214
  with_connection_pool do |conn|
164
215
  conn.exec_params(query, params)
165
216
  end
166
- rescue PG::UndefinedTable, PG::UndefinedColumn
217
+ rescue PG::UndefinedTable, PG::UndefinedColumn => e
218
+ @logger&.warn("ZoektCollector query failed: #{e.class}: #{e.message.lines.first&.strip}")
167
219
  nil
168
220
  end
169
221
  end
222
+ # rubocop:enable Metrics/ClassLength
170
223
 
171
224
  # The prober which is called when gathering metrics
172
225
  # rubocop:disable Metrics/ClassLength
@@ -231,6 +284,24 @@ module GitLab
231
284
  "gauge"
232
285
  )
233
286
 
287
+ PrometheusMetrics.describe(
288
+ "search_zoekt_node_enabled_namespaces",
289
+ "Number of enabled namespaces per Zoekt node",
290
+ "gauge"
291
+ )
292
+
293
+ PrometheusMetrics.describe(
294
+ "search_zoekt_node_tasks",
295
+ "Number of Zoekt indexing tasks on a node, broken down by state",
296
+ "gauge"
297
+ )
298
+
299
+ PrometheusMetrics.describe(
300
+ "search_zoekt_indices_with_stale_used_storage_bytes",
301
+ "Number of Zoekt indices whose used_storage_bytes value has not been updated since the last index run",
302
+ "gauge"
303
+ )
304
+
234
305
  def initialize(metrics: PrometheusMetrics.new, **opts)
235
306
  @metrics = metrics
236
307
  @collector = opts[:collector] || ZoektCollector.new(**opts)
@@ -255,6 +326,9 @@ module GitLab
255
326
  process_indices_state_results(results[:indices_state_query_result])
256
327
  process_indices_watermark_results(results[:indices_watermark_query_result])
257
328
  process_indices_storage_results(results[:indices_storage_query_result])
329
+ process_node_enabled_namespaces_results(results[:node_enabled_namespaces_query_result])
330
+ process_node_tasks_results(results[:node_tasks_query_result])
331
+ process_indices_stale_used_storage_results(results[:indices_stale_used_storage_query_result])
258
332
  end
259
333
 
260
334
  def process_task_results(results)
@@ -349,23 +423,11 @@ module GitLab
349
423
  end
350
424
 
351
425
  def add_zoekt_repositories_state_to_metric(row)
352
- @metrics.add(
353
- "search_zoekt_repositories_states_total",
354
- row["count"].to_i,
355
- **{
356
- state: row["state"]
357
- }.compact
358
- )
426
+ @metrics.add("search_zoekt_repositories_states_total", row["count"].to_i, **{ state: row["state"] }.compact)
359
427
  end
360
428
 
361
429
  def add_zoekt_indices_state_to_metric(row)
362
- @metrics.add(
363
- "search_zoekt_indices_states",
364
- row["count"].to_i,
365
- **{
366
- state: row["state"]
367
- }.compact
368
- )
430
+ @metrics.add("search_zoekt_indices_states", row["count"].to_i, **{ state: row["state"] }.compact)
369
431
  end
370
432
 
371
433
  def add_zoekt_indices_watermark_to_metric(row)
@@ -378,26 +440,56 @@ module GitLab
378
440
  )
379
441
  end
380
442
 
381
- # rubocop:disable Metrics/MethodLength
382
- def add_zoekt_indices_storage_to_metric(row)
443
+ def process_node_enabled_namespaces_results(results)
444
+ results&.each { |row| add_zoekt_node_enabled_namespaces_to_metric(row) }
445
+ end
446
+
447
+ def process_node_tasks_results(results)
448
+ results&.each { |row| add_zoekt_node_tasks_to_metric(row) }
449
+ end
450
+
451
+ def process_indices_stale_used_storage_results(results)
452
+ results&.each { |row| add_zoekt_indices_stale_used_storage_to_metric(row) }
453
+ end
454
+
455
+ def add_zoekt_node_enabled_namespaces_to_metric(row)
383
456
  @metrics.add(
384
- "search_zoekt_indices_reserved_storage_bytes",
385
- row["reserved_storage_bytes"].to_i,
457
+ "search_zoekt_node_enabled_namespaces",
458
+ row["count"].to_i,
386
459
  **{
387
- zoekt_index_id: row["id"],
388
- zoekt_node_name: row["node_name"]
460
+ node_id: row["node_id"],
461
+ node_name: row["node_name"]
389
462
  }.compact
390
463
  )
464
+ end
465
+
466
+ def add_zoekt_node_tasks_to_metric(row)
467
+ state_int = row["state"]&.to_i
468
+ return if row["state"].nil?
469
+
470
+ state_label = ZoektCollector::ZOEKT_TASK_STATES[state_int]
471
+ return if state_label.nil?
472
+
391
473
  @metrics.add(
392
- "search_zoekt_indices_used_storage_bytes",
393
- row["used_storage_bytes"].to_i,
474
+ "search_zoekt_node_tasks",
475
+ row["count"].to_i,
394
476
  **{
395
- zoekt_index_id: row["id"],
396
- zoekt_node_name: row["node_name"]
477
+ node_id: row["node_id"],
478
+ node_name: row["node_name"],
479
+ state: state_label
397
480
  }.compact
398
481
  )
399
482
  end
400
- # rubocop:enable Metrics/MethodLength
483
+
484
+ def add_zoekt_indices_stale_used_storage_to_metric(row)
485
+ @metrics.add("search_zoekt_indices_with_stale_used_storage_bytes", row["count"].to_i)
486
+ end
487
+
488
+ def add_zoekt_indices_storage_to_metric(row)
489
+ payload = { zoekt_index_id: row["id"], zoekt_node_name: row["node_name"] }.compact
490
+ @metrics.add("search_zoekt_indices_reserved_storage_bytes", row["reserved_storage_bytes"].to_i, **payload)
491
+ @metrics.add("search_zoekt_indices_used_storage_bytes", row["used_storage_bytes"].to_i, **payload)
492
+ end
401
493
 
402
494
  def write_to(target)
403
495
  target.write(@metrics.to_s)
@@ -1,5 +1,5 @@
1
1
  module GitLab
2
2
  module Exporter
3
- VERSION = "16.7.0".freeze
3
+ VERSION = "16.9.0".freeze
4
4
  end
5
5
  end
@@ -36,6 +36,23 @@ describe GitLab::Exporter::Database::ZoektCollector do
36
36
  "node_name" => "zoekt-2" }
37
37
  ]
38
38
  end
39
+ let(:zoekt_node_enabled_namespaces_query) { described_class::ZOEKT_NODE_ENABLED_NAMESPACES_QUERY }
40
+ let(:zoekt_node_enabled_namespaces_query_results) do
41
+ [
42
+ { "node_id" => "1", "node_name" => "zoekt-1", "count" => "3" },
43
+ { "node_id" => "2", "node_name" => "zoekt-2", "count" => "5" }
44
+ ]
45
+ end
46
+ let(:zoekt_node_tasks_query) { described_class::ZOEKT_NODE_TASKS_QUERY }
47
+ let(:zoekt_node_tasks_query_results) do
48
+ [
49
+ { "node_id" => "1", "node_name" => "zoekt-1", "state" => "0", "count" => "4" },
50
+ { "node_id" => "1", "node_name" => "zoekt-1", "state" => "1", "count" => "2" },
51
+ { "node_id" => "2", "node_name" => "zoekt-2", "state" => "10", "count" => "7" }
52
+ ]
53
+ end
54
+ let(:zoekt_indices_stale_used_storage_query) { described_class::ZOEKT_INDICES_STALE_USED_STORAGE_QUERY }
55
+ let(:zoekt_indices_stale_used_storage_query_results) { [{ "count" => "6" }] }
39
56
 
40
57
  let(:zoekt_repository_schema_version_query_results) { [{ "count" => "1" }] }
41
58
 
@@ -80,7 +97,10 @@ describe GitLab::Exporter::Database::ZoektCollector do
80
97
  repositories_state_query_result: zoekt_repositories_state_query_results,
81
98
  indices_state_query_result: zoekt_indices_state_query_results,
82
99
  indices_watermark_query_result: zoekt_indices_watermark_query_results,
83
- indices_storage_query_result: zoekt_indices_storage_query_results
100
+ indices_storage_query_result: zoekt_indices_storage_query_results,
101
+ node_enabled_namespaces_query_result: zoekt_node_enabled_namespaces_query_results,
102
+ node_tasks_query_result: zoekt_node_tasks_query_results,
103
+ indices_stale_used_storage_query_result: zoekt_indices_stale_used_storage_query_results
84
104
  }
85
105
  end
86
106
 
@@ -122,6 +142,12 @@ describe GitLab::Exporter::Database::ZoektCollector do
122
142
  .and_return(zoekt_indices_watermark_query_results)
123
143
  expect(connection).to receive(:exec_params).with(zoekt_indices_storage_query, [])
124
144
  .and_return(zoekt_indices_storage_query_results)
145
+ expect(connection).to receive(:exec_params).with(zoekt_node_enabled_namespaces_query, [])
146
+ .and_return(zoekt_node_enabled_namespaces_query_results)
147
+ expect(connection).to receive(:exec_params).with(zoekt_node_tasks_query, [])
148
+ .and_return(zoekt_node_tasks_query_results)
149
+ expect(connection).to receive(:exec_params).with(zoekt_indices_stale_used_storage_query, [])
150
+ .and_return(zoekt_indices_stale_used_storage_query_results)
125
151
 
126
152
  expect(collector.run).to eq(result)
127
153
  end
@@ -144,6 +170,12 @@ describe GitLab::Exporter::Database::ZoektCollector do
144
170
  .and_raise(PG::UndefinedTable)
145
171
  allow(connection).to receive(:exec_params).with(zoekt_indices_storage_query, [])
146
172
  .and_raise(PG::UndefinedTable)
173
+ allow(connection).to receive(:exec_params).with(zoekt_node_enabled_namespaces_query, [])
174
+ .and_raise(PG::UndefinedTable)
175
+ allow(connection).to receive(:exec_params).with(zoekt_node_tasks_query, [])
176
+ .and_raise(PG::UndefinedTable)
177
+ allow(connection).to receive(:exec_params).with(zoekt_indices_stale_used_storage_query, [])
178
+ .and_raise(PG::UndefinedTable)
147
179
 
148
180
  expect(collector.run).to eq({})
149
181
  end
@@ -167,6 +199,12 @@ describe GitLab::Exporter::Database::ZoektCollector do
167
199
  .and_raise(PG::UndefinedColumn)
168
200
  expect(connection).to receive(:exec_params).with(zoekt_indices_storage_query, [])
169
201
  .and_raise(PG::UndefinedColumn)
202
+ expect(connection).to receive(:exec_params).with(zoekt_node_enabled_namespaces_query, [])
203
+ .and_raise(PG::UndefinedColumn)
204
+ expect(connection).to receive(:exec_params).with(zoekt_node_tasks_query, [])
205
+ .and_raise(PG::UndefinedColumn)
206
+ expect(connection).to receive(:exec_params).with(zoekt_indices_stale_used_storage_query, [])
207
+ .and_raise(PG::UndefinedColumn)
170
208
 
171
209
  expect(collector.run).to eq({ task_processing_query_result: zoekt_tasks_processing_query_results })
172
210
  end
@@ -174,6 +212,33 @@ describe GitLab::Exporter::Database::ZoektCollector do
174
212
  end
175
213
  end
176
214
 
215
+ describe "ZOEKT_NODE_STORAGE_QUERY" do
216
+ subject(:query) { described_class::ZOEKT_NODE_STORAGE_QUERY }
217
+
218
+ it "selects the unclaimed_storage_bytes and storage_percent_used aliases" do
219
+ expect(query).to match(/AS unclaimed_storage_bytes/)
220
+ expect(query).to match(/AS storage_percent_used/)
221
+ end
222
+
223
+ it "computes unclaimed_storage_bytes from real columns rather than referencing a non-existent column" do
224
+ # zoekt_nodes does not have an unclaimed_storage_bytes column; it must be derived
225
+ # from usable_storage_bytes minus the SUM of zoekt_indices.reserved_storage_bytes.
226
+ expect(query).to include("usable_storage_bytes")
227
+ expect(query).to include("reserved_storage_bytes")
228
+ expect(query).to include("zoekt_indices")
229
+ expect(query).not_to match(/^\s*unclaimed_storage_bytes\b/)
230
+ end
231
+
232
+ it "computes storage_percent_used from used_bytes / total_bytes with a guard" do
233
+ # zoekt_nodes does not have a storage_percent_used column; it must be derived
234
+ # from used_bytes / total_bytes (with a divide-by-zero guard).
235
+ expect(query).to include("used_bytes")
236
+ expect(query).to include("total_bytes")
237
+ expect(query).to match(/total_bytes\s*>\s*0/)
238
+ expect(query).not_to match(/^\s*storage_percent_used\b/)
239
+ end
240
+ end
241
+
177
242
  describe "#zoekt_indexing_enabled?" do
178
243
  it "returns true when zoekt is enabled" do
179
244
  expect(connection).to receive(:exec)
@@ -230,6 +295,19 @@ describe GitLab::Exporter::Database::ZoektProber do
230
295
  "node_name" => "zoekt-1" },
231
296
  { "id" => "2", "reserved_storage_bytes" => "2000000", "used_storage_bytes" => "1500000",
232
297
  "node_name" => "zoekt-2" }
298
+ ],
299
+ node_enabled_namespaces_query_result: [
300
+ { "node_id" => "1", "node_name" => "zoekt-1", "count" => "3" },
301
+ { "node_id" => "2", "node_name" => "zoekt-2", "count" => "5" }
302
+ ],
303
+ node_tasks_query_result: [
304
+ { "node_id" => "1", "node_name" => "zoekt-1", "state" => "0", "count" => "4" },
305
+ { "node_id" => "1", "node_name" => "zoekt-1", "state" => "1", "count" => "2" },
306
+ { "node_id" => "2", "node_name" => "zoekt-2", "state" => "10", "count" => "7" },
307
+ { "node_id" => "2", "node_name" => "zoekt-2", "state" => nil, "count" => "1" }
308
+ ],
309
+ indices_stale_used_storage_query_result: [
310
+ { "count" => "6" }
233
311
  ]
234
312
  }
235
313
  end
@@ -334,7 +412,73 @@ describe GitLab::Exporter::Database::ZoektProber do
334
412
  )
335
413
  end
336
414
 
415
+ data[:node_enabled_namespaces_query_result].each do |ns_data|
416
+ expect(metrics).to receive(:add)
417
+ .with(
418
+ "search_zoekt_node_enabled_namespaces",
419
+ ns_data["count"].to_i,
420
+ node_id: ns_data["node_id"],
421
+ node_name: ns_data["node_name"]
422
+ )
423
+ end
424
+
425
+ # Only rows with a known state integer are emitted; the nil-state row is skipped.
426
+ task_state_map = GitLab::Exporter::Database::ZoektCollector::ZOEKT_TASK_STATES
427
+ data[:node_tasks_query_result].each do |task_data|
428
+ next if task_data["state"].nil?
429
+
430
+ state_label = task_state_map[task_data["state"].to_i]
431
+ next if state_label.nil?
432
+
433
+ expect(metrics).to receive(:add)
434
+ .with(
435
+ "search_zoekt_node_tasks",
436
+ task_data["count"].to_i,
437
+ node_id: task_data["node_id"],
438
+ node_name: task_data["node_name"],
439
+ state: state_label
440
+ )
441
+ end
442
+
443
+ data[:indices_stale_used_storage_query_result].each do |stale_data|
444
+ expect(metrics).to receive(:add)
445
+ .with(
446
+ "search_zoekt_indices_with_stale_used_storage_bytes",
447
+ stale_data["count"].to_i
448
+ )
449
+ end
450
+
337
451
  probe_db
338
452
  end
453
+
454
+ context "when node_tasks_query_result contains nil state rows" do
455
+ it "skips rows where state is nil" do
456
+ nil_state_data = data.merge(
457
+ node_tasks_query_result: [{ "node_id" => "1", "node_name" => "zoekt-1", "state" => nil, "count" => "3" }],
458
+ node_enabled_namespaces_query_result: [],
459
+ indices_stale_used_storage_query_result: []
460
+ )
461
+ allow(collector).to receive(:run).and_return(nil_state_data)
462
+
463
+ expect(metrics).not_to receive(:add).with("search_zoekt_node_tasks", anything, anything)
464
+
465
+ described_class.new(metrics: metrics, collector: collector, **opts).probe_db
466
+ end
467
+ end
468
+
469
+ context "when node_tasks_query_result contains an unknown state integer" do
470
+ it "skips rows where state integer is not in ZOEKT_TASK_STATES" do
471
+ unknown_state_data = data.merge(
472
+ node_tasks_query_result: [{ "node_id" => "1", "node_name" => "zoekt-1", "state" => "99", "count" => "2" }],
473
+ node_enabled_namespaces_query_result: [],
474
+ indices_stale_used_storage_query_result: []
475
+ )
476
+ allow(collector).to receive(:run).and_return(unknown_state_data)
477
+
478
+ expect(metrics).not_to receive(:add).with("search_zoekt_node_tasks", anything, anything)
479
+
480
+ described_class.new(metrics: metrics, collector: collector, **opts).probe_db
481
+ end
482
+ end
339
483
  end
340
484
  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: 16.7.0
4
+ version: 16.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pablo Carranza
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 2.14.1
47
+ version: 2.14.3
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 2.14.1
54
+ version: 2.14.3
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pg
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 7.2.0
75
+ version: 8.0.1
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 7.2.0
82
+ version: 8.0.1
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: quantile
85
85
  requirement: !ruby/object:Gem::Requirement