File Coverage

blib/lib/DBIx/OpenTracing.pm
Criterion Covered Total %
statement 223 225 99.1
branch 55 60 91.6
condition 15 18 83.3
subroutine 54 55 98.1
pod 10 10 100.0
total 357 368 97.0


line stmt bran cond sub pod time code
1             package DBIx::OpenTracing;
2 7     7   2318128 use strict;
  7         66  
  7         246  
3 7     7   39 use warnings;
  7         12  
  7         248  
4 7     7   38 use feature qw[ state ];
  7         14  
  7         1010  
5 7     7   1930 use syntax 'maybe';
  7         111020  
  7         55  
6 7     7   15794 use B;
  7         27  
  7         383  
7 7     7   45 use Carp qw[ croak ];
  7         21  
  7         294  
8 7     7   6962 use DBI;
  7         57405  
  7         328  
9 7     7   3133 use DBIx::OpenTracing::Constants ':ALL';
  7         18  
  7         1120  
10 7     7   86 use List::Util qw[ sum0 ];
  7         16  
  7         700  
11 7     7   1885 use OpenTracing::GlobalTracer;
  7         45983  
  7         47  
12 7     7   472 use Package::Constants;
  7         14  
  7         171  
13 7     7   35 use Scalar::Util qw[ blessed looks_like_number ];
  7         12  
  7         401  
14 7     7   3560 use Scope::Context;
  7         30522  
  7         793  
15              
16             our $VERSION = 'v0.0.9';
17              
18 7     7   54 use constant TAGS_DEFAULT => (DB_TAG_TYPE ,=> 'sql');
  7         13  
  7         896  
19              
20             use constant {
21 7         3322 _DBI_EXECUTE => \&DBI::st::execute,
22             _DBI_DO => \&DBI::db::do,
23             _DBI_SELECTROW_ARRAY => \&DBI::db::selectrow_array,
24             _DBI_SELECTROW_ARRAYREF => \&DBI::db::selectrow_arrayref,
25             _DBI_SELECTROW_HASHREF => \&DBI::db::selectrow_hashref,
26             _DBI_SELECTALL_ARRAYREF => \&DBI::db::selectall_arrayref,
27             _DBI_SELECTALL_ARRAY => \&DBI::db::selectall_array,
28             _DBI_SELECTROW_ARRAY => \&DBI::db::selectrow_array,
29             _DBI_SELECTALL_HASHREF => \&DBI::db::selectall_hashref,
30             _DBI_SELECTCOL_ARRAYREF => \&DBI::db::selectcol_arrayref,
31 7     7   48 };
  7         23  
32             if (%DBIx::QueryLog::SKIP_PKG_MAP) { # hide from DBIx::QueryLog's caller()
33             $DBIx::QueryLog::SKIP_PKG_MAP{ (__PACKAGE__) } = 1;
34             }
35              
36             our $is_currently_traced; # lexicals can't be localized
37             my ($is_enabled, $is_suspended);
38              
39 31     31   159 sub _numeric_result { 0+ $_[0] }
40 16 100   16   100 sub _sum_elements { looks_like_number($_[0]) ? $_[0] : 1 }
41 74     74   118 sub _array_size { scalar @{ $_[0] } }
  74         313  
42 16     16   33 sub _hash_key_count { scalar keys %{ $_[0] } }
  16         93  
43              
44             # signature processors statement, bind values
45 169     169   641 sub _sig_dbh_stmt_attr_bind { @_[ 1, 3 .. $#_ ] }
46 16     16   63 sub _sig_dbh_stmt_key_attr_bind { @_[ 1, 4 .. $#_ ] }
47 10     10   43 sub _sig_sth_bind { @_[ 0, 1 .. $#_ ] }
48              
49             sub enable {
50 29 100 100 29 1 85438 return if $is_enabled or $is_suspended;
51              
52 7     7   63 no warnings 'redefine';
  7         16  
  7         3160  
53             state $execute = _gen_wrapper(_DBI_EXECUTE, {
54             signature => \&_sig_sth_bind,
55             row_counter => \&_numeric_result,
56             count_condition => sub {
57 10     10   30 my ($sth) = @_;
58 10         65 my $fields = $sth->{NUM_OF_FIELDS};
59 10   66     79 return !defined $fields || $fields == 0; # non-select
60             },
61 23         450 });
62 23         88 *DBI::st::execute = $execute;
63              
64 23         56 state $do = _gen_wrapper(_DBI_DO, {
65             signature => \&_sig_dbh_stmt_attr_bind,
66             row_counter => \&_numeric_result,
67             });
68 23         69 *DBI::db::do = $do;
69              
70 23         97 state $selectall_array = _gen_wrapper(_DBI_SELECTALL_ARRAY, {
71             signature => \&_sig_dbh_stmt_attr_bind,
72             row_counter => \&_sum_elements
73             });
74 23         55 *DBI::db::selectall_array = $selectall_array;
75              
76 23         52 state $selectall_arrayref = _gen_wrapper(_DBI_SELECTALL_ARRAYREF, {
77             signature => \&_sig_dbh_stmt_attr_bind,
78             row_counter => \&_array_size
79             });
80 23         46 *DBI::db::selectall_arrayref = $selectall_arrayref;
81              
82 23         51 state $selectcol_arrayref = _gen_wrapper(_DBI_SELECTCOL_ARRAYREF, {
83             signature => \&_sig_dbh_stmt_attr_bind,
84             row_counter => \&_array_size,
85             });
86 23         47 *DBI::db::selectcol_arrayref = $selectcol_arrayref;
87              
88 23         49 state $selectrow_array = _gen_wrapper(_DBI_SELECTROW_ARRAY, {
89             signature => \&_sig_dbh_stmt_attr_bind,
90             });
91 23         46 *DBI::db::selectrow_array = $selectrow_array;
92              
93 23         71 state $selectrow_arrayref = _gen_wrapper(_DBI_SELECTROW_ARRAYREF, {
94             signature => \&_sig_dbh_stmt_attr_bind
95             });
96 23         46 *DBI::db::selectrow_arrayref = $selectrow_arrayref;
97              
98 23         65 state $selectall_hashref = _gen_wrapper(_DBI_SELECTALL_HASHREF, {
99             signature => \&_sig_dbh_stmt_key_attr_bind,
100             row_counter => \&_hash_key_count,
101             });
102 23         52 *DBI::db::selectall_hashref = $selectall_hashref;
103              
104 23         59 state $selectrow_hashref = _gen_wrapper(_DBI_SELECTROW_HASHREF, {
105             signature => \&_sig_dbh_stmt_attr_bind,
106             });
107 23         49 *DBI::db::selectrow_hashref = $selectrow_hashref;
108            
109 23         53 $is_enabled = 1;
110              
111 23         68 return;
112             }
113              
114             sub disable {
115 20 100   20 1 38448 return unless $is_enabled;
116              
117 7     7   58 no warnings 'redefine';
  7         13  
  7         17229  
118 16         59 *DBI::st::execute = _DBI_EXECUTE;
119 16         393 *DBI::db::do = _DBI_DO;
120 16         26 *DBI::db::selectall_array = _DBI_SELECTALL_ARRAY;
121 16         31 *DBI::db::selectall_arrayref = _DBI_SELECTALL_ARRAYREF;
122 16         28 *DBI::db::selectcol_arrayref = _DBI_SELECTCOL_ARRAYREF;
123 16         31 *DBI::db::selectrow_array = _DBI_SELECTROW_ARRAY;
124 16         31 *DBI::db::selectrow_arrayref = _DBI_SELECTROW_ARRAYREF;
125 16         49 *DBI::db::selectall_hashref = _DBI_SELECTALL_HASHREF;
126 16         32 *DBI::db::selectrow_hashref = _DBI_SELECTROW_HASHREF;
127            
128 16         24 $is_enabled = 0;
129              
130 16         45 return;
131             }
132              
133             sub import {
134 7     7   75 my ($class, $tag_mode) = @_;
135              
136 7         26 enable();
137 7 100       225 return if not defined $tag_mode;
138              
139 3         18 my @sensitive_tags = (
140             DB_TAG_SQL,
141             DB_TAG_BIND,
142             DB_TAG_USER,
143             DB_TAG_DBNAME,
144             );
145              
146 3 100       26 if ($tag_mode eq '-none') {
    100          
    50          
147 1         6 $class->hide_tags(DB_TAGS_ALL);
148             }
149             elsif ($tag_mode eq '-safe') {
150 1         4 $class->hide_tags(@sensitive_tags);
151             }
152             elsif ($tag_mode eq '-secure') {
153 1         4 $class->_disable_tags(@sensitive_tags);
154             }
155             else {
156 0         0 croak "Unknown mode: $tag_mode";
157             }
158 3         100 return;
159             }
160              
161 0     0   0 sub unimport { disable() }
162              
163             sub _tags_dbh {
164 205     205   427 my ($dbh) = @_;
165 205 100 66     1556 return if !blessed($dbh) or !$dbh->isa('DBI::db');
166              
167 195         1529 my $dbname = $dbh->{Name};
168 195 50       710 $dbname = $1 if $dbname =~ /dbname=([^;]+);/;
169              
170             return (
171             maybe DB_TAG_USER ,=> $dbh->{Username},
172 195         1111 maybe DB_TAG_DBNAME ,=> $dbname,
173             );
174             }
175              
176             sub _tags_sth {
177 195     195   366 my ($sth) = @_;
178 195         325 my (%tags, $sql);
179 195 100 66     854 if (!blessed($sth) or !$sth->isa('DBI::st')) {
180 185         362 $sql = "$sth";
181             }
182             else {
183 10         48 %tags = _tags_dbh($sth->{Database});
184 10         57 $sql = $sth->{Statement};
185             }
186 195         381 $sql = _remove_sql_comments($sql);
187              
188 195         434 $tags{ (DB_TAG_SQL) } = $sql;
189              
190 195 100       379 if (my $summary = _gen_sql_summary($sql)) {
191 175         356 $tags{ (DB_TAG_SQL_SUMMARY) } = $summary;
192             }
193              
194 195         863 return %tags;
195             }
196              
197             sub _remove_sql_comments { # TODO: support engine-specific syntax properly
198 195     195   363 my ($sql) = @_;
199              
200 195         6115 $sql =~ s{
201             (?> # skip over strings and quoted table names
202             (['"`]) # opening quote
203             .*?
204             (?<!\\)(?:\\{2})* # make sure the closing quote is not escaped
205             \1 # closing quote
206             )? \K
207             | \#.*?$ # hash until end of line
208             | --.*?$ # double-dash until end of line
209             | /\* (?s).*? \*/ # multi-line C-style comment
210             }{}gmx;
211              
212 195         558 return $sql;
213             }
214              
215             sub _gen_sql_summary {
216 195     195   333 my ($sql) = @_;
217              
218             # comments are removed, so the first occurence should be the keyword
219 195         894 my ($type) = $sql =~ /\b(
220             insert | select | update
221             | delete | truncate | show
222             | alter | create | drop
223             )/ix;
224 195 100       534 return if not $type;
225              
226 175         347 my $table = '...';
227 175 100       1050 if ($sql =~ m{(?:from|into|update|truncate|drop|alter|table)\s+(`?)(\w+)\1}i) {
228 131         367 $table = $2;
229             }
230 175         714 return uc($type) . ": $table";
231             }
232              
233             sub _tags_bind_values {
234 195     195   391 my ($bind_ref) = @_;
235 195 100       543 return if not @$bind_ref;
236              
237 50 50       124 my $bind_str = join ',', map { defined $_ ? "`$_`" : 'undef' } @$bind_ref;
  94         360  
238 50         150 return (DB_TAG_BIND ,=> $bind_str);
239             }
240              
241             sub _tags_caller {
242 195     195   1358 my ($call_package, $call_filename, $call_line) = caller(1);
243 195         1116 my $call_sub = (caller(2))[3];
244             return (
245 195         1670 maybe
246             DB_TAG_CALLER_SUB ,=> $call_sub,
247             DB_TAG_CALLER_FILE ,=> $call_filename,
248             DB_TAG_CALLER_LINE ,=> $call_line,
249             DB_TAG_CALLER_PACKAGE ,=> $call_package,
250             );
251             }
252              
253             {
254             my (%hidden_tags, %disabled_tags);
255              
256             sub _filter_tags {
257 332     332   654 my ($tags) = @_;
258 332         865 delete @$tags{ keys %disabled_tags, keys %hidden_tags };
259 332         1308 return $tags;
260             }
261              
262             sub _tag_enabled {
263 137     137   302 my ($tag) = @_;
264 137         416 return !!_filter_tags({ $tag => 1 })->{$tag};
265             }
266              
267             sub hide_tags {
268 26     26 1 22355 my ($class, @tags) = @_;;
269 26 100       85 return if not @tags;
270              
271 20         66 undef @hidden_tags{@tags};
272 20         68 return;
273             }
274              
275             sub show_tags {
276 31     31 1 101647 my ($class, @tags) = @_;
277 31 100       100 return if not @tags;
278              
279 25         71 delete @hidden_tags{@tags};
280 25         79 return;
281             }
282              
283             sub hide_tags_temporarily {
284 8     8 1 7684 my $class = shift;
285 8         35 my @tags = grep { not exists $hidden_tags{$_} } @_;
  8         31  
286 8         25 $class->hide_tags(@tags);
287 8     8   31 Scope::Context->up->reap(sub { $class->show_tags(@tags) });
  8         791  
288             }
289              
290             sub show_tags_temporarily {
291 8     8 1 8265 my $class = shift;
292 8         21 my @tags = grep { exists $hidden_tags{$_} } @_;
  8         28  
293 8         27 $class->show_tags(@tags);
294 8     8   31 Scope::Context->up->reap(sub { $class->hide_tags(@tags) });
  8         741  
295             }
296              
297             sub _disable_tags {
298 3     3   11 my ($class, @tags) = @_;
299 3         13 undef @disabled_tags{@tags};
300 3         6 return;
301             }
302              
303             sub _enable_tags {
304 2     2   10 my ($class, @tags) = @_;
305 2         8 delete @disabled_tags{@tags};
306 2         12 return;
307             }
308              
309             sub disable_tags {
310 2     2 1 21 my $class = shift;
311 2         7 my @tags = grep { not exists $disabled_tags{$_} } @_;
  4         15  
312 2         12 $class->_disable_tags(@tags);
313 2     2   9 Scope::Context->up->reap(sub { $class->_enable_tags(@tags) });
  2         198  
314             }
315             }
316              
317             sub _add_tag {
318 137     137   341 my ($span, $tag, $value) = @_;
319 137 100       264 return unless _tag_enabled($tag);
320 135         715 $span->add_tag($tag => $value);
321             }
322              
323             sub _gen_wrapper {
324 63     63   122 my ($method, $args) = @_;
325 63         102 my $row_counter = $args->{row_counter};
326 63         81 my $sig_processor = $args->{signature};
327 63         87 my $count_condition = $args->{count_condition};
328 63         286 my $method_name = B::svref_2object($method)->GV->NAME;
329              
330             my $can_count_rows = sub {
331 175 100 100 175   882 defined $row_counter
332             and
333             !defined $count_condition || $count_condition->(@_)
334 63         364 };
335              
336             return sub {
337 324 100   324   1228990 goto $method if $is_currently_traced;
338 195         394 local $is_currently_traced = 1;
339              
340 195         523 my ($statement, @bind) = $sig_processor->(@_);
341 195         391 my $handle = shift;
342              
343 195         688 my $tracer = OpenTracing::GlobalTracer->get_global_tracer();
344 195         1230 my $scope = $tracer->start_active_span(
345             "dbi_$method_name",
346             tags => _filter_tags({
347             TAGS_DEFAULT,
348             _tags_sth($statement),
349             _tags_dbh($handle),
350             _tags_bind_values(\@bind),
351             _tags_caller(),
352             }),
353             );
354 195         1481939594 my $span = $scope->get_span();
355              
356 195         393 my $result;
357 195         327 my $wantarray = wantarray; # eval has its own
358 195         303 my $failed = !eval {
359 195 100       504 if ($wantarray) {
360 26         176 $result = [ $handle->$method(@_) ];
361             }
362             else {
363 169         1456 $result = $handle->$method(@_);
364             }
365 185         115566 1;
366             };
367 195         2450 my $error = $@;
368              
369 195 100 100     1756 if ($failed or defined $handle->err) {
    100          
370 20         78 $span->add_tag(error => 1);
371             }
372             elsif ($can_count_rows->($handle)) {
373 137 100       382 my $rows = sum0(map { $row_counter->($_) } $wantarray ? @$result : $result);
  137         359  
374 137         358 _add_tag($span, DB_TAG_ROWS,=> $rows);
375             }
376 195         25093 $scope->close();
377              
378 195 100       25958 die $error if $failed;
379 185 100       3299 return $wantarray ? @$result : $result;
380             }
381 63         325 }
382              
383             sub enable_temporarily {
384 8 100   8 1 9593 return if $is_enabled;
385              
386 6         20 enable();
387 6         24 Scope::Context->up->reap(\&disable);
388             }
389              
390             sub disable_temporarily {
391 8 100   8 1 53707 return unless $is_enabled;
392              
393 4         15 disable();
394 4         42 Scope::Context->up->reap(\&enable);
395             }
396              
397             sub suspend {
398 2 50   2 1 19 return if $is_suspended;
399              
400 2         7 my $was_enabled = $is_enabled;
401 2         6 disable();
402 2         4 $is_suspended = 1;
403 2 50   2   18 Scope::Context->up->reap(sub { $is_suspended = 0; enable() if $was_enabled });
  2         18434  
  2         13  
404             }
405              
406             1;