gitlab-exporter 16.8.0 → 16.10.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: 8b320ac3c29627e5bca0700ab6007fb3681a286679032bccae4595af29e764fc
4
- data.tar.gz: ab52c7b430a87959228fdb4483c2bf3a26f9838c983a5292dd27bb6b72123a8d
3
+ metadata.gz: 3a3b7fa869eab272c352626dfd0f8acc2cbcc3fb2df46da92ea0387471282d2b
4
+ data.tar.gz: fc2f8a0a213b2a97a92ed5f417a5856f181c9f422cbbc4640c9cce45d86e4fcf
5
5
  SHA512:
6
- metadata.gz: dbd696eb2e666142c275e46aff04eb9beda6739863fe505249f0074d1d8d1145fe6b41573e0f3521feee2bcde7dd2ad33e8fc76b1abc3f75e34e23d95d94258f
7
- data.tar.gz: 2d426ce702f2d360b5aa8cc5ae51f4924295e717615c6da3adbd9ef1e46c241ed40e151082918fa7c963c7367c857730e1fafb41ffc328bc6b3d14879615abcf
6
+ metadata.gz: 2f6603c161ae6c9346d5b3ecd919f4df54bceb8232e588db181d1142eedc2e90231039b15a357ceed1ae62ae735b7540567e2717bbacadcea5e83ddd5b366fb2
7
+ data.tar.gz: 760580b43d2290a933842a4ffbd4182fe60660b401d03e9df6a7c8d85f6c3363054124b9aba2fd5a4983fb1989a11f2df28cf2f3cbdf7ca67e653bde4be95bf7
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-exporter (16.8.0)
4
+ gitlab-exporter (16.10.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 (= 8.0.1)
9
+ puma (= 8.0.2)
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 (8.0.1)
43
+ puma (8.0.2)
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", "8.0.1"
29
+ s.add_runtime_dependency "puma", "8.0.2"
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,101 +1,147 @@
1
1
  -- Originally from: https://github.com/ioguix/pgsql-bloat-estimation/blob/master/btree/btree_bloat.sql
2
- -- WARNING: executed with a non-superuser role, the query inspect only index on tables you are granted to read.
2
+ --
3
+ -- Rewritten to use CROSS JOIN LATERAL stages so this query stays in sync with the
4
+ -- `postgres_index_bloat_estimate(p_schema, p_idxname)` function used by the main GitLab
5
+ -- application's reindexing pipeline:
6
+ -- https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/migrate/20260526120000_add_postgres_index_bloat_estimate_function.rb
7
+ --
8
+ -- Expanding the index columns inside a LATERAL join (instead of putting a set-returning
9
+ -- function such as generate_series in the SELECT list) avoids the ProjectSet barrier that
10
+ -- prevents the planner from pushing predicates down. See:
11
+ -- https://gitlab.com/gitlab-org/gitlab/-/work_items/569363
12
+ -- https://gitlab.com/gitlab-org/gitlab/-/work_items/599210
13
+ --
14
+ -- WARNING: executed with a non-superuser role, the query inspects only indexes on tables you are granted to read.
3
15
  -- 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
5
- SELECT current_database(), nspname AS schemaname, tblname, idxname AS object_name, bs*(relpages)::bigint AS real_size,
6
- bs*(relpages-est_pages)::bigint AS extra_size,
7
- 100 * (relpages-est_pages)::float / relpages AS extra_ratio,
8
- fillfactor,
9
- CASE WHEN relpages > est_pages_ff
10
- THEN bs*(relpages-est_pages_ff)
16
+ -- This query is compatible with PostgreSQL 9.3 and after (LATERAL joins).
17
+ --
18
+ -- Note: unlike the previous ioguix-style query, this form does NOT need to materialize
19
+ -- pg_stats to stay fast on PostgreSQL 17. The old query joined pg_stats once as a single
20
+ -- large join that the PG17 planner badly underestimated (see f18ac4b); here pg_stats is
21
+ -- looked up per index inside the LATERAL, resolving through the pg_statistic indexes, so
22
+ -- there is no single join for the planner to misestimate.
23
+ SELECT
24
+ current_database(),
25
+ n.nspname AS schemaname,
26
+ ct.relname AS tblname,
27
+ ci.relname AS object_name,
28
+ c.bs * ci.relpages::bigint AS real_size,
29
+ c.bs * (ci.relpages - bloat.est_pages)::bigint AS extra_size,
30
+ 100 * (ci.relpages - bloat.est_pages)::float / ci.relpages AS extra_ratio,
31
+ c.fillfactor,
32
+ CASE WHEN ci.relpages > bloat.est_pages_ff
33
+ THEN c.bs * (ci.relpages - bloat.est_pages_ff)
11
34
  ELSE 0
12
35
  END AS bloat_size,
13
- 100 * (relpages-est_pages_ff)::float / relpages AS bloat_ratio,
14
- is_na
15
- -- , 100-(pst).avg_leaf_density AS pst_avg_bloat, est_pages, index_tuple_hdr_bm, maxalign, pagehdr, nulldatawidth, nulldatahdrwidth, reltuples, relpages -- (DEBUG INFO)
16
- FROM (
17
- SELECT coalesce(1 +
18
- ceil(reltuples/floor((bs-pageopqdata-pagehdr)/(4+nulldatahdrwidth)::float)), 0 -- ItemIdData size + computed avg size of a tuple (nulldatahdrwidth)
19
- ) AS est_pages,
20
- coalesce(1 +
21
- ceil(reltuples/floor((bs-pageopqdata-pagehdr)*fillfactor/(100*(4+nulldatahdrwidth)::float))), 0
22
- ) AS est_pages_ff,
23
- bs, nspname, tblname, idxname, relpages, fillfactor, is_na
24
- -- , pgstatindex(idxoid) AS pst, index_tuple_hdr_bm, maxalign, pagehdr, nulldatawidth, nulldatahdrwidth, reltuples -- (DEBUG INFO)
25
- FROM (
26
- SELECT maxalign, bs, nspname, tblname, idxname, reltuples, relpages, idxoid, fillfactor,
27
- ( index_tuple_hdr_bm +
28
- maxalign - CASE -- Add padding to the index tuple header to align on MAXALIGN
29
- WHEN index_tuple_hdr_bm%maxalign = 0 THEN maxalign
30
- ELSE index_tuple_hdr_bm%maxalign
31
- END
32
- + nulldatawidth + maxalign - CASE -- Add padding to the data to align on MAXALIGN
33
- WHEN nulldatawidth = 0 THEN 0
34
- WHEN nulldatawidth::integer%maxalign = 0 THEN maxalign
35
- ELSE nulldatawidth::integer%maxalign
36
- END
37
- )::numeric AS nulldatahdrwidth, pagehdr, pageopqdata, is_na
38
- -- , index_tuple_hdr_bm, nulldatawidth -- (DEBUG INFO)
39
- FROM (
40
- SELECT n.nspname, i.tblname, i.idxname, i.reltuples, i.relpages,
41
- i.idxoid, i.fillfactor, current_setting('block_size')::numeric AS bs,
42
- CASE -- MAXALIGN: 4 on 32bits, 8 on 64bits (and mingw32 ?)
43
- WHEN version() ~ 'mingw32' OR version() ~ '64-bit|x86_64|ppc64|ia64|amd64' THEN 8
44
- ELSE 4
45
- END AS maxalign,
46
- /* per page header, fixed size: 20 for 7.X, 24 for others */
47
- 24 AS pagehdr,
48
- /* per page btree opaque data */
49
- 16 AS pageopqdata,
50
- /* per tuple header: add IndexAttributeBitMapData if some cols are null-able */
51
- CASE WHEN max(coalesce(s.null_frac,0)) = 0
52
- THEN 2 -- IndexTupleData size
53
- ELSE 2 + (( 32 + 8 - 1 ) / 8) -- IndexTupleData size + IndexAttributeBitMapData size ( max num filed per index + 8 - 1 /8)
54
- END AS index_tuple_hdr_bm,
55
- /* data len: we remove null values save space using it fractionnal part from stats */
56
- sum( (1-coalesce(s.null_frac, 0)) * coalesce(s.avg_width, 1024)) AS nulldatawidth,
57
- max( CASE WHEN i.atttypid = 'pg_catalog.name'::regtype THEN 1 ELSE 0 END ) > 0 AS is_na
58
- FROM (
59
- SELECT ct.relname AS tblname, ct.relnamespace, ic.idxname, ic.attpos, ic.indkey, ic.indkey[ic.attpos], ic.reltuples, ic.relpages, ic.tbloid, ic.idxoid, ic.fillfactor,
60
- coalesce(a1.attnum, a2.attnum) AS attnum, coalesce(a1.attname, a2.attname) AS attname, coalesce(a1.atttypid, a2.atttypid) AS atttypid,
61
- CASE WHEN a1.attnum IS NULL
62
- THEN ic.idxname
63
- ELSE ct.relname
64
- END AS attrelname
65
- FROM (
66
- SELECT idxname, reltuples, relpages, tbloid, idxoid, fillfactor, indkey,
67
- pg_catalog.generate_series(1,indnatts) AS attpos
68
- FROM (
69
- SELECT ci.relname AS idxname, ci.reltuples, ci.relpages, i.indrelid AS tbloid,
70
- i.indexrelid AS idxoid,
71
- coalesce(substring(
72
- array_to_string(ci.reloptions, ' ')
73
- from 'fillfactor=([0-9]+)')::smallint, 90) AS fillfactor,
74
- i.indnatts,
75
- pg_catalog.string_to_array(pg_catalog.textin(
76
- pg_catalog.int2vectorout(i.indkey)),' ')::int[] AS indkey
77
- FROM pg_catalog.pg_index i
78
- JOIN pg_catalog.pg_class ci ON ci.oid = i.indexrelid
79
- WHERE ci.relam=(SELECT oid FROM pg_am WHERE amname = 'btree')
80
- AND ci.relpages > 0
81
- ) AS idx_data
82
- ) AS ic
83
- JOIN pg_catalog.pg_class ct ON ct.oid = ic.tbloid
84
- LEFT JOIN pg_catalog.pg_attribute a1 ON
85
- ic.indkey[ic.attpos] <> 0
86
- AND a1.attrelid = ic.tbloid
87
- AND a1.attnum = ic.indkey[ic.attpos]
88
- LEFT JOIN pg_catalog.pg_attribute a2 ON
89
- ic.indkey[ic.attpos] = 0
90
- AND a2.attrelid = ic.idxoid
91
- AND a2.attnum = ic.attpos
92
- ) i
93
- 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
97
- GROUP BY 1,2,3,4,5,6,7,8,9,10,11
98
- ) AS rows_data_stats
99
- ) AS rows_hdr_pdg_stats
100
- ) AS relation_stats
101
- ORDER BY nspname, tblname, idxname;
36
+ 100 * (ci.relpages - bloat.est_pages_ff)::float / ci.relpages AS bloat_ratio,
37
+ bloat.is_na
38
+ FROM pg_catalog.pg_class ci
39
+ JOIN pg_catalog.pg_index i ON i.indexrelid = ci.oid
40
+ JOIN pg_catalog.pg_class ct ON ct.oid = i.indrelid
41
+ JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace
42
+
43
+ -- Per-index system constants and fillfactor.
44
+ CROSS JOIN LATERAL (
45
+ SELECT
46
+ current_setting('block_size')::numeric AS bs,
47
+ CASE -- MAXALIGN: 4 on 32bits, 8 on 64bits (and mingw32 ?)
48
+ WHEN version() ~ 'mingw32'::text
49
+ OR version() ~ '64-bit|x86_64|ppc64|ia64|amd64'::text THEN 8
50
+ ELSE 4
51
+ END AS maxalign,
52
+ 24 AS pagehdr, -- per page header, fixed size: 24 for 8.3 and after
53
+ 16 AS pageopqdata, -- per page btree opaque data
54
+ COALESCE(
55
+ substring(array_to_string(ci.reloptions, ' '::text) FROM 'fillfactor=([0-9]+)')::smallint::integer,
56
+ 90
57
+ ) AS fillfactor
58
+ ) c
59
+
60
+ -- Aggregate the per-column statistics for this index, expanding the index columns
61
+ -- through a LATERAL generate_series rather than a SELECT-list set-returning function.
62
+ CROSS JOIN LATERAL (
63
+ SELECT
64
+ max(CASE WHEN COALESCE(a1.atttypid, a2.atttypid) = 'pg_catalog.name'::regtype THEN 1 ELSE 0 END) > 0 AS is_na,
65
+ /* per tuple header: add IndexAttributeBitMapData if some cols are null-able */
66
+ CASE WHEN max(COALESCE(s_t.null_frac, s_x.null_frac, 0::real)) = 0::double precision
67
+ THEN 2 -- IndexTupleData size
68
+ ELSE 2 + (32 + 8 - 1) / 8 -- IndexTupleData size + IndexAttributeBitMapData size
69
+ END AS index_tuple_hdr_bm,
70
+ /* data len: we remove null values, saving space, using their fractional part from stats */
71
+ sum((1::double precision - COALESCE(s_t.null_frac, s_x.null_frac, 0::real))
72
+ * COALESCE(s_t.avg_width, s_x.avg_width, 1024)::double precision) AS nulldatawidth
73
+ FROM generate_series(1, i.indnatts::integer) AS gs(attpos)
74
+ -- Decode the indkey int2vector into an int[] once per index (not once per column).
75
+ CROSS JOIN LATERAL (
76
+ SELECT string_to_array(textin(int2vectorout(i.indkey)), ' '::text)::integer[] AS indkey_arr
77
+ ) ik
78
+ LEFT JOIN pg_catalog.pg_attribute a1
79
+ ON ik.indkey_arr[gs.attpos] <> 0
80
+ AND a1.attrelid = i.indrelid
81
+ AND a1.attnum = ik.indkey_arr[gs.attpos]
82
+ LEFT JOIN pg_catalog.pg_attribute a2
83
+ ON ik.indkey_arr[gs.attpos] = 0
84
+ AND a2.attrelid = ci.oid
85
+ AND a2.attnum = gs.attpos
86
+ LEFT JOIN pg_catalog.pg_stats s_t
87
+ ON s_t.schemaname = n.nspname
88
+ AND s_t.tablename = ct.relname
89
+ AND s_t.attname = a1.attname
90
+ LEFT JOIN pg_catalog.pg_stats s_x
91
+ ON s_x.schemaname = n.nspname
92
+ AND s_x.tablename = ci.relname
93
+ AND s_x.attname = a2.attname
94
+ ) agg
95
+
96
+ -- Index tuple header width, aligned on MAXALIGN.
97
+ CROSS JOIN LATERAL (
98
+ SELECT
99
+ (
100
+ (agg.index_tuple_hdr_bm + c.maxalign
101
+ - CASE -- Add padding to the index tuple header to align on MAXALIGN
102
+ WHEN (agg.index_tuple_hdr_bm % c.maxalign) = 0 THEN c.maxalign
103
+ ELSE agg.index_tuple_hdr_bm % c.maxalign
104
+ END)::double precision
105
+ + agg.nulldatawidth
106
+ + c.maxalign::double precision
107
+ - CASE -- Add padding to the data to align on MAXALIGN
108
+ WHEN agg.nulldatawidth = 0::double precision THEN 0
109
+ WHEN (agg.nulldatawidth::integer % c.maxalign) = 0 THEN c.maxalign
110
+ ELSE agg.nulldatawidth::integer % c.maxalign
111
+ END::double precision
112
+ )::numeric AS nulldatahdrwidth
113
+ ) hdr
114
+
115
+ -- Estimated number of pages, with and without the fillfactor applied.
116
+ CROSS JOIN LATERAL (
117
+ SELECT
118
+ agg.is_na,
119
+ -- NULLIF guards against division by zero: floor(...) reaches 0 once
120
+ -- nulldatahdrwidth exceeds the usable page size (an index whose columns are
121
+ -- entirely unanalyzed and fall back to the 1024-byte default width). Without
122
+ -- it, PostgreSQL raises a hard error that would fail the whole probe; instead
123
+ -- the NULL flows through the outer COALESCE to est_pages = 0.
124
+ COALESCE(
125
+ 1::double precision + ceil(
126
+ ci.reltuples / NULLIF(floor(
127
+ (c.bs - c.pageopqdata::numeric - c.pagehdr::numeric)::double precision
128
+ / (4::numeric + hdr.nulldatahdrwidth)::double precision
129
+ ), 0)
130
+ ),
131
+ 0::double precision
132
+ ) AS est_pages,
133
+ COALESCE(
134
+ 1::double precision + ceil(
135
+ ci.reltuples / NULLIF(floor(
136
+ ((c.bs - c.pageopqdata::numeric - c.pagehdr::numeric) * c.fillfactor::numeric)::double precision
137
+ / (100::double precision * (4::numeric + hdr.nulldatahdrwidth)::double precision)
138
+ ), 0)
139
+ ),
140
+ 0::double precision
141
+ ) AS est_pages_ff
142
+ ) bloat
143
+
144
+ WHERE ci.relkind = 'i'
145
+ AND ci.relam = (SELECT oid FROM pg_am WHERE amname = 'btree'::name)
146
+ AND ci.relpages > 0
147
+ ORDER BY n.nspname, ct.relname, ci.relname;
@@ -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
@@ -108,6 +109,40 @@ module GitLab
108
109
  zoekt_nodes ON zoekt_indices.zoekt_node_id = zoekt_nodes.id
109
110
  SQL
110
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
+
111
146
  def run
112
147
  return {} unless zoekt_indexing_enabled?
113
148
 
@@ -119,7 +154,10 @@ module GitLab
119
154
  repositories_state_query_result: execute(ZOEKT_REPOSITORIES_STATE_QUERY),
120
155
  indices_state_query_result: execute(ZOEKT_INDICES_STATE_QUERY),
121
156
  indices_watermark_query_result: execute(ZOEKT_INDICES_WATERMARK_QUERY),
122
- 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)
123
161
  }.compact
124
162
  end
125
163
 
@@ -181,6 +219,7 @@ module GitLab
181
219
  nil
182
220
  end
183
221
  end
222
+ # rubocop:enable Metrics/ClassLength
184
223
 
185
224
  # The prober which is called when gathering metrics
186
225
  # rubocop:disable Metrics/ClassLength
@@ -245,6 +284,24 @@ module GitLab
245
284
  "gauge"
246
285
  )
247
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
+
248
305
  def initialize(metrics: PrometheusMetrics.new, **opts)
249
306
  @metrics = metrics
250
307
  @collector = opts[:collector] || ZoektCollector.new(**opts)
@@ -269,6 +326,9 @@ module GitLab
269
326
  process_indices_state_results(results[:indices_state_query_result])
270
327
  process_indices_watermark_results(results[:indices_watermark_query_result])
271
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])
272
332
  end
273
333
 
274
334
  def process_task_results(results)
@@ -363,23 +423,11 @@ module GitLab
363
423
  end
364
424
 
365
425
  def add_zoekt_repositories_state_to_metric(row)
366
- @metrics.add(
367
- "search_zoekt_repositories_states_total",
368
- row["count"].to_i,
369
- **{
370
- state: row["state"]
371
- }.compact
372
- )
426
+ @metrics.add("search_zoekt_repositories_states_total", row["count"].to_i, **{ state: row["state"] }.compact)
373
427
  end
374
428
 
375
429
  def add_zoekt_indices_state_to_metric(row)
376
- @metrics.add(
377
- "search_zoekt_indices_states",
378
- row["count"].to_i,
379
- **{
380
- state: row["state"]
381
- }.compact
382
- )
430
+ @metrics.add("search_zoekt_indices_states", row["count"].to_i, **{ state: row["state"] }.compact)
383
431
  end
384
432
 
385
433
  def add_zoekt_indices_watermark_to_metric(row)
@@ -392,26 +440,56 @@ module GitLab
392
440
  )
393
441
  end
394
442
 
395
- # rubocop:disable Metrics/MethodLength
396
- 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)
397
456
  @metrics.add(
398
- "search_zoekt_indices_reserved_storage_bytes",
399
- row["reserved_storage_bytes"].to_i,
457
+ "search_zoekt_node_enabled_namespaces",
458
+ row["count"].to_i,
400
459
  **{
401
- zoekt_index_id: row["id"],
402
- zoekt_node_name: row["node_name"]
460
+ node_id: row["node_id"],
461
+ node_name: row["node_name"]
403
462
  }.compact
404
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
+
405
473
  @metrics.add(
406
- "search_zoekt_indices_used_storage_bytes",
407
- row["used_storage_bytes"].to_i,
474
+ "search_zoekt_node_tasks",
475
+ row["count"].to_i,
408
476
  **{
409
- zoekt_index_id: row["id"],
410
- zoekt_node_name: row["node_name"]
477
+ node_id: row["node_id"],
478
+ node_name: row["node_name"],
479
+ state: state_label
411
480
  }.compact
412
481
  )
413
482
  end
414
- # 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
415
493
 
416
494
  def write_to(target)
417
495
  target.write(@metrics.to_s)
@@ -1,5 +1,5 @@
1
1
  module GitLab
2
2
  module Exporter
3
- VERSION = "16.8.0".freeze
3
+ VERSION = "16.10.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
@@ -257,6 +295,19 @@ describe GitLab::Exporter::Database::ZoektProber do
257
295
  "node_name" => "zoekt-1" },
258
296
  { "id" => "2", "reserved_storage_bytes" => "2000000", "used_storage_bytes" => "1500000",
259
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" }
260
311
  ]
261
312
  }
262
313
  end
@@ -361,7 +412,73 @@ describe GitLab::Exporter::Database::ZoektProber do
361
412
  )
362
413
  end
363
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
+
364
451
  probe_db
365
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
366
483
  end
367
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.8.0
4
+ version: 16.10.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: 8.0.1
75
+ version: 8.0.2
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: 8.0.1
82
+ version: 8.0.2
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: quantile
85
85
  requirement: !ruby/object:Gem::Requirement