spec/elastic_apm/central_config_spec.rb (282 lines of code) (raw):

# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # frozen_string_literal: true require 'spec_helper' module ElasticAPM RSpec.describe CentralConfig do let(:config) do Config.new( central_config: true, service_name: 'MyApp', log_level: Logger::DEBUG ) end subject { described_class.new(config) } describe '#start' do it 'polls for config' do req_stub = stub_response({ transaction_sample_rate: '0.5' }) subject.start subject.promise.wait expect(req_stub).to have_been_requested.at_least_once subject.stop end context 'with complex service name' do let(:config) do Config.new( central_config: true, service_name: 'My app with +-_', log_level: Logger::DEBUG ) end it 'escapes chars' do req_stub = stub_response({}, service_name: 'My%20app%20with%20%2B-_') subject.start subject.promise.wait expect(req_stub).to have_been_requested.at_least_once subject.stop end end context 'environment' do let(:config) do Config.new( central_config: true, service_name: 'MyApp', environment: 'staging' ) end it 'includes env in params' do req_stub = stub_response({}, environment: 'staging') subject.start subject.promise.wait expect(req_stub).to have_been_requested.at_least_once subject.stop end end context 'when disabled' do let(:config) { Config.new(central_config: false) } it 'does nothing' do req_stub = stub_response({ transaction_sample_rate: '0.5' }) subject.start expect(subject.promise).to be nil expect(req_stub).to_not have_been_requested subject.stop end end end describe 'stop and start again' do before do subject.start subject.stop end after { subject.stop } it 'restarts fetching the config' do req_stub = stub_response({ transaction_sample_rate: '0.5' }) subject.start subject.promise.wait expect(req_stub).to have_been_requested.at_least_once end end describe '#fetch_and_apply_config' do it 'queries APM Server and applies config' do req_stub = stub_response({ transaction_sample_rate: '0.5' }) expect(config.logger).to receive(:info) expect(config.logger).to receive(:debug).twice subject.fetch_and_apply_config subject.promise.wait # why more times, sometimes? expect(req_stub).to have_been_requested.at_least_once expect(subject.config.transaction_sample_rate).to eq(0.5) end it 'reverts config if later 404' do stub_response({ transaction_sample_rate: '0.5' }) subject.fetch_and_apply_config subject.promise.wait stub_response('Not found', response: { status: 404 }) subject.fetch_and_apply_config subject.promise.wait expect(subject.config.transaction_sample_rate).to eq(1.0) end context 'when server responds 200 and cache-control' do it 'schedules a new poll' do stub_response( {}, response: { headers: { 'Cache-Control': 'must-revalidate, max-age=123' } } ) subject.fetch_and_apply_config subject.promise.wait expect(subject.scheduled_task).to be_pending expect(subject.scheduled_task.initial_delay).to eq 123 end end context 'when server responds 304' do it 'doesn\'t restore config, schedules a new poll' do stub_response( { transaction_sample_rate: 0.5 }, response: { headers: { 'Cache-Control': 'must-revalidate, max-age=0.1' } } ) subject.fetch_and_apply_config subject.promise.wait stub_response( nil, response: { status: 304, headers: { 'Cache-Control': 'must-revalidate, max-age=123' } } ) subject.fetch_and_apply_config subject.promise.wait expect(subject.scheduled_task).to be_pending expect(subject.scheduled_task.initial_delay).to eq 123 expect(subject.config.transaction_sample_rate).to eq(0.5) end end context 'when server sends etag header' do it 'includes etag in next request' do stub_response( nil, response: { headers: { 'Etag': '___etag___' } } ) subject.fetch_and_apply_config subject.promise.wait stub_response( nil, request: { headers: { 'If-None-Match': '___etag___' } }, response: { headers: { 'Etag': '___etag___' } } ) subject.fetch_and_apply_config subject.promise.wait end end context 'when server responds 404' do it 'schedules a new poll' do stub_response('Not found', response: { status: 404 }) subject.fetch_and_apply_config subject.promise.wait expect(subject.scheduled_task).to be_pending expect(subject.scheduled_task.initial_delay).to eq 300 end end context 'when there is a network error' do it 'schedules a new poll' do stub_response(nil, error: HTTP::ConnectionError) subject.fetch_and_apply_config subject.promise.wait expect(subject.scheduled_task).to be_pending expect(subject.scheduled_task.initial_delay).to eq 300 end end end describe '#fetch_config' do context 'when successful' do it 'returns response object' do stub_response({ ok: 1 }) expect(subject.fetch_config).to be_a(HTTP::Response) end end context 'when not found' do before do stub_response('Not found', response: { status: 404 }) end it 'raises an error' do expect { subject.fetch_config } .to raise_error(CentralConfig::ClientError) end it 'includes the response' do begin subject.fetch_config rescue CentralConfig::ClientError => e expect(e.response).to be_a(HTTP::Response) end end end context 'when server error' do it 'raises an error' do stub_response('Server error', response: { status: 500 }) expect { subject.fetch_config } .to raise_error(CentralConfig::ServerError) end end context 'with a secret token' do before { config.secret_token = 'zecret' } it 'sets auth header' do stub_response( {}, request: { headers: { 'Authorization': 'Bearer zecret' } } ) subject.fetch_config end end context 'with an api key' do before do config.api_key = 'a_base64_encoded_string' end it 'sets auth header' do stub_response( {}, request: { headers: { 'Authorization': 'ApiKey a_base64_encoded_string' } } ) subject.fetch_config end end end describe '#assign' do it 'updates config' do subject.assign(transaction_sample_rate: 0.5) expect(subject.config.transaction_sample_rate).to eq(0.5) end it 'reverts to previous when missing' do subject.assign(transaction_sample_rate: 0.5) subject.assign({}) expect(subject.config.transaction_sample_rate).to eq(1.0) end it 'goes back and forth' do subject.assign(transaction_sample_rate: 0.5) subject.assign({}) subject.assign(transaction_sample_rate: 0.5) expect(subject.config.transaction_sample_rate).to eq(0.5) end describe 'log level' do it 'maps `trace` to `debug`' do subject.assign(log_level: 'trace') expect(subject.config.log_level).to eq(Logger::DEBUG) end it 'maps `critical` to `fatal`' do subject.assign(log_level: 'critical') expect(subject.config.log_level).to eq(Logger::FATAL) end it 'maps `off` to `fatal`' do subject.assign(log_level: 'off') expect(subject.config.log_level).to eq(Logger::FATAL) end it 'maps `debug` to `debug`' do subject.assign(log_level: 'debug') expect(subject.config.log_level).to eq(Logger::DEBUG) end end end describe '#handle_forking!' do it 'reschedules the scheduled task' do req_stub = stub_response({ transaction_sample_rate: '0.5' }) subject.handle_forking! subject.promise.wait expect(subject.scheduled_task).to be_pending expect(req_stub).to have_been_requested.at_least_once subject.stop end end def stub_response(body, request: {}, response: {}, error: nil, service_name: "MyApp", environment: ENV['RAILS_ENV']) url = "http://localhost:8200/config/v1/agents" \ "?service.name=#{service_name}" \ "&service.environment=#{environment}" return stub_request(:get, url).to_raise(error) if error stub_request(:get, url).tap do |stub| stub.with(request) if request.any? stub.to_return(body: body&.to_json, **response) end end end end