2018-01-18 13:21:42 +00:00
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe Gitlab::Profiler do
|
|
|
|
RSpec::Matchers.define_negated_matcher :not_change, :change
|
|
|
|
|
|
|
|
let(:null_logger) { Logger.new('/dev/null') }
|
|
|
|
let(:private_token) { 'private' }
|
|
|
|
|
|
|
|
describe '.profile' do
|
|
|
|
let(:app) { double(:app) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow(ActionDispatch::Integration::Session).to receive(:new).and_return(app)
|
|
|
|
allow(app).to receive(:get)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a profile result' do
|
|
|
|
expect(described_class.profile('/')).to be_an_instance_of(RubyProf::Profile)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'uses the custom logger given' do
|
|
|
|
expect(described_class).to receive(:create_custom_logger)
|
|
|
|
.with(null_logger, private_token: anything)
|
|
|
|
.and_call_original
|
|
|
|
|
|
|
|
described_class.profile('/', logger: null_logger)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'sends a POST request when data is passed' do
|
|
|
|
post_data = '{"a":1}'
|
|
|
|
|
|
|
|
expect(app).to receive(:post).with(anything, post_data, anything)
|
|
|
|
|
|
|
|
described_class.profile('/', post_data: post_data)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'uses the private_token for auth if given' do
|
|
|
|
expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
|
|
|
|
expect(app).to receive(:get).with('/api/v4/users')
|
|
|
|
|
|
|
|
described_class.profile('/', private_token: private_token)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'uses the user for auth if given' do
|
|
|
|
user = double(:user)
|
|
|
|
user_token = 'user'
|
|
|
|
|
|
|
|
allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token)
|
|
|
|
|
|
|
|
expect(app).to receive(:get).with('/', nil, 'Private-Token' => user_token)
|
|
|
|
expect(app).to receive(:get).with('/api/v4/users')
|
|
|
|
|
|
|
|
described_class.profile('/', user: user)
|
|
|
|
end
|
|
|
|
|
2018-02-16 06:38:45 +00:00
|
|
|
context 'when providing a user without a personal access token' do
|
|
|
|
it 'raises an error' do
|
|
|
|
user = double(:user)
|
|
|
|
allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([])
|
|
|
|
|
|
|
|
expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-01-18 13:21:42 +00:00
|
|
|
it 'uses the private_token for auth if both it and user are set' do
|
|
|
|
user = double(:user)
|
|
|
|
user_token = 'user'
|
|
|
|
|
|
|
|
allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token)
|
|
|
|
|
|
|
|
expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
|
|
|
|
expect(app).to receive(:get).with('/api/v4/users')
|
|
|
|
|
|
|
|
described_class.profile('/', user: user, private_token: private_token)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.create_custom_logger' do
|
|
|
|
it 'does nothing when nil is passed' do
|
|
|
|
expect(described_class.create_custom_logger(nil)).to be_nil
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'the new logger' do
|
|
|
|
let(:custom_logger) do
|
|
|
|
described_class.create_custom_logger(null_logger, private_token: private_token)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not affect the existing logger' do
|
|
|
|
expect(null_logger).not_to receive(:debug)
|
|
|
|
expect(custom_logger).to receive(:debug).and_call_original
|
|
|
|
|
|
|
|
custom_logger.debug('Foo')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'strips out the private token' do
|
|
|
|
expect(custom_logger).to receive(:add) do |severity, _progname, message|
|
2018-03-09 13:35:21 +00:00
|
|
|
next if message.include?('spec/')
|
|
|
|
|
2018-01-18 13:21:42 +00:00
|
|
|
expect(severity).to eq(Logger::DEBUG)
|
|
|
|
expect(message).to include('public').and include(described_class::FILTERED_STRING)
|
|
|
|
expect(message).not_to include(private_token)
|
2018-03-09 13:35:21 +00:00
|
|
|
end.twice
|
2018-01-18 13:21:42 +00:00
|
|
|
|
|
|
|
custom_logger.debug("public #{private_token}")
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'tracks model load times by model' do
|
|
|
|
custom_logger.debug('This is not a model load')
|
|
|
|
custom_logger.debug('User Load (1.2ms)')
|
|
|
|
custom_logger.debug('User Load (1.3ms)')
|
|
|
|
custom_logger.debug('Project Load (10.4ms)')
|
|
|
|
|
2018-03-21 16:22:56 +00:00
|
|
|
expect(custom_logger.load_times_by_model).to eq('User' => [1.2, 1.3],
|
|
|
|
'Project' => [10.4])
|
2018-01-18 13:21:42 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'logs the backtrace, ignoring lines as appropriate' do
|
|
|
|
# Skip Rails's backtrace cleaning.
|
|
|
|
allow(Rails.backtrace_cleaner).to receive(:clean, &:itself)
|
|
|
|
|
|
|
|
expect(custom_logger).to receive(:add)
|
|
|
|
.with(Logger::DEBUG,
|
|
|
|
anything,
|
|
|
|
a_string_matching(File.basename(__FILE__)))
|
|
|
|
.twice
|
|
|
|
|
|
|
|
expect(custom_logger).not_to receive(:add).with(Logger::DEBUG,
|
|
|
|
anything,
|
|
|
|
a_string_matching('lib/gitlab/profiler.rb'))
|
|
|
|
|
|
|
|
# Force a part of the backtrace to be in the (ignored) profiler source
|
|
|
|
# file.
|
|
|
|
described_class.with_custom_logger(nil) { custom_logger.debug('Foo') }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-15 12:29:55 +00:00
|
|
|
describe '.clean_backtrace' do
|
|
|
|
it 'uses the Rails backtrace cleaner' do
|
|
|
|
backtrace = []
|
|
|
|
|
|
|
|
expect(Rails.backtrace_cleaner).to receive(:clean).with(backtrace)
|
|
|
|
|
|
|
|
described_class.clean_backtrace(backtrace)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'removes lines from IGNORE_BACKTRACES' do
|
|
|
|
backtrace = [
|
|
|
|
"lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:280:in `block in migrate'",
|
|
|
|
"lib/gitlab/metrics/influx_db.rb:103:in `measure'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:278:in `migrate'",
|
|
|
|
"lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'",
|
|
|
|
"lib/gitlab/git/commit.rb:66:in `find'",
|
|
|
|
"app/models/repository.rb:1047:in `find_commit'",
|
|
|
|
"lib/gitlab/metrics/instrumentation.rb:159:in `block in find_commit'",
|
|
|
|
"lib/gitlab/metrics/method_call.rb:36:in `measure'",
|
|
|
|
"lib/gitlab/metrics/instrumentation.rb:159:in `find_commit'",
|
|
|
|
"app/models/repository.rb:113:in `commit'",
|
|
|
|
"lib/gitlab/i18n.rb:50:in `with_locale'",
|
|
|
|
"lib/gitlab/middleware/multipart.rb:95:in `call'",
|
|
|
|
"lib/gitlab/request_profiler/middleware.rb:14:in `call'",
|
|
|
|
"ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'",
|
|
|
|
"ee/lib/gitlab/jira/middleware.rb:15:in `call'"
|
|
|
|
]
|
|
|
|
|
|
|
|
expect(described_class.clean_backtrace(backtrace))
|
|
|
|
.to eq([
|
|
|
|
"lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:280:in `block in migrate'",
|
|
|
|
"lib/gitlab/gitaly_client.rb:278:in `migrate'",
|
|
|
|
"lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'",
|
|
|
|
"lib/gitlab/git/commit.rb:66:in `find'",
|
|
|
|
"app/models/repository.rb:1047:in `find_commit'",
|
|
|
|
"app/models/repository.rb:113:in `commit'",
|
|
|
|
"ee/lib/gitlab/jira/middleware.rb:15:in `call'"
|
|
|
|
])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-01-18 13:21:42 +00:00
|
|
|
describe '.with_custom_logger' do
|
|
|
|
context 'when the logger is set' do
|
|
|
|
it 'uses the replacement logger for the duration of the block' do
|
|
|
|
expect(null_logger).to receive(:debug).and_call_original
|
|
|
|
|
|
|
|
expect { described_class.with_custom_logger(null_logger) { ActiveRecord::Base.logger.debug('foo') } }
|
|
|
|
.to not_change { ActiveRecord::Base.logger }
|
|
|
|
.and not_change { ActionController::Base.logger }
|
|
|
|
.and not_change { ActiveSupport::LogSubscriber.colorize_logging }
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns the result of the block' do
|
|
|
|
expect(described_class.with_custom_logger(null_logger) { 2 }).to eq(2)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the logger is nil' do
|
|
|
|
it 'returns the result of the block' do
|
|
|
|
expect(described_class.with_custom_logger(nil) { 2 }).to eq(2)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not modify the standard Rails loggers' do
|
|
|
|
expect { described_class.with_custom_logger(nil) { } }
|
|
|
|
.to not_change { ActiveRecord::Base.logger }
|
|
|
|
.and not_change { ActionController::Base.logger }
|
|
|
|
.and not_change { ActiveSupport::LogSubscriber.colorize_logging }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-03-21 16:22:56 +00:00
|
|
|
|
|
|
|
describe '.log_load_times_by_model' do
|
|
|
|
it 'logs the model, query count, and time by slowest first' do
|
|
|
|
expect(null_logger).to receive(:load_times_by_model).and_return(
|
|
|
|
'User' => [1.2, 1.3],
|
|
|
|
'Project' => [10.4]
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(null_logger).to receive(:info).with('Project total (1): 10.4ms')
|
|
|
|
expect(null_logger).to receive(:info).with('User total (2): 2.5ms')
|
|
|
|
|
|
|
|
described_class.log_load_times_by_model(null_logger)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does nothing when called with a logger that does not have load times' do
|
|
|
|
expect(null_logger).not_to receive(:info)
|
|
|
|
|
|
|
|
expect(described_class.log_load_times_by_model(null_logger)).to be_nil
|
|
|
|
end
|
|
|
|
end
|
2018-01-18 13:21:42 +00:00
|
|
|
end
|