File Coverage

lib/DBI/Log.pm
Criterion Covered Total %
statement 146 180 81.1
branch 42 68 61.7
condition 7 17 41.1
subroutine 14 18 77.7
pod 0 3 0.0
total 209 286 73.0


line stmt bran cond sub pod time code
1             package DBI::Log;
2              
3 1     1   163483 use 5.006;
  1         4  
4 1     1   6 no strict;
  1         1  
  1         69  
5 1     1   8 no warnings;
  1         1  
  1         55  
6 1     1   7 use DBI;
  1         4  
  1         47  
7 1     1   663 use IO::Handle;
  1         9235  
  1         69  
8 1     1   8 use Time::HiRes;
  1         2  
  1         10  
9              
10             our $VERSION = "0.12";
11             our %opts = (
12             file => $file,
13             trace => 0,
14             timing => 0,
15             replace_placeholders => 1,
16             fh => undef,
17             exclude => undef,
18             format => "sql",
19             );
20              
21             my $orig_execute = \&DBI::st::execute;
22             *DBI::st::execute = sub {
23 4     4   287712 my ($sth, @args) = @_;
24 4         43 my $log = pre_query("execute", $sth->{Database}, $sth, $sth->{Statement}, \@args);
25 4         39183 my $retval = $orig_execute->($sth, @args);
26 4         28 post_query($log);
27 4         29 return $retval;
28             };
29              
30             my $orig_selectall_arrayref = \&DBI::db::selectall_arrayref;
31             *DBI::db::selectall_arrayref = sub {
32 1     1   4763 my ($dbh, $query, $yup, @args) = @_;
33 1         13 my $log = pre_query("selectall_arrayref", $dbh, undef, $query, \@args);
34 1         31 my $retval = $orig_selectall_arrayref->($dbh, $query, $yup, @args);
35 1         125 post_query($log);
36 1         6 return $retval;
37             };
38              
39             my $orig_selectcol_arrayref = \&DBI::db::selectcol_arrayref;
40             *DBI::db::selectcol_arrayref = sub {
41 1     1   6329 my ($dbh, $query, $yup, @args) = @_;
42 1         14 my $log = pre_query("selectcol_arrayref", $dbh, undef, $query, \@args);
43 1         45 my $retval = $orig_selectcol_arrayref->($dbh, $query, $yup, @args);
44 1         62 post_query($log);
45 1         5 return $retval;
46             };
47              
48             my $orig_selectall_hashref = \&DBI::db::selectall_hashref;
49             *DBI::db::selectall_hashref = sub {
50 0     0   0 my ($dbh, $query, $yup, @args) = @_;
51 0         0 my $log = pre_query("selectall_hashref", $dbh, undef, $query, \@args);
52 0         0 my $retval = $orig_selectall_hashref->($dbh, $query, $yup, @args);
53 0         0 post_query($log);
54 0         0 return $retval;
55             };
56              
57             my $orig_selectrow_arrayref = \&DBI::db::selectrow_arrayref;
58             *DBI::db::selectrow_arrayref = sub {
59 0     0   0 my ($dbh, $query, $yup, @args) = @_;
60 0         0 my $log = pre_query("selectrow_arrayref", $dbh, $sth, $query, \@args);
61 0         0 my $retval = $orig_selectrow_arrayref->($dbh, $query, $yup, @args);
62 0         0 post_query($log);
63 0         0 return $retval;
64             };
65              
66             my $orig_selectrow_array = \&DBI::db::selectrow_array;
67             *DBI::db::selectrow_array = sub {
68 0     0   0 my ($dbh, $query, $yup, @args) = @_;
69 0         0 my $log = pre_query("selectrow_array", $dbh, undef, $query, \@args);
70 0         0 my $retval = $orig_selectrow_array->($dbh, $query, $yup, @args);
71 0         0 post_query($log);
72 0         0 return $retval;
73             };
74              
75             my $orig_selectrow_hashref = \&DBI::db::selectrow_hashref;
76             *DBI::db::selectrow_hashref = sub {
77 0     0   0 my ($dbh, $query, $yup, @args) = @_;
78 0         0 my $log = pre_query("selectrow_hashref", $dbh, undef, $query, \@args);
79 0         0 my $retval = $orig_selectrow_hashref->($dbh, $query, $yup, @args);
80 0         0 post_query($log);
81 0         0 return $retval;
82             };
83              
84             my $orig_do = \&DBI::db::do;
85             *DBI::db::do = sub {
86 3     3   13622 my ($dbh, $query, $yup, @args) = @_;
87 3         37 my $log = pre_query("do", $dbh, undef, $query, \@args);
88 3         73 my $retval = $orig_do->($dbh, $query, $yup, @args);
89 2         15048 post_query($log);
90 2         20 return $retval;
91             };
92              
93              
94             sub import {
95 2     2   4342 my ($package, %args) = @_;
96 2         11 for my $key (keys %args) {
97 3         32 $opts{$key} = $args{$key};
98             }
99 2 50       10 if (!$opts{file}) {
100 0         0 $opts{fh} = \*STDERR;
101             }
102             else {
103 2         10 my $file2 = $opts{file};
104 2 50       16 if ($file2 =~ m{^~/}) {
105 0   0     0 my $home = $ENV{HOME} || (getpwuid($<))[7];
106 0         0 $file2 =~ s{^~/}{$home/};
107             }
108 2 50       393 open $opts{fh}, ">>", $file2 or die "Can't open $opts{file}: $!\n";
109             # autoflush so that tailing to watch queries being performed works
110             # as you'd expect
111 2         34 $opts{fh}->autoflush(1);
112             }
113             }
114              
115             sub pre_query {
116 9     9 0 87 my ($name, $dbh, $sth, $query, $args) = @_;
117 9         43 my $log = {};
118 9         13 my $mcount = 0;
119              
120             # Some DBI functions are composed of other DBI functions, so make sure we
121             # are only logging the top level one. For example $dbh->do() will call
122             # $dbh->execute() internally, so we need to make sure a DBI::Log function
123             # logs the $dbh->do() and not the internal $dbh->execute(). If multiple
124             # functions were called, we return and flag this log entry to be skipped in
125             # the post_query() part.
126 9         86 for (my $i = 0; my @caller = caller($i); $i++) {
127 22         38 my ($package, $file, $line, $sub) = @caller;
128 22 100       59 if ($package eq "DBI::Log") {
129 12         12 $mcount++;
130 12 100       80 if ($mcount > 1) {
131 3         15 $log->{skip} = 1;
132 3         14 return $log;
133             }
134             }
135             }
136 6         7 my @callers;
137 6         39 for (my $i = 0; my @caller = caller($i); $i++) {
138 13         52 push @callers, \@caller;
139             }
140              
141             # Order the call stack based on the highest level calls first, then the
142             # lower level calls. Once you reach a package that is excluded, do not show
143             # any more lines in the stack trace. By default, it will exclude anything
144             # past the DBI::Log package, but if user provides an exclude option, it will
145             # stop there.
146 6         9 my @filtered_callers;
147 6         11 CALLER: for my $caller (reverse @callers) {
148 13         21 my ($package, $file, $line, $long_sub) = @$caller;
149 13 100       22 if ($package eq "DBI::Log") {
150 6         15 last CALLER;
151             }
152 7 50       17 if ($opts{exclude}) {
153 0         0 for my $item (@{$opts{exclude}}) {
  0         0  
154 0 0       0 if ($package =~ /^$item(::|$)/) {
155 0         0 last CALLER;
156             }
157             }
158             }
159 7         14 push @filtered_callers, $caller;
160              
161             }
162 6 50       12 if (!$opts{trace}) {
163 6         13 @filtered_callers = ($filtered_callers[-1]);
164             }
165              
166 6         6 my @stack;
167 6         9 for my $caller (@filtered_callers) {
168 6         12 my ($package, $file, $line, $long_sub) = @$caller;
169 6         7 my $sub = $long_sub;
170 6         63 $sub =~ s/.*:://;
171 6 50       56 $sub = $name if $long_sub =~ /^DBI::Log::__ANON__/;
172 6         65 push @stack, {
173             sub => $sub,
174             file => $file,
175             line => $line,
176             };
177             }
178              
179 6 100 66     23 if (ref($query) && ref($query) eq "DBI::st") {
180 1         4 $sth = $query;
181 1         13 $query = $query->{Statement};
182             }
183              
184 6 50 33     49 if ($dbh && $opts{replace_placeholders}) {
185             # When you use $sth->bind_param(1, "value") the params can be found in
186             # $sth->{ParamValues} and they override arguments sent in to
187             # $sth->execute()
188              
189 6         12 my @args_copy = @$args;
190 6         7 my %values;
191 6 50 66     38 if ($sth && $sth->{ParamValues}) {
192 2         3 %values = %{$sth->{ParamValues}};
  2         12  
193             }
194 6         32 for my $key (keys %values) {
195 0 0 0     0 if (defined $key && $key =~ /^\d+$/) {
196 0         0 $args_copy[$key - 1] = $values{$key};
197             }
198             }
199              
200 6         20 for my $i (0 .. @args_copy - 1) {
201 4         14 my $value = $args_copy[$i];
202 4         55 $value = $dbh->quote($value);
203 4         66 $query =~ s{\?}{$value}e;
  4         16  
204             }
205             }
206              
207 6         64 $query =~ s/^\s*\n|\s*$//g;
208 6         36 $log->{time_started} = Time::HiRes::time();
209 6         34 $log->{query} = $query;
210 6         13 $log->{stack} = \@stack;
211 6 100       20 if ($opts{format} eq "json") {
212             # For JSON output we don't want to output anything yet, so post_query()
213             # can emit the whole JSON object, just remember them.
214             }
215             else {
216 5         7 my $mesg;
217 5         230 $mesg .= "-- " . scalar(localtime()) . "\n";
218 5         12 for my $caller (@stack) {
219 5         32 $mesg .= "-- $caller->{sub} $caller->{file} $caller->{line}\n";
220             }
221 5         7 $mesg .= "$query\n";
222 5         6 print {$opts{fh}} $mesg;
  5         204  
223             }
224              
225 6         36 return $log;
226             }
227              
228             sub post_query {
229 8     8 0 16 my ($log) = @_;
230 8 100       31 return if $log->{skip};
231 5         39 $log->{time_ended} = Time::HiRes::time();
232 5         90 $log->{time_taken} = sprintf "%.3f", $log->{time_ended} - $log->{time_started};
233              
234 5 100       21 if ($opts{format} eq "json") {
235             # print all the info as JSON
236 1         15 print {$opts{fh}} to_json($log) . "\n";
  1         16  
237             }
238             else {
239             # For SQL output format, pre_query already printed most of the info, we
240             # just need to add the time taken - and that only if we're doing
241             # timings...
242 4 50       9 if ($opts{timing}) {
243 0         0 print {$opts{fh}} "-- $log->{time_taken}s\n";
  0         0  
244             }
245 4         7 print {$opts{fh}} "\n";
  4         161  
246             }
247             }
248              
249             sub to_json {
250 10     10 0 26 my ($val, $depth) = @_;
251 10   100     85 $depth ||= 0;
252 10         13 my $pretty = 0;
253              
254 10         14 my $out;
255 10 50       85 if (!defined $val) {
    100          
    100          
    100          
256 0         0 $out = "null";
257             }
258             elsif (ref $val eq "HASH") {
259 2         10 $out = "{";
260 2 50       11 $out .= "\n" if $pretty;
261 2         7 my $i = 0;
262 2         15 for my $key (sort keys %$val) {
263 8         19 my $val2 = $val->{$key};
264 8 100       21 if ($i) {
265 6 50       16 $out .= $pretty ? ",\n" : ", ";
266             }
267 8 50       23 $out .= " " x ($depth + 1) if $pretty;
268 8         80 $out .= "\"$key\": " . to_json($val2, $depth + 1);
269 8         18 $i++;
270             }
271 2 50       7 $out .= "\n" if $pretty;
272 2 50       34 $out .= " " x ($depth) if $pretty;
273 2         5 $out .= "}";
274             }
275             elsif (ref $val eq "ARRAY") {
276 1         2 $out = "[";
277 1 50       6 $out .= "\n" if $pretty;
278 1         6 for my $i (0 .. @$val - 1) {
279 1         6 my $val2 = $val->[$i];
280 1 50       3 if ($i) {
281 0 0       0 $out .= $pretty ? ",\n" : ", ";
282             }
283 1 50       4 $out .= " " x ($depth + 1) if $pretty;
284 1         5 $out .= to_json($val2, $depth + 1);
285             }
286 1 50       6 $out .= "\n" if $pretty;
287 1 50       2 $out .= " " x ($depth) if $pretty;
288 1         5 $out .= "]";
289             }
290             elsif ($val =~ /^(-?\d+(\.\d*)?(e[+-]?\d+)?)$/i) {
291 4         9 $out = $val;
292             }
293             else {
294             # Make the value suitable to use in a JSON string - no newlines, escape
295             # control characters and double quotes.
296 3         7 $val =~ s/"/\\"/g;
297 3         10 $val =~ s/\n/ /g;
298 3         8 $val =~ s/([\x00-\x1F])/sprintf("\\u%04x", ord($1))/eg;
  0         0  
299 3         8 $out = "\"$val\"";
300             }
301              
302 10         115 return $out;
303             }
304              
305             1;
306              
307             __END__