Commit c9e3678c authored by Stan Hu's avatar Stan Hu

Log Redis call count and duration to log files

It has been difficult to see trends in the number of Redis calls without
having the number and duration of the calls in structured logs. This
commit adds `redis_calls` and `redis_duration_ms` fields to all relevant
logs (e.g. api_json.log, production_json.log, Sidekiq, etc.).

Closes https://gitlab.com/gitlab-org/gitlab/issues/208821
parent c236c0c0
---
title: Log Redis call count and duration to log files
merge_request: 27735
author:
type: other
...@@ -5,30 +5,11 @@ module Gitlab ...@@ -5,30 +5,11 @@ module Gitlab
module GrapeLogging module GrapeLogging
module Loggers module Loggers
class PerfLogger < ::GrapeLogging::Loggers::Base class PerfLogger < ::GrapeLogging::Loggers::Base
def parameters(_, _) include ::Gitlab::InstrumentationHelper
gitaly_data.merge(rugged_data)
end
def gitaly_data
gitaly_calls = Gitlab::GitalyClient.get_request_count
return {} if gitaly_calls.zero? def parameters(_, _)
payload = {}
{ payload.tap { add_instrumentation_data(payload) }
gitaly_calls: Gitlab::GitalyClient.get_request_count,
gitaly_duration: Gitlab::GitalyClient.query_time_ms
}
end
def rugged_data
rugged_calls = Gitlab::RuggedInstrumentation.query_count
return {} if rugged_calls.zero?
{
rugged_calls: rugged_calls,
rugged_duration_ms: Gitlab::RuggedInstrumentation.query_time_ms
}
end end
end end
end end
......
# frozen_string_literal: true
require 'redis'
module Gitlab
module Instrumentation
module RedisInterceptor
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
if ::RequestStore.active?
::Gitlab::Instrumentation::Redis.increment_request_count
::Gitlab::Instrumentation::Redis.add_duration(duration)
::Gitlab::Instrumentation::Redis.add_call_details(duration, args)
end
end
end
class Redis
REDIS_REQUEST_COUNT = :redis_request_count
REDIS_CALL_DURATION = :redis_call_duration
REDIS_CALL_DETAILS = :redis_call_details
def self.get_request_count
::RequestStore[REDIS_REQUEST_COUNT] || 0
end
def self.increment_request_count
::RequestStore[REDIS_REQUEST_COUNT] ||= 0
::RequestStore[REDIS_REQUEST_COUNT] += 1
end
def self.detail_store
::RequestStore[REDIS_CALL_DETAILS] ||= []
end
def self.query_time_ms
(self.query_time * 1000).round(2)
end
def self.query_time
::RequestStore[REDIS_CALL_DURATION] || 0
end
def self.add_duration(duration)
total_time = query_time + duration
::RequestStore[REDIS_CALL_DURATION] = total_time
end
def self.add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
end
end
end
class ::Redis::Client
prepend ::Gitlab::Instrumentation::RedisInterceptor
end
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module InstrumentationHelper module InstrumentationHelper
extend self extend self
KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms).freeze KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms redis_calls redis_duration_ms).freeze
def add_instrumentation_data(payload) def add_instrumentation_data(payload)
gitaly_calls = Gitlab::GitalyClient.get_request_count gitaly_calls = Gitlab::GitalyClient.get_request_count
...@@ -20,6 +20,13 @@ module Gitlab ...@@ -20,6 +20,13 @@ module Gitlab
payload[:rugged_calls] = rugged_calls payload[:rugged_calls] = rugged_calls
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
end end
redis_calls = Gitlab::Instrumentation::Redis.get_request_count
if redis_calls > 0
payload[:redis_calls] = redis_calls
payload[:redis_duration_ms] = Gitlab::Instrumentation::Redis.query_time_ms
end
end end
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
......
...@@ -17,7 +17,7 @@ module Peek ...@@ -17,7 +17,7 @@ module Peek
end end
def detail_store def detail_store
::Gitlab::SafeRequestStore["#{key}_call_details"] ||= [] ::Gitlab::SafeRequestStore["#{key}_call_details".to_sym] ||= []
end end
private private
......
# frozen_string_literal: true # frozen_string_literal: true
require 'redis'
module Gitlab
module Peek
module RedisInstrumented
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
add_call_details(duration, args)
end
private
def add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
def detail_store
::Gitlab::SafeRequestStore['redis_call_details'] ||= []
end
end
end
end
module Peek module Peek
module Views module Views
class RedisDetailed < DetailedView class RedisDetailed < DetailedView
...@@ -63,7 +29,3 @@ module Peek ...@@ -63,7 +29,3 @@ module Peek
end end
end end
end end
class Redis::Client
prepend Gitlab::Peek::RedisInstrumented
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GrapeLogging::Loggers::PerfLogger do
subject { described_class.new }
describe ".parameters" do
let(:mock_request) { OpenStruct.new(env: {}) }
describe 'when no performance datais are present' do
it 'returns an empty Hash' do
expect(subject.parameters(mock_request, nil)).to eq({})
end
end
describe 'when Redis calls are present', :request_store do
it 'returns a Hash with Redis information' do
Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') }
payload = subject.parameters(mock_request, nil)
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
require 'rspec-parameterized' require 'rspec-parameterized'
describe Gitlab::InstrumentationHelper do describe Gitlab::InstrumentationHelper do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
describe '.add_instrumentation_data', :request_store do
let(:payload) { {} }
subject { described_class.add_instrumentation_data(payload) }
it 'adds nothing' do
subject
expect(payload).to eq({})
end
context 'when Gitaly calls are made' do
it 'adds Gitaly data and omits Redis data' do
project = create(:project)
RequestStore.clear!
project.repository.exists?
subject
expect(payload[:gitaly_calls]).to eq(1)
expect(payload[:gitaly_duration]).to be >= 0
expect(payload[:redis_calls]).to be_nil
expect(payload[:redis_duration_ms]).to be_nil
end
end
context 'when Redis calls are made' do
it 'adds Redis data and omits Gitaly data' do
Gitlab::Redis::Cache.with { |redis| redis.get('test-instrumentation') }
subject
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
expect(payload[:gitaly_calls]).to be_nil
expect(payload[:gitaly_duration]).to be_nil
end
end
end
describe '.queue_duration_for_job' do describe '.queue_duration_for_job' do
where(:enqueued_at, :created_at, :time_now, :expected_duration) do where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f "2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
......
...@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end end
end end
context 'with Gitaly and Rugged calls' do context 'with Gitaly, Rugged, and Redis calls' do
let(:timing_data) do let(:timing_data) do
{ {
gitaly_calls: 10, gitaly_calls: 10,
gitaly_duration: 10000, gitaly_duration: 10000,
rugged_calls: 1, rugged_calls: 1,
rugged_duration_ms: 5000 rugged_duration_ms: 5000,
redis_calls: 3,
redis_duration_ms: 1234
} }
end end
before do let(:expected_end_payload) do
job.merge!(timing_data) end_payload.except('args').merge(timing_data)
end end
it 'logs with Gitaly and Rugged timing data' do it 'logs with Gitaly and Rugged timing data' do
Timecop.freeze(timestamp) do Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload.except('args')).ordered expect(logger).to receive(:info).with(start_payload.except('args')).ordered
expect(logger).to receive(:info).with(end_payload.except('args')).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered
subject.call(job, 'test_queue') { } subject.call(job, 'test_queue') do
job.merge!(timing_data)
end
end end
end end
end end
......
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