Commit 42472bdd authored by Dylan Griffith's avatar Dylan Griffith Committed by Thong Kuah

Log database duration per database

As part of https://gitlab.com/groups/gitlab-org/-/epics/5759 we're
preparing the GitLab application to handle connections to multiple
databases. As such we want to improve our observability to understand
these multiple databases.

This MR is a first step which will break down the total database
duration of a request by the database name of the database connection.
At present this will only be 1 extra field (`gitlabhq_production` on
production) but in future it will automatically start logging
information about the other databases we add.

We assume that database name should be sufficient for distinguishing
these connections in our logs for the time being. Technically we could
be conneting to multiple database servers with the same database name
but we don't plan on doing that first up and we can come up with a
different way to identify hosts with the same database name later if
that becomes necessary.
parent 6700c65b
---
name: multiple_database_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63490
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333227
milestone: '14.1'
type: development
group: group::sharding
default_enabled: false
...@@ -333,6 +333,16 @@ module Gitlab ...@@ -333,6 +333,16 @@ module Gitlab
end end
end end
def self.dbname(ar_connection)
if ar_connection.respond_to?(:pool) &&
ar_connection.pool.respond_to?(:db_config) &&
ar_connection.pool.db_config.respond_to?(:database)
return ar_connection.pool.db_config.database
end
'unknown'
end
# inside_transaction? will return true if the caller is running within a transaction. Handles special cases # inside_transaction? will return true if the caller is running within a transaction. Handles special cases
# when running inside a test environment, where tests may be wrapped in transactions # when running inside a test environment, where tests may be wrapped in transactions
def self.inside_transaction? def self.inside_transaction?
......
...@@ -72,6 +72,14 @@ module Gitlab ...@@ -72,6 +72,14 @@ module Gitlab
DB_LOAD_BALANCING_DURATIONS.each do |duration| DB_LOAD_BALANCING_DURATIONS.each do |duration|
payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3) payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3)
end end
if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml)
::Gitlab::SafeRequestStore[:duration_by_database]&.each do |dbname, duration_by_role|
duration_by_role.each do |db_role, duration|
payload[:"db_#{db_role}_#{dbname}_duration_s"] = duration.to_f.round(3)
end
end
end
end end
end end
end end
...@@ -93,9 +101,18 @@ module Gitlab ...@@ -93,9 +101,18 @@ module Gitlab
buckets ::Gitlab::Metrics::Subscribers::ActiveRecord::SQL_DURATION_BUCKET buckets ::Gitlab::Metrics::Subscribers::ActiveRecord::SQL_DURATION_BUCKET
end end
return unless ::Gitlab::SafeRequestStore.active?
duration = event.duration / 1000.0 duration = event.duration / 1000.0
duration_key = "db_#{db_role}_duration_s".to_sym duration_key = "db_#{db_role}_duration_s".to_sym
::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration ::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
# Per database metrics
dbname = ::Gitlab::Database.dbname(event.payload[:connection])
::Gitlab::SafeRequestStore[:duration_by_database] ||= {}
::Gitlab::SafeRequestStore[:duration_by_database][dbname] ||= {}
::Gitlab::SafeRequestStore[:duration_by_database][dbname][db_role] ||= 0
::Gitlab::SafeRequestStore[:duration_by_database][dbname][db_role] += duration
end end
def ignored_query?(payload) def ignored_query?(payload)
......
...@@ -414,6 +414,23 @@ RSpec.describe Gitlab::Database do ...@@ -414,6 +414,23 @@ RSpec.describe Gitlab::Database do
end end
end end
describe '.dbname' do
it 'returns the dbname for the connection' do
connection = ActiveRecord::Base.connection
expect(described_class.dbname(connection)).to be_a(String)
expect(described_class.dbname(connection)).to eq(connection.pool.db_config.database)
end
context 'when the pool is a NullPool' do
it 'returns unknown' do
connection = double(:active_record_connection, pool: ActiveRecord::ConnectionAdapters::NullPool.new)
expect(described_class.dbname(connection)).to eq('unknown')
end
end
end
describe '#true_value' do describe '#true_value' do
it 'returns correct value' do it 'returns correct value' do
expect(described_class.true_value).to eq "'t'" expect(described_class.true_value).to eq "'t'"
......
...@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do ...@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:env) { {} } let(:env) { {} }
let(:subscriber) { described_class.new } let(:subscriber) { described_class.new }
let(:connection) { double(:connection) } let(:connection) { ActiveRecord::Base.connection }
describe '#transaction' do describe '#transaction' do
let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') } let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') }
......
...@@ -298,6 +298,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -298,6 +298,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true) allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
end end
let(:dbname) { ::Gitlab::Database.dbname(ActiveRecord::Base.connection) }
let(:expected_end_payload_with_db) do let(:expected_end_payload_with_db) do
expected_end_payload.merge( expected_end_payload.merge(
'db_duration_s' => a_value >= 0.1, 'db_duration_s' => a_value >= 0.1,
...@@ -311,7 +313,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -311,7 +313,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'db_primary_count' => a_value >= 1, 'db_primary_count' => a_value >= 1,
'db_primary_cached_count' => 0, 'db_primary_cached_count' => 0,
'db_primary_wal_count' => 0, 'db_primary_wal_count' => 0,
'db_primary_duration_s' => a_value > 0 'db_primary_duration_s' => a_value > 0,
"db_primary_#{dbname}_duration_s" => a_value > 0
) )
end end
......
...@@ -5,9 +5,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| ...@@ -5,9 +5,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
2.times do 2.times do
Gitlab::WithRequestStore.with_request_store do Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event) subscriber.sql(event)
connection = event.payload[:connection]
if db_role == :primary if db_role == :primary
expect(described_class.db_counter_payload).to eq( expected = {
db_count: record_query ? 1 : 0, db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0,
...@@ -19,9 +20,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| ...@@ -19,9 +20,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_replica_duration_s: 0.0, db_replica_duration_s: 0.0,
db_primary_wal_count: record_wal_query ? 1 : 0, db_primary_wal_count: record_wal_query ? 1 : 0,
db_replica_wal_count: 0 db_replica_wal_count: 0
) }
expected[:"db_primary_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query
elsif db_role == :replica elsif db_role == :replica
expect(described_class.db_counter_payload).to eq( expected = {
db_count: record_query ? 1 : 0, db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0,
...@@ -33,14 +35,32 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| ...@@ -33,14 +35,32 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_replica_duration_s: record_query ? 0.002 : 0, db_replica_duration_s: record_query ? 0.002 : 0,
db_replica_wal_count: record_wal_query ? 1 : 0, db_replica_wal_count: record_wal_query ? 1 : 0,
db_primary_wal_count: 0 db_primary_wal_count: 0
) }
expected[:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query
else else
expect(described_class.db_counter_payload).to eq( expected = {
db_count: record_query ? 1 : 0, db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0 db_cached_count: record_cached_query ? 1 : 0
) }
end end
expect(described_class.db_counter_payload).to eq(expected)
end
end
end
context 'when multiple_database_metrics is disabled' do
before do
stub_feature_flags(multiple_database_metrics: false)
end
it 'does not include per database metrics' do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
connection = event.payload[:connection]
expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s")
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