diff options
Diffstat (limited to 'spec/lib/gitlab/metrics')
-rw-r--r-- | spec/lib/gitlab/metrics/subscribers/external_http_spec.rb | 172 | ||||
-rw-r--r-- | spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb | 203 |
2 files changed, 375 insertions, 0 deletions
diff --git a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb new file mode 100644 index 00000000000..5bcaf8fbc47 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do + let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:subscriber) { described_class.new } + + let(:event_1) do + double(:event, payload: { + method: 'POST', code: "200", duration: 0.321, + scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + end + + let(:event_2) do + double(:event, payload: { + method: 'GET', code: "301", duration: 0.12, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', + query: 'current=true' + }) + end + + let(:event_3) do + double(:event, payload: { + method: 'POST', duration: 5.3, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', + query: 'current=true', + exception_object: Net::ReadTimeout.new + }) + end + + describe '.detail_store' do + context 'when external HTTP detail store is empty' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + end + + it 'returns an empty array' do + expect(described_class.detail_store).to eql([]) + end + end + + context 'when the performance bar is not enabled' do + it 'returns an empty array' do + expect(described_class.detail_store).to eql([]) + end + end + + context 'when external HTTP detail store has some values' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + Gitlab::SafeRequestStore[:external_http_detail_store] = [{ + method: 'POST', code: "200", duration: 0.321 + }] + end + + it 'returns the external http detailed store' do + expect(described_class.detail_store).to eql([{ method: 'POST', code: "200", duration: 0.321 }]) + end + end + end + + describe '.payload' do + context 'when SafeRequestStore does not have any item from external HTTP' do + it 'returns an empty array' do + expect(described_class.payload).to eql(external_http_count: 0, external_http_duration_s: 0.0) + end + end + + context 'when external HTTP recorded some values' do + before do + Gitlab::SafeRequestStore[:external_http_count] = 7 + Gitlab::SafeRequestStore[:external_http_duration_s] = 1.2 + end + + it 'returns the external http detailed store' do + expect(described_class.payload).to eql(external_http_count: 7, external_http_duration_s: 1.2) + end + end + end + + describe '#request' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + allow(subscriber).to receive(:current_transaction).and_return(transaction) + end + + it 'tracks external HTTP request count' do + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: "200", method: "POST" }) + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: "301", method: "GET" }) + + subscriber.request(event_1) + subscriber.request(event_2) + end + + it 'tracks external HTTP duration' do + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 0.321) + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 0.12) + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 5.3) + + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + end + + it 'tracks external HTTP exceptions' do + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: 'undefined', method: "POST" }) + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_exception_total, 1) + + subscriber.request(event_3) + end + + it 'stores per-request counters' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + expect(Gitlab::SafeRequestStore[:external_http_count]).to eq(3) + expect(Gitlab::SafeRequestStore[:external_http_duration_s]).to eq(5.741) # 0.321 + 0.12 + 5.3 + end + + it 'stores a portion of events into the detail store' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + expect(Gitlab::SafeRequestStore[:external_http_detail_store].length).to eq(3) + expect(Gitlab::SafeRequestStore[:external_http_detail_store][0]).to include( + method: 'POST', code: "200", duration: 0.321, + scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', + query: 'current=true', exception_object: nil, + backtrace: be_a(Array) + ) + expect(Gitlab::SafeRequestStore[:external_http_detail_store][1]).to include( + method: 'GET', code: "301", duration: 0.12, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', + query: 'current=true', exception_object: nil, + backtrace: be_a(Array) + ) + expect(Gitlab::SafeRequestStore[:external_http_detail_store][2]).to include( + method: 'POST', duration: 5.3, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', + query: 'current=true', + exception_object: be_a(Net::ReadTimeout), + backtrace: be_a(Array) + ) + end + + context 'when the performance bar is not enabled' do + before do + Gitlab::SafeRequestStore.delete(:peek_enabled) + end + + it 'does not capture detail store' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + expect(Gitlab::SafeRequestStore[:external_http_detail_store]).to be(nil) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb new file mode 100644 index 00000000000..2d595632772 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do + let(:subscriber) { described_class.new } + + describe '.payload' do + context 'when the request store is empty' do + it 'returns empty data' do + expect(described_class.payload).to eql( + rack_attack_redis_count: 0, + rack_attack_redis_duration_s: 0.0 + ) + end + end + + context 'when the request store already has data' do + before do + Gitlab::SafeRequestStore[:rack_attack_instrumentation] = { + rack_attack_redis_count: 10, + rack_attack_redis_duration_s: 9.0 + } + end + + it 'returns the accumulated data' do + expect(described_class.payload).to eql( + rack_attack_redis_count: 10, + rack_attack_redis_duration_s: 9.0 + ) + end + end + end + + describe '#redis' do + it 'accumulates per-request RackAttack cache usage' do + freeze_time do + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 1.second, '1', { operation: 'fetch' } + ) + ) + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 2.seconds, '1', { operation: 'write' } + ) + ) + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 3.seconds, '1', { operation: 'read' } + ) + ) + end + + expect(Gitlab::SafeRequestStore[:rack_attack_instrumentation]).to eql( + rack_attack_redis_count: 3, + rack_attack_redis_duration_s: 6.0 + ) + end + end + + shared_examples 'log into auth logger' do + context 'when matched throttle does not require user information' do + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_unauthenticated' + } + ) + ) + end + + it 'logs request information' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_unauthenticated' + ) + ) + subscriber.send(match_type, event) + end + end + + context 'when matched throttle requires user information' do + context 'when user not found' do + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_authenticated_api', + 'rack.attack.match_discriminator' => 'not_exist_user_id' + } + ) + ) + end + + it 'logs request information and user id' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_authenticated_api', + user_id: 'not_exist_user_id' + ) + ) + subscriber.send(match_type, event) + end + end + + context 'when user found' do + let(:user) { create(:user) } + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_authenticated_api', + 'rack.attack.match_discriminator' => user.id + } + ) + ) + end + + it 'logs request information and user meta' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_authenticated_api', + user_id: user.id, + 'meta.user' => user.username + ) + ) + subscriber.send(match_type, event) + end + end + end + end + + describe '#throttle' do + let(:match_type) { :throttle } + let(:event_name) { 'throttle.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#blocklist' do + let(:match_type) { :blocklist } + let(:event_name) { 'blocklist.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#track' do + let(:match_type) { :track } + let(:event_name) { 'track.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#safelist' do + let(:event) do + ActiveSupport::Notifications::Event.new( + 'safelist.rack_attack', Time.current, Time.current + 2.seconds, '1', request: double( + :request, + env: { + 'rack.attack.matched' => 'throttle_unauthenticated' + } + ) + ) + end + + it 'adds the matched name to safe request store' do + subscriber.safelist(event) + expect(Gitlab::SafeRequestStore[:instrumentation_throttle_safelist]).to eql('throttle_unauthenticated') + end + end +end |