Skip to content

Commit 4dcb0b9

Browse files
Log DB query request metrics (#4442)
* Log DB query request metrics - number of queries per request - accumulated query time * Fix request metrics logging if log_db_queries is false * if log_db_queries is falses, the two new fields would be logged but always with "0" as value --------- Co-authored-by: Jochen Ehret <[email protected]>
1 parent 76a120b commit 4dcb0b9

File tree

10 files changed

+119
-16
lines changed

10 files changed

+119
-16
lines changed

lib/cloud_controller/db.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'cloud_controller/db_connection/options_factory'
44
require 'cloud_controller/db_connection/finalizer'
55
require 'sequel/extensions/query_length_logging'
6+
require 'sequel/extensions/request_query_metrics'
67

78
module VCAP::CloudController
89
class DB
@@ -30,12 +31,13 @@ def self.connect(opts, logger)
3031
db.logger = logger
3132
db.sql_log_level = opts[:log_level]
3233
db.extension :caller_logging
33-
db.caller_logging_ignore = /sequel_paginator|query_length_logging|sequel_pg|paginated_collection_renderer/
34+
db.caller_logging_ignore = /sequel_paginator|query_length_logging|sequel_pg|paginated_collection_renderer|request_query_metrics/
3435
db.caller_logging_formatter = lambda do |caller|
3536
"(#{caller.sub(%r{^.*/cloud_controller_ng/}, '')})"
3637
end
3738

3839
db.extension(:query_length_logging)
40+
db.extension(:request_query_metrics)
3941
db.opts[:query_size_log_threshold] = opts[:query_size_log_threshold]
4042
end
4143
db.default_collate = 'utf8_bin' if db.database_type == :mysql

lib/cloud_controller/logs/request_logs.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,21 @@ def start_request(request_id, env)
2626
@incomplete_requests.store(request_id, env)
2727
end
2828

29-
def complete_request(request_id, status, env, time_taken)
29+
def complete_request(request_id, status, env, time_taken_ms, total_db_query_time_us=nil, db_query_count=nil)
3030
return if @incomplete_requests.delete(request_id).nil?
3131

3232
request = ActionDispatch::Request.new(env)
33-
@logger.info("Completed #{status} vcap-request-id: #{request_id}",
34-
{ status_code: status,
35-
time_taken_in_ms: time_taken,
36-
request_method: request.request_method,
37-
request_fullpath: request.filtered_path })
33+
34+
log_fields = {
35+
status_code: status,
36+
time_taken_in_ms: time_taken_ms,
37+
request_method: request.request_method,
38+
request_fullpath: request.filtered_path
39+
}
40+
log_fields[:total_db_query_time_in_ms] = (total_db_query_time_us.to_f / 1_000) unless total_db_query_time_us.nil?
41+
log_fields[:db_query_count] = db_query_count unless db_query_count.nil?
42+
43+
@logger.info("Completed #{status} vcap-request-id: #{request_id}", log_fields)
3844
end
3945

4046
def log_incomplete_requests
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
Sequel::Database.register_extension(:request_query_metrics) do |db|
4+
mod = Module.new do
5+
private
6+
7+
def log_duration(duration, message)
8+
VCAP::Request.record_db_query((duration * 1_000_000).round)
9+
10+
super
11+
end
12+
end
13+
14+
db.singleton_class.prepend(mod)
15+
end

lib/vcap/request.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@ def b3_span_id=(span_id)
5555
def b3_span_id
5656
Thread.current[:b3_span_id]
5757
end
58+
59+
def db_query_metrics
60+
init_db_query_metrics
61+
62+
Thread.current[:db_query_metrics]
63+
end
64+
65+
def record_db_query(duration)
66+
init_db_query_metrics
67+
68+
Thread.current[:db_query_metrics].total_query_time_us += duration
69+
Thread.current[:db_query_metrics].query_count += 1
70+
end
71+
72+
def reset_db_query_metrics
73+
Thread.current[:db_query_metrics] = Struct.new(:total_query_time_us, :query_count).new(0, 0)
74+
end
75+
76+
private
77+
78+
def init_db_query_metrics
79+
reset_db_query_metrics if Thread.current[:db_query_metrics].nil?
80+
end
5881
end
5982
end
6083
end

middleware/request_logs.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,26 @@ def initialize(app, request_logs)
99
def call(env)
1010
request_id = env['cf.request_id']
1111
@request_logs.start_request(request_id, env)
12-
start_time = Time.now
12+
start_timer = timer
1313

1414
status, headers, body = @app.call(env)
15-
# convert to milliseconds
16-
time_taken = (Time.now - start_time) * 1000
17-
time_taken = time_taken.to_i
18-
@request_logs.complete_request(request_id, status, env, time_taken)
15+
time_taken_ms = timer - start_timer
16+
17+
args = [request_id, status, env, time_taken_ms]
18+
if VCAP::CloudController::Config.config.get(:db, :log_db_queries)
19+
db_query_metrics = ::VCAP::Request.db_query_metrics
20+
args += [db_query_metrics.total_query_time_us, db_query_metrics.query_count]
21+
end
22+
@request_logs.complete_request(*args)
1923

2024
[status, headers, body]
2125
end
26+
27+
private
28+
29+
def timer
30+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
31+
end
2232
end
2333
end
2434
end

middleware/request_metrics.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ def initialize(app, request_metrics)
77
end
88

99
def call(env)
10+
::VCAP::Request.reset_db_query_metrics
1011
@request_metrics.start_request
1112

1213
status, headers, body = @app.call(env)

spec/unit/lib/cloud_controller/logs/request_logs_spec.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ module VCAP::CloudController::Logs
99
let(:fake_fullpath) { 'fullpath' }
1010
let(:fake_request) { double('request', request_method: 'request_method', ip: fake_ip, filtered_path: 'filtered_path', fullpath: fake_fullpath) }
1111
let(:request_id) { 'ID' }
12-
let(:time_taken) { 30 }
12+
let(:time_taken_ms) { 30 }
13+
let(:total_db_query_time_us) { 123_456 }
14+
let(:db_query_count) { 5 }
1315
let(:env) { { 'cf.user_guid' => 'user-guid' } }
1416
let(:status) { 200 }
1517

@@ -46,10 +48,12 @@ module VCAP::CloudController::Logs
4648
end
4749

4850
it 'logs the completion of the request' do
49-
request_logs.complete_request(request_id, status, env, time_taken)
51+
request_logs.complete_request(request_id, status, env, time_taken_ms, total_db_query_time_us, db_query_count)
5052
expect(logger).to have_received(:info).with(
5153
/\ACompleted 200 vcap-request-id: ID/,
5254
time_taken_in_ms: 30,
55+
total_db_query_time_in_ms: 123.456,
56+
db_query_count: 5,
5357
request_method: 'request_method',
5458
request_fullpath: 'filtered_path',
5559
status_code: 200
@@ -59,7 +63,7 @@ module VCAP::CloudController::Logs
5963

6064
context 'without a matching start request' do
6165
it 'does not log the completion of the request' do
62-
request_logs.complete_request(request_id, status, env, time_taken)
66+
request_logs.complete_request(request_id, status, env, time_taken_ms, total_db_query_time_us, db_query_count)
6367
expect(logger).not_to have_received(:info)
6468
end
6569
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe 'request_query_metrics' do
4+
let(:db) { DbConfig.new.connection }
5+
6+
before do
7+
# Add a logger to the database connection to ensure that the duration is captured
8+
db.loggers << Logger.new(StringIO.new)
9+
db.extension(:request_query_metrics)
10+
allow(VCAP::Request).to receive(:record_db_query).and_wrap_original do |original_method|
11+
original_method.call(1_234)
12+
end
13+
end
14+
15+
it 'increments the thread local total query time on each query' do
16+
expect do
17+
2.times { db.fetch('SELECT 1').all }
18+
end.to change(VCAP::Request.db_query_metrics, :total_query_time_us).by(2_468)
19+
end
20+
21+
it 'increments the thread local query count on each query' do
22+
expect do
23+
db.fetch('SELECT 1').all
24+
end.to change(VCAP::Request.db_query_metrics, :query_count).by(1)
25+
end
26+
end

spec/unit/middleware/request_logs_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ module Middleware
1010
let(:env) { { 'cf.request_id' => 'ID' } }
1111

1212
describe 'handling the request' do
13+
before do
14+
VCAP::CloudController::Config.config.get(:db)[:log_db_queries] = true
15+
end
16+
1317
it 'calls start request on request logs before the request' do
1418
middleware.call(env)
1519
expect(request_logs).to have_received(:start_request).with('ID', env)
@@ -20,6 +24,17 @@ module Middleware
2024
end
2125

2226
it 'calls complete request on request logs after the request' do
27+
middleware.call(env)
28+
expect(request_logs).to have_received(:complete_request).with('ID', 200, { 'cf.request_id' => 'ID' }, be_a(Numeric), be_a(Numeric), be_a(Numeric))
29+
end
30+
end
31+
32+
describe 'when db query logging is disabled' do
33+
before do
34+
VCAP::CloudController::Config.config.get(:db)[:log_db_queries] = false
35+
end
36+
37+
it 'calls complete request on request logs without db query metrics' do
2338
middleware.call(env)
2439
expect(request_logs).to have_received(:complete_request).with('ID', 200, { 'cf.request_id' => 'ID' }, be_a(Numeric))
2540
end

spec/unit/middleware/request_metrics_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ module Middleware
99
let(:request_metrics) { instance_double(VCAP::CloudController::Metrics::RequestMetrics, start_request: nil, complete_request: nil) }
1010

1111
describe 'handling the request' do
12-
it 'calls start request on request metrics before the request' do
12+
it 'resets the db query count and calls start request on request metrics before the request' do
13+
expect(VCAP::Request).to receive(:reset_db_query_metrics)
1314
middleware.call({})
1415
expect(request_metrics).to have_received(:start_request)
1516
end

0 commit comments

Comments
 (0)