Commit 6aa8c3ae authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '350546-puma-metrics-server-support' into 'master'

Allow `metrics_server` to target Puma

See merge request gitlab-org/gitlab!78527
parents f27de61d ae94c880
...@@ -20,6 +20,8 @@ module Gitlab ...@@ -20,6 +20,8 @@ module Gitlab
!thread.nil? !thread.nil?
end end
# Possible options:
# - synchronous [Boolean] if true, turns `start` into a blocking call
def initialize(**options) def initialize(**options)
@synchronous = options[:synchronous] @synchronous = options[:synchronous]
@mutex = Mutex.new @mutex = Mutex.new
......
...@@ -9,6 +9,10 @@ module Gitlab ...@@ -9,6 +9,10 @@ module Gitlab
class BaseExporter < Daemon class BaseExporter < Daemon
attr_reader :server attr_reader :server
# @param settings [Hash] SettingsLogic hash containing the `*_exporter` config
# @param log_enabled [Boolean] whether to log HTTP requests
# @param log_file [String] path to where the server log should be located
# @param gc_requests [Boolean] whether to run a major GC after each scraper request
def initialize(settings, log_enabled:, log_file:, gc_requests: false, **options) def initialize(settings, log_enabled:, log_file:, gc_requests: false, **options)
super(**options) super(**options)
......
...@@ -23,6 +23,7 @@ require_relative '../lib/gitlab/metrics/system' ...@@ -23,6 +23,7 @@ require_relative '../lib/gitlab/metrics/system'
require_relative '../lib/gitlab/metrics/samplers/base_sampler' require_relative '../lib/gitlab/metrics/samplers/base_sampler'
require_relative '../lib/gitlab/metrics/samplers/ruby_sampler' require_relative '../lib/gitlab/metrics/samplers/ruby_sampler'
require_relative '../lib/gitlab/metrics/exporter/base_exporter' require_relative '../lib/gitlab/metrics/exporter/base_exporter'
require_relative '../lib/gitlab/metrics/exporter/web_exporter'
require_relative '../lib/gitlab/metrics/exporter/sidekiq_exporter' require_relative '../lib/gitlab/metrics/exporter/sidekiq_exporter'
require_relative '../lib/gitlab/metrics/exporter/metrics_middleware' require_relative '../lib/gitlab/metrics/exporter/metrics_middleware'
require_relative '../lib/gitlab/metrics/exporter/health_checks_middleware' require_relative '../lib/gitlab/metrics/exporter/health_checks_middleware'
......
...@@ -7,7 +7,7 @@ require_relative 'dependencies' ...@@ -7,7 +7,7 @@ require_relative 'dependencies'
class MetricsServer # rubocop:disable Gitlab/NamespacedClass class MetricsServer # rubocop:disable Gitlab/NamespacedClass
class << self class << self
def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: []) def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
raise "The only valid target is 'sidekiq' currently" unless target == 'sidekiq' raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
pid = Process.fork pid = Process.fork
...@@ -52,11 +52,18 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass ...@@ -52,11 +52,18 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
# Warming up ensures that these files exist prior to the exporter starting up. # Warming up ensures that these files exist prior to the exporter starting up.
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(prefix: name, warmup: true).start Gitlab::Metrics::Samplers::RubySampler.initialize_instance(prefix: name, warmup: true).start
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize default_opts = { gc_requests: true, synchronous: true }
settings = Settings.new(Settings.monitoring[name]) exporter =
server = exporter_class.instance(settings, gc_requests: true, synchronous: true) case @target
when 'puma'
Gitlab::Metrics::Exporter::WebExporter.instance(**default_opts)
else
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
settings = Settings.new(Settings.monitoring[name])
exporter_class.instance(settings, **default_opts)
end
server.start exporter.start
end end
def name def name
......
...@@ -12,6 +12,11 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do ...@@ -12,6 +12,11 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
{ {
'test' => { 'test' => {
'monitoring' => { 'monitoring' => {
'web_exporter' => {
'address' => 'localhost',
'enabled' => true,
'port' => 3807
},
'sidekiq_exporter' => { 'sidekiq_exporter' => {
'address' => 'localhost', 'address' => 'localhost',
'enabled' => true, 'enabled' => true,
...@@ -22,56 +27,58 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do ...@@ -22,56 +27,58 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
} }
end end
context 'with a running server' do %w(puma sidekiq).each do |target|
let(:metrics_dir) { Dir.mktmpdir } context "with a running server targeting #{target}" do
let(:metrics_dir) { Dir.mktmpdir }
before do before do
# We need to send a request to localhost # We need to send a request to localhost
WebMock.allow_net_connect! WebMock.allow_net_connect!
config_file.write(YAML.dump(config)) config_file.write(YAML.dump(config))
config_file.close config_file.close
env = { env = {
'GITLAB_CONFIG' => config_file.path, 'GITLAB_CONFIG' => config_file.path,
'METRICS_SERVER_TARGET' => 'sidekiq', 'METRICS_SERVER_TARGET' => target,
'WIPE_METRICS_DIR' => '1', 'WIPE_METRICS_DIR' => '1',
'prometheus_multiproc_dir' => metrics_dir 'prometheus_multiproc_dir' => metrics_dir
} }
@pid = Process.spawn(env, 'bin/metrics-server', pgroup: true) @pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
end end
after do after do
webmock_enable! webmock_enable!
if @pid if @pid
pgrp = Process.getpgid(@pid) pgrp = Process.getpgid(@pid)
Timeout.timeout(10) do Timeout.timeout(10) do
Process.kill('TERM', -pgrp) Process.kill('TERM', -pgrp)
Process.waitpid(@pid) Process.waitpid(@pid)
end end
expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false) expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false)
end
rescue Errno::ESRCH => _
# 'No such process' means the process died before
ensure
config_file.unlink
FileUtils.rm_rf(metrics_dir, secure: true)
end end
rescue Errno::ESRCH => _
# 'No such process' means the process died before
ensure
config_file.unlink
FileUtils.rm_rf(metrics_dir, secure: true)
end
it 'serves /metrics endpoint' do it 'serves /metrics endpoint' do
expect do expect do
Timeout.timeout(10) do Timeout.timeout(10) do
http_ok = false http_ok = false
until http_ok until http_ok
sleep 1 sleep 1
response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true) response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true)
http_ok = response&.success? http_ok = response&.success?
end
end end
end end.not_to raise_error
end.not_to raise_error end
end end
end end
end end
...@@ -15,9 +15,16 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -15,9 +15,16 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
# we need to reset it after testing. # we need to reset it after testing.
let!(:old_multiprocess_files_dir) { prometheus_config.multiprocess_files_dir } let!(:old_multiprocess_files_dir) { prometheus_config.multiprocess_files_dir }
let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) }
before do before do
# We do not want this to have knock-on effects on the test process. # We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals) allow(Gitlab::ProcessManagement).to receive(:modify_signals)
# This being a singleton, we stub it out because only one instance is allowed
# to exist per process.
allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double)
allow(ruby_sampler_double).to receive(:start)
end end
after do after do
...@@ -27,35 +34,49 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -27,35 +34,49 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
FileUtils.rm_rf(metrics_dir, secure: true) FileUtils.rm_rf(metrics_dir, secure: true)
end end
describe '.spawn' do %w(puma sidekiq).each do |target|
context 'when in parent process' do context "when targeting #{target}" do
it 'forks into a new process and detaches it' do describe '.spawn' do
expect(Process).to receive(:fork).and_return(99) context 'when in parent process' do
expect(Process).to receive(:detach).with(99) it 'forks into a new process and detaches it' do
expect(Process).to receive(:fork).and_return(99)
expect(Process).to receive(:detach).with(99)
described_class.spawn('sidekiq', metrics_dir: metrics_dir) described_class.spawn(target, metrics_dir: metrics_dir)
end end
end end
context 'when in child process' do context 'when in child process' do
before do before do
# This signals the process that it's "inside" the fork # This signals the process that it's "inside" the fork
expect(Process).to receive(:fork).and_return(nil) expect(Process).to receive(:fork).and_return(nil)
expect(Process).not_to receive(:detach) expect(Process).not_to receive(:detach)
end end
it 'starts the metrics server with the given arguments' do it 'starts the metrics server with the given arguments' do
expect_next_instance_of(MetricsServer) do |server| expect_next_instance_of(MetricsServer) do |server|
expect(server).to receive(:start) expect(server).to receive(:start)
end end
described_class.spawn('sidekiq', metrics_dir: metrics_dir) described_class.spawn(target, metrics_dir: metrics_dir)
end end
it 'resets signal handlers from parent process' do it 'resets signal handlers from parent process' do
expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT') expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
described_class.spawn('sidekiq', metrics_dir: metrics_dir, trapped_signals: %i[A B]) described_class.spawn(target, metrics_dir: metrics_dir, trapped_signals: %i[A B])
end
end
end
end
end
context 'when targeting invalid target' do
describe '.spawn' do
it 'raises an error' do
expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
raise_error('Target must be one of [puma,sidekiq]')
)
end end
end end
end end
...@@ -64,7 +85,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -64,7 +85,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) } let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
let(:exporter_double) { double('fake_exporter', start: true) } let(:exporter_double) { double('fake_exporter', start: true) }
let(:settings) { { "fake_exporter" => { "enabled" => true } } } let(:settings) { { "fake_exporter" => { "enabled" => true } } }
let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) }
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)} subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
...@@ -74,9 +94,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath ...@@ -74,9 +94,6 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
settings['fake_exporter'], gc_requests: true, synchronous: true settings['fake_exporter'], gc_requests: true, synchronous: true
).and_return(exporter_double) ).and_return(exporter_double)
expect(Settings).to receive(:monitoring).and_return(settings) expect(Settings).to receive(:monitoring).and_return(settings)
allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double)
allow(ruby_sampler_double).to receive(:start)
end end
it 'configures ::Prometheus::Client' do it 'configures ::Prometheus::Client' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment