gitlab-exporter 16.9.0 → 17.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 +4 -4
- data/Gemfile.lock +2 -18
- data/config/gitlab-exporter.yml.example +0 -1
- data/gitlab-exporter.gemspec +4 -2
- data/lib/gitlab_exporter/database/bloat_btree.sql +143 -103
- data/lib/gitlab_exporter/version.rb +1 -1
- data/lib/gitlab_exporter/web_exporter.rb +96 -104
- data/spec/web_exporter_spec.rb +127 -0
- metadata +17 -30
- data/lib/gitlab_exporter/rack_vulndb_255039_patch.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc0cadf1891c19066f388f384d51f47b9c489e8e0ee7c162565228fb661bada
|
|
4
|
+
data.tar.gz: 7ae7c0ac398b1832c58c2348297139fd88e3ff02e447669043990312160ed0a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a358dbc90fe7bed11fc33a179fe2eb726e0d218925a56e7f076cc4c51d14272de4adca57cd5ae084e0a57d51feed0cf331b2dbf3ae67a5c0a2a972c4dba1863
|
|
7
|
+
data.tar.gz: c937591c8d81ad5296955bd5a55f92bcdd6f2ecb0f1fe7cdd36e9f429e88f5bbf2da8bce8375e8897fec29eda18ea0dd60fcd1bbfc5070c49e9b886a0faf3037
|
data/Gemfile.lock
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab-exporter (
|
|
4
|
+
gitlab-exporter (17.0.0)
|
|
5
|
+
base64 (= 0.2.0)
|
|
5
6
|
connection_pool (= 2.5.5)
|
|
6
7
|
deep_merge (~> 1.2.2)
|
|
7
8
|
faraday (= 2.14.3)
|
|
8
9
|
pg (= 1.6.3)
|
|
9
|
-
puma (= 8.0.1)
|
|
10
10
|
quantile (= 0.2.1)
|
|
11
11
|
redis (= 4.8.1)
|
|
12
12
|
redis-namespace (= 1.11.0)
|
|
13
13
|
sidekiq (= 6.5.12)
|
|
14
|
-
sinatra (~> 3.2.0)
|
|
15
14
|
webrick (~> 1.9)
|
|
16
15
|
|
|
17
16
|
GEM
|
|
@@ -31,23 +30,15 @@ GEM
|
|
|
31
30
|
language_server-protocol (3.17.0.5)
|
|
32
31
|
lint_roller (1.1.0)
|
|
33
32
|
logger (1.7.0)
|
|
34
|
-
mustermann (3.0.3)
|
|
35
|
-
ruby2_keywords (~> 0.0.1)
|
|
36
|
-
nio4r (2.7.4)
|
|
37
33
|
parallel (1.27.0)
|
|
38
34
|
parser (3.3.9.0)
|
|
39
35
|
ast (~> 2.4.1)
|
|
40
36
|
racc
|
|
41
37
|
pg (1.6.3)
|
|
42
38
|
prism (1.9.0)
|
|
43
|
-
puma (8.0.1)
|
|
44
|
-
nio4r (~> 2.0)
|
|
45
39
|
quantile (0.2.1)
|
|
46
40
|
racc (1.8.1)
|
|
47
41
|
rack (2.2.15)
|
|
48
|
-
rack-protection (3.2.0)
|
|
49
|
-
base64 (>= 0.1.0)
|
|
50
|
-
rack (~> 2.2, >= 2.2.4)
|
|
51
42
|
rainbow (3.1.1)
|
|
52
43
|
redis (4.8.1)
|
|
53
44
|
redis-namespace (1.11.0)
|
|
@@ -81,17 +72,10 @@ GEM
|
|
|
81
72
|
parser (>= 3.3.7.2)
|
|
82
73
|
prism (~> 1.7)
|
|
83
74
|
ruby-progressbar (1.13.0)
|
|
84
|
-
ruby2_keywords (0.0.5)
|
|
85
75
|
sidekiq (6.5.12)
|
|
86
76
|
connection_pool (>= 2.2.5, < 3)
|
|
87
77
|
rack (~> 2.0)
|
|
88
78
|
redis (>= 4.5.0, < 5)
|
|
89
|
-
sinatra (3.2.0)
|
|
90
|
-
mustermann (~> 3.0)
|
|
91
|
-
rack (~> 2.2, >= 2.2.4)
|
|
92
|
-
rack-protection (= 3.2.0)
|
|
93
|
-
tilt (~> 2.0)
|
|
94
|
-
tilt (2.6.0)
|
|
95
79
|
unicode-display_width (3.1.4)
|
|
96
80
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
|
97
81
|
unicode-emoji (4.0.4)
|
data/gitlab-exporter.gemspec
CHANGED
|
@@ -22,16 +22,18 @@ Gem::Specification.new do |s|
|
|
|
22
22
|
|
|
23
23
|
s.required_ruby_version = Gem::Requirement.new(">= 3.0")
|
|
24
24
|
|
|
25
|
+
# base64 is required by sidekiq but not declared by it, and is no longer a
|
|
26
|
+
# default gem as of Ruby 3.4. It used to be pulled in transitively via
|
|
27
|
+
# sinatra -> rack-protection; declare it directly now that those are gone.
|
|
28
|
+
s.add_runtime_dependency "base64", "0.2.0"
|
|
25
29
|
s.add_runtime_dependency "connection_pool", "2.5.5"
|
|
26
30
|
s.add_runtime_dependency "deep_merge", "~> 1.2.2"
|
|
27
31
|
s.add_runtime_dependency "faraday", "2.14.3"
|
|
28
32
|
s.add_runtime_dependency "pg", "1.6.3"
|
|
29
|
-
s.add_runtime_dependency "puma", "8.0.1"
|
|
30
33
|
s.add_runtime_dependency "quantile", "0.2.1"
|
|
31
34
|
s.add_runtime_dependency "redis", "4.8.1"
|
|
32
35
|
s.add_runtime_dependency "redis-namespace", "1.11.0"
|
|
33
36
|
s.add_runtime_dependency "sidekiq", "6.5.12"
|
|
34
|
-
s.add_runtime_dependency "sinatra", "~> 3.2.0"
|
|
35
37
|
s.add_runtime_dependency "webrick", "~> 1.9"
|
|
36
38
|
|
|
37
39
|
s.add_development_dependency "rspec", "~> 3.12.0"
|
|
@@ -1,107 +1,147 @@
|
|
|
1
1
|
-- Originally from: https://github.com/ioguix/pgsql-bloat-estimation/blob/master/btree/btree_bloat.sql
|
|
2
|
-
--
|
|
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
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
SELECT
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
17
34
|
ELSE 0
|
|
18
35
|
END AS bloat_size,
|
|
19
|
-
100 * (relpages-est_pages_ff)::float / relpages AS bloat_ratio,
|
|
20
|
-
is_na
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,146 +1,138 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "webrick"
|
|
2
2
|
require "English"
|
|
3
3
|
require "cgi"
|
|
4
|
+
require "stringio"
|
|
4
5
|
|
|
5
|
-
require_relative "rack_vulndb_255039_patch"
|
|
6
6
|
require_relative "tls_helper"
|
|
7
7
|
|
|
8
8
|
module GitLab
|
|
9
9
|
module Exporter
|
|
10
|
-
# Metrics web exporter
|
|
11
|
-
class WebExporter
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@app.call(env)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def memory_usage
|
|
31
|
-
io = IO.popen(%W[ps -o rss= -p #{$PID}])
|
|
32
|
-
|
|
33
|
-
mem = io.read
|
|
34
|
-
io.close
|
|
35
|
-
|
|
36
|
-
return 0 unless $CHILD_STATUS.to_i.zero?
|
|
37
|
-
|
|
38
|
-
mem.to_i
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Performs a major GC after each request. We found that this helps to free up
|
|
43
|
-
# several MB of memory in conjunction with sricter malloc config.
|
|
44
|
-
# See https://gitlab.com/gitlab-org/gitlab/-/issues/297241
|
|
45
|
-
class RunGC
|
|
46
|
-
def initialize(app)
|
|
47
|
-
@app = app
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def call(env)
|
|
51
|
-
@app.call(env).tap do
|
|
52
|
-
GC.start
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
10
|
+
# Metrics web exporter backed directly by WEBrick (no Sinatra/Rack).
|
|
11
|
+
class WebExporter
|
|
12
|
+
DEFAULT_WEB_SERVER = "webrick".freeze
|
|
13
|
+
DEFAULT_LISTEN_ADDRESS = "0.0.0.0".freeze
|
|
14
|
+
DEFAULT_LISTEN_PORT = 9168
|
|
15
|
+
DEFAULT_MEMORY_THRESHOLD = 1024
|
|
16
|
+
|
|
17
|
+
# Security response headers previously added by Sinatra's Rack::Protection
|
|
18
|
+
# middleware, which is gone now that we serve directly via WEBrick. Kept for
|
|
19
|
+
# parity so scanners and consumers see the same headers as before.
|
|
20
|
+
SECURITY_HEADERS = {
|
|
21
|
+
"X-Content-Type-Options" => "nosniff",
|
|
22
|
+
"X-Frame-Options" => "SAMEORIGIN",
|
|
23
|
+
"X-XSS-Protection" => "1; mode=block"
|
|
24
|
+
}.freeze
|
|
56
25
|
|
|
57
26
|
class << self
|
|
58
27
|
include TLSHelper
|
|
59
28
|
|
|
60
|
-
DEFAULT_WEB_SERVER = "webrick".freeze
|
|
61
|
-
|
|
62
29
|
def setup(config)
|
|
63
|
-
|
|
64
|
-
setup_probes(config[:probes])
|
|
30
|
+
server_config = config[:server] || {}
|
|
65
31
|
|
|
66
|
-
memory_threshold = (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
32
|
+
memory_threshold = server_config.fetch(:memory_threshold, DEFAULT_MEMORY_THRESHOLD)
|
|
33
|
+
@memory_threshold = memory_threshold.to_i * 1024
|
|
34
|
+
|
|
35
|
+
@server = build_server(server_config)
|
|
36
|
+
mount_probes(config[:probes])
|
|
70
37
|
|
|
71
38
|
# Defrag heap after everything is loaded into memory.
|
|
72
39
|
GC.compact
|
|
73
40
|
end
|
|
74
41
|
|
|
75
|
-
def
|
|
76
|
-
|
|
42
|
+
def run!
|
|
43
|
+
trap_signals
|
|
44
|
+
@server.start
|
|
77
45
|
end
|
|
78
46
|
|
|
79
|
-
|
|
80
|
-
config ||= {}
|
|
47
|
+
private
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
|
|
49
|
+
def build_server(config)
|
|
50
|
+
warn_deprecated_server(config[:name])
|
|
51
|
+
|
|
52
|
+
options = {
|
|
53
|
+
Port: config.fetch(:listen_port, DEFAULT_LISTEN_PORT),
|
|
54
|
+
BindAddress: config.fetch(:listen_address, DEFAULT_LISTEN_ADDRESS)
|
|
55
|
+
}
|
|
84
56
|
|
|
85
|
-
# Depending on whether TLS is enabled or not, bind string
|
|
86
|
-
# will be different.
|
|
87
57
|
if config.fetch(:tls_enabled, "false").to_s == "true"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
set(:bind, config.fetch(:listen_address, "0.0.0.0"))
|
|
58
|
+
validate_tls_config(config)
|
|
59
|
+
options.merge!(webrick_tls_config(config))
|
|
91
60
|
end
|
|
61
|
+
|
|
62
|
+
WEBrick::HTTPServer.new(**options)
|
|
92
63
|
end
|
|
93
64
|
|
|
94
|
-
|
|
95
|
-
|
|
65
|
+
# The web server used to be configurable (e.g. Puma) via the `server.name`
|
|
66
|
+
# setting when the exporter ran on top of Sinatra. It now always uses WEBrick,
|
|
67
|
+
# so the setting is ignored. Warn operators whose config still sets it.
|
|
68
|
+
#
|
|
69
|
+
# TODO: This is step one of a two-step deprecation. In a future major release,
|
|
70
|
+
# raise instead of warning so the ignored `server.name` setting fails loudly.
|
|
71
|
+
def warn_deprecated_server(name)
|
|
72
|
+
return if name.nil? || name.to_s == DEFAULT_WEB_SERVER
|
|
73
|
+
|
|
74
|
+
warn "DEPRECATION: `server.name` (#{name.inspect}) is no longer supported and is ignored; " \
|
|
75
|
+
"the exporter now always uses WEBrick. Please remove `server.name` from your config."
|
|
76
|
+
end
|
|
96
77
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
def mount_probes(probes)
|
|
79
|
+
(probes || {}).each do |probe_name, params|
|
|
80
|
+
opts =
|
|
81
|
+
if params.delete(:multiple)
|
|
82
|
+
params
|
|
83
|
+
else
|
|
84
|
+
{ probe_name => params }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@server.mount_proc("/#{probe_name}") do |_request, response|
|
|
88
|
+
handle_probe(response, opts)
|
|
89
|
+
end
|
|
104
90
|
end
|
|
105
91
|
end
|
|
106
92
|
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
def handle_probe(response, opts)
|
|
94
|
+
# A memory killer that kills the process if we exceeded a certain threshold.
|
|
95
|
+
check_memory_threshold!
|
|
110
96
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
97
|
+
prober = Prober.new(metrics: PrometheusMetrics.new(include_timestamp: false), logger: @server.logger, **opts)
|
|
98
|
+
prober.probe_all
|
|
99
|
+
|
|
100
|
+
buffer = StringIO.new
|
|
101
|
+
prober.write_to(buffer)
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
SECURITY_HEADERS.each do |header, value|
|
|
104
|
+
response[header] = value
|
|
105
|
+
end
|
|
106
|
+
response.content_type = "text/plain; version=0.0.4; charset=utf-8"
|
|
107
|
+
response.body = buffer.string
|
|
108
|
+
ensure
|
|
109
|
+
# Performs a major GC after each request. We found that this helps to free up
|
|
110
|
+
# several MB of memory in conjunction with stricter malloc config.
|
|
111
|
+
# See https://gitlab.com/gitlab-org/gitlab/-/issues/297241
|
|
112
|
+
GC.start
|
|
113
|
+
end
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
def check_memory_threshold!
|
|
116
|
+
usage = memory_usage
|
|
117
|
+
return if usage <= @memory_threshold
|
|
122
118
|
|
|
123
|
-
|
|
119
|
+
puts "Memory usage of #{usage} exceeded threshold of #{@memory_threshold}, signalling KILL"
|
|
120
|
+
Process.kill("KILL", $PID)
|
|
124
121
|
end
|
|
125
122
|
|
|
126
|
-
def
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
params
|
|
131
|
-
else
|
|
132
|
-
{ probe_name => params }
|
|
133
|
-
end
|
|
123
|
+
def memory_usage
|
|
124
|
+
# Use the block form so the pipe is closed automatically even if the
|
|
125
|
+
# read raises, avoiding a file-descriptor leak.
|
|
126
|
+
mem = IO.popen(%W[ps -o rss= -p #{$PID}], &:read)
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
content_type "text/plain; version=0.0.4; charset=utf-8"
|
|
137
|
-
prober = Prober.new(metrics: PrometheusMetrics.new(include_timestamp: false), logger: logger, **opts)
|
|
128
|
+
return 0 unless $CHILD_STATUS.to_i.zero?
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
mem.to_i
|
|
131
|
+
end
|
|
141
132
|
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
def trap_signals
|
|
134
|
+
%w[INT TERM].each do |signal|
|
|
135
|
+
trap(signal) { @server.shutdown }
|
|
144
136
|
end
|
|
145
137
|
end
|
|
146
138
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "gitlab_exporter/web_exporter"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
describe GitLab::Exporter::WebExporter do
|
|
7
|
+
# Each test builds a fresh server via `.setup`; drop the reference afterwards
|
|
8
|
+
# so class-level state does not leak between examples.
|
|
9
|
+
after do
|
|
10
|
+
described_class.instance_variable_set(:@server, nil)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe ".setup" do
|
|
14
|
+
let(:probes) { {} }
|
|
15
|
+
|
|
16
|
+
it "builds a WEBrick server with the configured port and bind address" do
|
|
17
|
+
options = capture_server_options(server: { listen_address: "127.0.0.1", listen_port: 12_345 })
|
|
18
|
+
|
|
19
|
+
expect(options).to include(Port: 12_345, BindAddress: "127.0.0.1")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "falls back to the default port and bind address" do
|
|
23
|
+
options = capture_server_options({})
|
|
24
|
+
|
|
25
|
+
expect(options).to include(Port: 9168, BindAddress: "0.0.0.0")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "mounts a handler for each configured probe" do
|
|
29
|
+
server = instance_double(WEBrick::HTTPServer)
|
|
30
|
+
allow(WEBrick::HTTPServer).to receive(:new).and_return(server)
|
|
31
|
+
|
|
32
|
+
expect(server).to receive(:mount_proc).with("/ruby")
|
|
33
|
+
|
|
34
|
+
described_class.setup(server: {}, probes: { ruby: { methods: %w[probe_gc], opts: {} } })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "raises when TLS is enabled but no certificate is configured" do
|
|
38
|
+
expect {
|
|
39
|
+
described_class.setup(server: { tls_enabled: true }, probes: probes)
|
|
40
|
+
}.to raise_error(/tls_cert_path not specified/)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Capture the keyword options passed to WEBrick without binding a real socket.
|
|
44
|
+
def capture_server_options(config)
|
|
45
|
+
captured = nil
|
|
46
|
+
allow(WEBrick::HTTPServer).to receive(:new) do |**options|
|
|
47
|
+
captured = options
|
|
48
|
+
instance_double(WEBrick::HTTPServer)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
described_class.setup(config.merge(probes: {}))
|
|
52
|
+
captured
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "deprecating the server.name setting" do
|
|
57
|
+
it "warns when a non-WEBrick server is requested" do
|
|
58
|
+
expect {
|
|
59
|
+
described_class.send(:warn_deprecated_server, "puma")
|
|
60
|
+
}.to output(/DEPRECATION.*no longer supported/).to_stderr
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "does not warn when the server is webrick" do
|
|
64
|
+
expect { described_class.send(:warn_deprecated_server, "webrick") }.not_to output.to_stderr
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "does not warn when no server name is set" do
|
|
68
|
+
expect { described_class.send(:warn_deprecated_server, nil) }.not_to output.to_stderr
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "serving probe requests" do
|
|
73
|
+
let(:config) do
|
|
74
|
+
{
|
|
75
|
+
server: { listen_address: "127.0.0.1", listen_port: 0 },
|
|
76
|
+
probes: { ruby: { methods: %w[probe_gc], opts: {} } }
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
let(:server) { described_class.instance_variable_get(:@server) }
|
|
81
|
+
let(:base_uri) { "http://127.0.0.1:#{server.listeners.first.addr[1]}" }
|
|
82
|
+
|
|
83
|
+
before do
|
|
84
|
+
# WEBrick binds its logger and access log to $stderr at construction time
|
|
85
|
+
# and emits a startup banner; silence $stderr around setup so the log
|
|
86
|
+
# device is a throwaway buffer and the test output stays clean.
|
|
87
|
+
original_stderr = $stderr
|
|
88
|
+
$stderr = StringIO.new
|
|
89
|
+
begin
|
|
90
|
+
described_class.setup(config)
|
|
91
|
+
ensure
|
|
92
|
+
$stderr = original_stderr
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@server_thread = Thread.new { server.start }
|
|
96
|
+
Timeout.timeout(5) { sleep 0.01 until server.status == :Running }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
after do
|
|
100
|
+
server.shutdown
|
|
101
|
+
@server_thread&.join(5)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "returns Prometheus metrics for a known probe" do
|
|
105
|
+
response = Net::HTTP.get_response(URI("#{base_uri}/ruby"))
|
|
106
|
+
|
|
107
|
+
expect(response.code).to eq("200")
|
|
108
|
+
expect(response.content_type).to eq("text/plain")
|
|
109
|
+
expect(response["Content-Type"]).to include("version=0.0.4")
|
|
110
|
+
expect(response.body).to match(/ruby_gc_stat_count \d+/)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "sets the security headers previously provided by Rack::Protection" do
|
|
114
|
+
response = Net::HTTP.get_response(URI("#{base_uri}/ruby"))
|
|
115
|
+
|
|
116
|
+
expect(response["X-Content-Type-Options"]).to eq("nosniff")
|
|
117
|
+
expect(response["X-Frame-Options"]).to eq("SAMEORIGIN")
|
|
118
|
+
expect(response["X-XSS-Protection"]).to eq("1; mode=block")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "returns 404 for unknown paths" do
|
|
122
|
+
response = Net::HTTP.get_response(URI("#{base_uri}/does-not-exist"))
|
|
123
|
+
|
|
124
|
+
expect(response.code).to eq("404")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
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:
|
|
4
|
+
version: 17.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pablo Carranza
|
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2016-07-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.2.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.2.0
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: connection_pool
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,20 +80,6 @@ dependencies:
|
|
|
66
80
|
- - '='
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: 1.6.3
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: puma
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - '='
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: 8.0.1
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - '='
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: 8.0.1
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: quantile
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -136,20 +136,6 @@ dependencies:
|
|
|
136
136
|
- - '='
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: 6.5.12
|
|
139
|
-
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: sinatra
|
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
|
142
|
-
requirements:
|
|
143
|
-
- - "~>"
|
|
144
|
-
- !ruby/object:Gem::Version
|
|
145
|
-
version: 3.2.0
|
|
146
|
-
type: :runtime
|
|
147
|
-
prerelease: false
|
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
-
requirements:
|
|
150
|
-
- - "~>"
|
|
151
|
-
- !ruby/object:Gem::Version
|
|
152
|
-
version: 3.2.0
|
|
153
139
|
- !ruby/object:Gem::Dependency
|
|
154
140
|
name: webrick
|
|
155
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -233,7 +219,6 @@ files:
|
|
|
233
219
|
- lib/gitlab_exporter/prober.rb
|
|
234
220
|
- lib/gitlab_exporter/process.rb
|
|
235
221
|
- lib/gitlab_exporter/prometheus.rb
|
|
236
|
-
- lib/gitlab_exporter/rack_vulndb_255039_patch.rb
|
|
237
222
|
- lib/gitlab_exporter/ruby.rb
|
|
238
223
|
- lib/gitlab_exporter/sidekiq.rb
|
|
239
224
|
- lib/gitlab_exporter/tls_helper.rb
|
|
@@ -258,6 +243,7 @@ files:
|
|
|
258
243
|
- spec/sidekiq_spec.rb
|
|
259
244
|
- spec/spec_helper.rb
|
|
260
245
|
- spec/util_spec.rb
|
|
246
|
+
- spec/web_exporter_spec.rb
|
|
261
247
|
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-exporter
|
|
262
248
|
licenses:
|
|
263
249
|
- MIT
|
|
@@ -300,3 +286,4 @@ test_files:
|
|
|
300
286
|
- spec/sidekiq_spec.rb
|
|
301
287
|
- spec/spec_helper.rb
|
|
302
288
|
- spec/util_spec.rb
|
|
289
|
+
- spec/web_exporter_spec.rb
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
if Gem.loaded_specs["rack"].version >= Gem::Version.new("3.0.0")
|
|
4
|
-
fail <<~ERR
|
|
5
|
-
This patch is unnecessary in Rack versions 3.0.0 or newer.
|
|
6
|
-
Please remove this file and the associated spec.
|
|
7
|
-
|
|
8
|
-
See https://github.com/rack/rack/blob/main/CHANGELOG.md#security (issue #1733)
|
|
9
|
-
ERR
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Patches a cache poisoning attack vector in Rack by not allowing semicolons
|
|
13
|
-
# to delimit query parameters.
|
|
14
|
-
# See https://github.com/rack/rack/issues/1732.
|
|
15
|
-
#
|
|
16
|
-
# Solution is taken from the same issue.
|
|
17
|
-
#
|
|
18
|
-
# The actual patch is due for release in Rack 3.0.0.
|
|
19
|
-
module Rack
|
|
20
|
-
class Request # rubocop:disable Style/Documentation
|
|
21
|
-
Helpers.module_eval do
|
|
22
|
-
# rubocop: disable Naming/MethodName
|
|
23
|
-
def GET
|
|
24
|
-
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
|
|
25
|
-
get_header(RACK_REQUEST_QUERY_HASH)
|
|
26
|
-
else
|
|
27
|
-
query_hash = parse_query(query_string, "&") # only allow ampersand here
|
|
28
|
-
set_header(RACK_REQUEST_QUERY_STRING, query_string)
|
|
29
|
-
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
# rubocop: enable Naming/MethodName
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|