diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index 058fe1c8139263b4558053723bfbdef6e437b763..5f579f90629324101009a0095a35f60df4ecf238 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -20,6 +20,8 @@ def thread? !thread.nil? end + # Possible options: + # - synchronous [Boolean] if true, turns `start` into a blocking call def initialize(**options) @synchronous = options[:synchronous] @mutex = Mutex.new diff --git a/lib/gitlab/metrics/exporter/base_exporter.rb b/lib/gitlab/metrics/exporter/base_exporter.rb index 4874ef62e4154b71024c0255ddde5a867d70b29d..2aea8d655fa1ff3f4b8960217842c146e2461e15 100644 --- a/lib/gitlab/metrics/exporter/base_exporter.rb +++ b/lib/gitlab/metrics/exporter/base_exporter.rb @@ -9,6 +9,10 @@ module Exporter class BaseExporter < Daemon 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) super(**options) diff --git a/metrics_server/dependencies.rb b/metrics_server/dependencies.rb index 5615cef42cebfd65cf3e1e363a771294f932d9ab..02cec1173e0ca1a2660183b3df55b7d0e85e11f8 100644 --- a/metrics_server/dependencies.rb +++ b/metrics_server/dependencies.rb @@ -23,6 +23,7 @@ require_relative '../lib/gitlab/metrics/samplers/base_sampler' require_relative '../lib/gitlab/metrics/samplers/ruby_sampler' 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/metrics_middleware' require_relative '../lib/gitlab/metrics/exporter/health_checks_middleware' diff --git a/metrics_server/metrics_server.rb b/metrics_server/metrics_server.rb index 68d39eaf77c2deba7846389b8a54ae7177b87ea3..f1bcd6f611eedc239f1953766379398640978ef3 100644 --- a/metrics_server/metrics_server.rb +++ b/metrics_server/metrics_server.rb @@ -7,7 +7,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass class << self 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 @@ -52,11 +52,18 @@ def start # Warming up ensures that these files exist prior to the exporter starting up. Gitlab::Metrics::Samplers::RubySampler.initialize_instance(prefix: name, warmup: true).start - exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize - settings = Settings.new(Settings.monitoring[name]) - server = exporter_class.instance(settings, gc_requests: true, synchronous: true) + default_opts = { gc_requests: true, synchronous: true } + exporter = + 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 def name diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb index 0178017ee371a22abc7851ba050d196f6a03e063..3b92c3c38fab6170040666b8a8db14083780d1b7 100644 --- a/spec/commands/metrics_server/metrics_server_spec.rb +++ b/spec/commands/metrics_server/metrics_server_spec.rb @@ -12,6 +12,11 @@ { 'test' => { 'monitoring' => { + 'web_exporter' => { + 'address' => 'localhost', + 'enabled' => true, + 'port' => 3807 + }, 'sidekiq_exporter' => { 'address' => 'localhost', 'enabled' => true, @@ -22,56 +27,58 @@ } end - context 'with a running server' do - let(:metrics_dir) { Dir.mktmpdir } + %w(puma sidekiq).each do |target| + context "with a running server targeting #{target}" do + let(:metrics_dir) { Dir.mktmpdir } - before do - # We need to send a request to localhost - WebMock.allow_net_connect! + before do + # We need to send a request to localhost + WebMock.allow_net_connect! - config_file.write(YAML.dump(config)) - config_file.close + config_file.write(YAML.dump(config)) + config_file.close - env = { - 'GITLAB_CONFIG' => config_file.path, - 'METRICS_SERVER_TARGET' => 'sidekiq', - 'WIPE_METRICS_DIR' => '1', - 'prometheus_multiproc_dir' => metrics_dir - } - @pid = Process.spawn(env, 'bin/metrics-server', pgroup: true) - end + env = { + 'GITLAB_CONFIG' => config_file.path, + 'METRICS_SERVER_TARGET' => target, + 'WIPE_METRICS_DIR' => '1', + 'prometheus_multiproc_dir' => metrics_dir + } + @pid = Process.spawn(env, 'bin/metrics-server', pgroup: true) + end - after do - webmock_enable! + after do + webmock_enable! - if @pid - pgrp = Process.getpgid(@pid) + if @pid + pgrp = Process.getpgid(@pid) - Timeout.timeout(10) do - Process.kill('TERM', -pgrp) - Process.waitpid(@pid) - end + Timeout.timeout(10) do + Process.kill('TERM', -pgrp) + Process.waitpid(@pid) + 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 - 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 - expect do - Timeout.timeout(10) do - http_ok = false - until http_ok - sleep 1 - response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true) - http_ok = response&.success? + it 'serves /metrics endpoint' do + expect do + Timeout.timeout(10) do + http_ok = false + until http_ok + sleep 1 + response = Gitlab::HTTP.try_get("http://localhost:3807/metrics", allow_local_requests: true) + http_ok = response&.success? + end end - end - end.not_to raise_error + end.not_to raise_error + end end end end diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb index fc18df9b5cda65dcbfc51066b09d305549357d30..c90fd162814f49f4e2a2fb35f3a9839e6137decd 100644 --- a/spec/metrics_server/metrics_server_spec.rb +++ b/spec/metrics_server/metrics_server_spec.rb @@ -15,9 +15,16 @@ # we need to reset it after testing. let!(:old_multiprocess_files_dir) { prometheus_config.multiprocess_files_dir } + let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) } + before do # We do not want this to have knock-on effects on the test process. 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 after do @@ -27,35 +34,49 @@ FileUtils.rm_rf(metrics_dir, secure: true) end - describe '.spawn' do - context 'when in parent process' do - 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) + %w(puma sidekiq).each do |target| + context "when targeting #{target}" do + describe '.spawn' do + context 'when in parent process' do + 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) - end - end + described_class.spawn(target, metrics_dir: metrics_dir) + end + end - context 'when in child process' do - before do - # This signals the process that it's "inside" the fork - expect(Process).to receive(:fork).and_return(nil) - expect(Process).not_to receive(:detach) - end + context 'when in child process' do + before do + # This signals the process that it's "inside" the fork + expect(Process).to receive(:fork).and_return(nil) + expect(Process).not_to receive(:detach) + end - it 'starts the metrics server with the given arguments' do - expect_next_instance_of(MetricsServer) do |server| - expect(server).to receive(:start) - end + it 'starts the metrics server with the given arguments' do + expect_next_instance_of(MetricsServer) do |server| + expect(server).to receive(:start) + end - described_class.spawn('sidekiq', metrics_dir: metrics_dir) - end + described_class.spawn(target, metrics_dir: metrics_dir) + end - it 'resets signal handlers from parent process' do - expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT') + it 'resets signal handlers from parent process' do + 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 @@ -64,7 +85,6 @@ let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) } let(:exporter_double) { double('fake_exporter', start: 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)} @@ -74,9 +94,6 @@ settings['fake_exporter'], gc_requests: true, synchronous: true ).and_return(exporter_double) 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 it 'configures ::Prometheus::Client' do