Skip to content

Commit 7253551

Browse files
Brian J. Cardiffasterite
andauthored
Add logging for executing queries (#134)
* Add logging for executing queries Arguments are translated to Log::Metadata::Value via DB::Statement#arg_to_log method. DB::Statement#before_query_or_exec & after_query_or_exec protected methods can be used to hook and run around the statement execution * Move the metadata converter to a module * Replace before/after with def_around_query_or_exec macro * Update src/db/enumerable_concat.cr Co-authored-by: Ary Borenszweig <asterite@gmail.com> Co-authored-by: Ary Borenszweig <asterite@gmail.com>
1 parent fad9e70 commit 7253551

File tree

6 files changed

+147
-16
lines changed

6 files changed

+147
-16
lines changed

spec/custom_drivers_types_spec.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ class FooDriver < DB::Driver
4848
end
4949

5050
class FooConnection < DB::Connection
51-
def build_prepared_statement(query) : DB::Statement
52-
FooStatement.new(self)
51+
def build_prepared_statement(command) : DB::Statement
52+
FooStatement.new(self, command)
5353
end
5454

55-
def build_unprepared_statement(query) : DB::Statement
55+
def build_unprepared_statement(command) : DB::Statement
5656
raise "not implemented"
5757
end
5858
end
@@ -112,7 +112,7 @@ class BarDriver < DB::Driver
112112

113113
class BarConnection < DB::Connection
114114
def build_prepared_statement(query) : DB::Statement
115-
BarStatement.new(self)
115+
BarStatement.new(self, query)
116116
end
117117

118118
def build_unprepared_statement(query) : DB::Statement

spec/dummy_driver.cr

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,22 @@ class DummyDriver < DB::Driver
9696
class DummyStatement < DB::Statement
9797
property params
9898

99-
def initialize(connection, @query : String, @prepared : Bool)
99+
def initialize(connection, command : String, @prepared : Bool)
100100
@params = Hash(Int32 | String, DB::Any | Array(DB::Any)).new
101-
super(connection)
102-
raise DB::Error.new(query) if query == "syntax error"
101+
super(connection, command)
102+
raise DB::Error.new(command) if command == "syntax error"
103103
end
104104

105105
protected def perform_query(args : Enumerable) : DB::ResultSet
106106
@connection.as(DummyConnection).check
107107
set_params args
108-
DummyResultSet.new self, @query
108+
DummyResultSet.new self, command
109109
end
110110

111111
protected def perform_exec(args : Enumerable) : DB::ExecResult
112112
@connection.as(DummyConnection).check
113113
set_params args
114-
raise DB::Error.new("forced exception due to query") if @query == "raise"
114+
raise DB::Error.new("forced exception due to query") if command == "raise"
115115
DB::ExecResult.new 0i64, 0_i64
116116
end
117117

@@ -149,9 +149,9 @@ class DummyDriver < DB::Driver
149149

150150
@@last_result_set : self?
151151

152-
def initialize(statement, query)
152+
def initialize(statement, command)
153153
super(statement)
154-
@top_values = query.split.map { |r| r.split(',') }.to_a
154+
@top_values = command.split.map { |r| r.split(',') }.to_a
155155
@column_count = @top_values.size > 0 ? @top_values[0].size : 2
156156

157157
@@last_result_set = self

spec/statement_spec.cr

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "./spec_helper"
2+
require "log/spec"
23

34
describe DB::Statement do
45
it "should build prepared statements" do
@@ -217,4 +218,40 @@ describe DB::Statement do
217218
end
218219
end
219220
end
221+
222+
describe "logging" do
223+
it "exec with no arguments" do
224+
Log.capture(DB::Log.source) do |logs|
225+
with_dummy do |db|
226+
db.exec "42"
227+
end
228+
229+
entry = logs.check(:debug, /Executing query/i).entry
230+
entry.data[:query].should eq("42")
231+
entry.data[:args].as_a.should be_empty
232+
end
233+
end
234+
235+
it "query with arguments" do
236+
Log.capture(DB::Log.source) do |logs|
237+
with_dummy do |db|
238+
db.exec "1, ?", args: ["a"]
239+
db.exec "2, ?", "a"
240+
db.exec "3, ?", ["a"]
241+
end
242+
243+
entry = logs.check(:debug, /Executing query/i).entry
244+
entry.data[:query].should eq("1, ?")
245+
entry.data[:args][0].as_s.should eq("a")
246+
247+
entry = logs.check(:debug, /Executing query/i).entry
248+
entry.data[:query].should eq("2, ?")
249+
entry.data[:args][0].as_s.should eq("a")
250+
251+
entry = logs.check(:debug, /Executing query/i).entry
252+
entry.data[:query].should eq("3, ?")
253+
entry.data[:args][0][0].as_s.should eq("a")
254+
end
255+
end
256+
end
220257
end

src/db.cr

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "uri"
2+
require "log"
23

34
# The DB module is a unified interface for database access.
45
# Individual database systems are supported by specific database driver shards.
@@ -75,6 +76,8 @@ require "uri"
7576
# ```
7677
#
7778
module DB
79+
Log = ::Log.for(self)
80+
7881
# Types supported to interface with database driver.
7982
# These can be used in any `ResultSet#read` or any `Database#query` related
8083
# method to be used as query parameters
@@ -134,7 +137,7 @@ module DB
134137
build_connection(uri)
135138
end
136139

137-
# ditto
140+
# :ditto:
138141
def self.connect(uri : URI | String, &block)
139142
cnn = build_connection(uri)
140143
begin

src/db/enumerable_concat.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module DB
2020
end
2121

2222
# returns given `e1 : T` an `Enumerable(T')` and `e2 : U` an `Enumerable(U') | Nil`
23-
# it retuns and `Enumerable(T' | U')` that enumerates the elements of `e1`
23+
# it returns an `Enumerable(T' | U')` that enumerates the elements of `e1`
2424
# and, later, the elements of `e2`.
2525
def self.build(e1 : T, e2 : U)
2626
return e1 if e2.nil? || e2.empty?

src/db/statement.cr

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ module DB
5151
# :nodoc:
5252
getter connection
5353

54-
def initialize(@connection : Connection)
54+
getter command : String
55+
56+
def initialize(@connection : Connection, @command : String)
5557
end
5658

5759
def release_connection
@@ -79,13 +81,17 @@ module DB
7981
end
8082

8183
private def perform_exec_and_release(args : Enumerable) : ExecResult
82-
return perform_exec(args)
84+
around_query_or_exec(args) do
85+
perform_exec(args)
86+
end
8387
ensure
8488
release_connection
8589
end
8690

8791
private def perform_query_with_rescue(args : Enumerable) : ResultSet
88-
return perform_query(args)
92+
around_query_or_exec(args) do
93+
perform_query(args)
94+
end
8995
rescue e : Exception
9096
# Release connection only when an exception occurs during the query
9197
# execution since we need the connection open while the ResultSet is open
@@ -95,5 +101,90 @@ module DB
95101

96102
protected abstract def perform_query(args : Enumerable) : ResultSet
97103
protected abstract def perform_exec(args : Enumerable) : ExecResult
104+
105+
# This method is called when executing the statement. Although it can be
106+
# redefined, it is recommended to use the `def_around_query_or_exec` macro
107+
# to be able to add new behaviors without loosing prior existing ones.
108+
protected def around_query_or_exec(args : Enumerable)
109+
yield
110+
end
111+
112+
# This macro allows injecting code to be run before and after the execution
113+
# of the request. It should return the yielded value. It must be called with 1
114+
# block argument that will be used to pass the `args : Enumerable`.
115+
#
116+
# ```
117+
# class DB::Statement
118+
# def_around_query_or_exec do |args|
119+
# # do something before query or exec
120+
# res = yield
121+
# # do something after query or exec
122+
# res
123+
# end
124+
# end
125+
# ```
126+
macro def_around_query_or_exec(&block)
127+
protected def around_query_or_exec(%args : Enumerable)
128+
previous_def do
129+
{% if block.args.size != 1 %}
130+
{% raise "Wrong number of block arguments (given #{block.args.size}, expected: 1)" %}
131+
{% end %}
132+
133+
{{ block.args.first.id }} = %args
134+
{{ block.body }}
135+
end
136+
end
137+
end
138+
139+
def_around_query_or_exec do |args|
140+
emit_log(args)
141+
yield
142+
end
143+
144+
protected def emit_log(args : Enumerable)
145+
Log.debug &.emit("Executing query", query: command, args: MetadataValueConverter.arg_to_log(args))
146+
end
147+
end
148+
149+
# This module converts DB supported values to `::Log::Metadata::Value`
150+
#
151+
# ### Note to implementors
152+
#
153+
# If the driver defines custom types to be used as arguments the default behavior
154+
# will be converting the value via `#to_s`. Otherwise you can define overloads to
155+
# change this behaviour.
156+
#
157+
# ```
158+
# module DB::MetadataValueConverter
159+
# def self.arg_to_log(arg : PG::Geo::Point)
160+
# ::Log::Metadata::Value.new("(#{arg.x}, #{arg.y})::point")
161+
# end
162+
# end
163+
# ```
164+
module MetadataValueConverter
165+
# Returns *arg* encoded as a `::Log::Metadata::Value`.
166+
def self.arg_to_log(arg) : ::Log::Metadata::Value
167+
::Log::Metadata::Value.new(arg.to_s)
168+
end
169+
170+
# :ditto:
171+
def self.arg_to_log(arg : Enumerable) : ::Log::Metadata::Value
172+
::Log::Metadata::Value.new(arg.to_a.map { |a| arg_to_log(a).as(::Log::Metadata::Value) })
173+
end
174+
175+
# :ditto:
176+
def self.arg_to_log(arg : Int) : ::Log::Metadata::Value
177+
::Log::Metadata::Value.new(arg.to_i64)
178+
end
179+
180+
# :ditto:
181+
def self.arg_to_log(arg : UInt64) : ::Log::Metadata::Value
182+
::Log::Metadata::Value.new(arg.to_s)
183+
end
184+
185+
# :ditto:
186+
def self.arg_to_log(arg : Nil | Bool | Int32 | Int64 | Float32 | Float64 | String | Time) : ::Log::Metadata::Value
187+
::Log::Metadata::Value.new(arg)
188+
end
98189
end
99190
end

0 commit comments

Comments
 (0)