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   2330753 use strict;
  7         64  
  7         209  
3 7     7   36 use warnings;
  7         15  
  7         219  
4 7     7   37 use feature qw[ state ];
  7         12  
  7         924  
5 7     7   1740 use syntax 'maybe';
  7         110743  
  7         45  
6 7     7   14578 use B;
  7         16  
  7         329  
7 7     7   46 use Carp qw[ croak ];
  7         13  
  7         304  
8 7     7   5928 use DBI;
  7         57268  
  7         348  
9 7     7   3174 use DBIx::OpenTracing::Constants ':ALL';
  7         17  
  7         1098  
10 7     7   54 use List::Util qw[ sum ];
  7         16  
  7         635  
11 7     7   1704 use OpenTracing::GlobalTracer;
  7         64023  
  7         56  
12 7     7   477 use Package::Constants;
  7         16  
  7         192  
13 7     7   40 use Scalar::Util qw[ blessed looks_like_number ];
  7         15  
  7         411  
14 7     7   3394 use Scope::Context;
  7         29348  
  7         352  
15              
16             our $VERSION = 'v0.0.8';
17              
18 7     7   48 use constant TAGS_DEFAULT => (DB_TAG_TYPE ,=> 'sql');
  7         18  
  7         885  
19              
20             use constant {
21 7         2814 _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         11  
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   195 sub _numeric_result { 0+ $_[0] }
40 12 100   12   78 sub _sum_elements { looks_like_number($_[0]) ? $_[0] : 1 }
41 68     68   132 sub _array_size { scalar @{ $_[0] } }
  68         373  
42 10     10   23 sub _hash_key_count { scalar keys %{ $_[0] } }
  10         68  
43              
44             # signature processors statement, bind values
45 157     157   681 sub _sig_dbh_stmt_attr_bind { @_[ 1, 3 .. $#_ ] }
46 10     10   49 sub _sig_dbh_stmt_key_attr_bind { @_[ 1, 4 .. $#_ ] }
47 10     10   51 sub _sig_sth_bind { @_[ 0, 1 .. $#_ ] }
48              
49             sub enable {
50 29 100 100 29 1 89878 return if $is_enabled or $is_suspended;
51              
52 7     7   67 no warnings 'redefine';
  7         14  
  7         3317  
53             state $execute = _gen_wrapper(_DBI_EXECUTE, {
54             signature => \&_sig_sth_bind,
55             row_counter => \&_numeric_result,
56             count_condition => sub {
57 10     10   28 my ($sth) = @_;
58 10         68 my $fields = $sth->{NUM_OF_FIELDS};
59 10   66     85 return !defined $fields || $fields == 0; # non-select
60             },
61 23         99 });
62 23         93 *DBI::st::execute = $execute;
63              
64 23         63 state $do = _gen_wrapper(_DBI_DO, {
65             signature => \&_sig_dbh_stmt_attr_bind,
66             row_counter => \&_numeric_result,
67             });
68 23         60 *DBI::db::do = $do;
69              
70 23         48 state $selectall_array = _gen_wrapper(_DBI_SELECTALL_ARRAY, {
71             signature => \&_sig_dbh_stmt_attr_bind,
72             row_counter => \&_sum_elements
73             });
74 23         63 *DBI::db::selectall_array = $selectall_array;
75              
76 23         47 state $selectall_arrayref = _gen_wrapper(_DBI_SELECTALL_ARRAYREF, {
77             signature => \&_sig_dbh_stmt_attr_bind,
78             row_counter => \&_array_size
79             });
80 23         50 *DBI::db::selectall_arrayref = $selectall_arrayref;
81              
82 23         53 state $selectcol_arrayref = _gen_wrapper(_DBI_SELECTCOL_ARRAYREF, {
83             signature => \&_sig_dbh_stmt_attr_bind,
84             row_counter => \&_array_size,
85             });
86 23         46 *DBI::db::selectcol_arrayref = $selectcol_arrayref;
87              
88 23         45 state $selectrow_array = _gen_wrapper(_DBI_SELECTROW_ARRAY, {
89             signature => \&_sig_dbh_stmt_attr_bind,
90             });
91 23         55 *DBI::db::selectrow_array = $selectrow_array;
92              
93 23         62 state $selectrow_arrayref = _gen_wrapper(_DBI_SELECTROW_ARRAYREF, {
94             signature => \&_sig_dbh_stmt_attr_bind
95             });
96 23         51 *DBI::db::selectrow_arrayref = $selectrow_arrayref;
97              
98 23         59 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         59 *DBI::db::selectall_hashref = $selectall_hashref;
103              
104 23         50 state $selectrow_hashref = _gen_wrapper(_DBI_SELECTROW_HASHREF, {
105             signature => \&_sig_dbh_stmt_attr_bind,
106             });
107 23         71 *DBI::db::selectrow_hashref = $selectrow_hashref;
108            
109 23         42 $is_enabled = 1;
110              
111 23         55 return;
112             }
113              
114             sub disable {
115 20 100   20 1 39816 return unless $is_enabled;
116              
117 7     7   57 no warnings 'redefine';
  7         19  
  7         17073  
118 16         60 *DBI::st::execute = _DBI_EXECUTE;
119 16         31 *DBI::db::do = _DBI_DO;
120 16         32 *DBI::db::selectall_array = _DBI_SELECTALL_ARRAY;
121 16         34 *DBI::db::selectall_arrayref = _DBI_SELECTALL_ARRAYREF;
122 16         26 *DBI::db::selectcol_arrayref = _DBI_SELECTCOL_ARRAYREF;
123 16         34 *DBI::db::selectrow_array = _DBI_SELECTROW_ARRAY;
124 16         23 *DBI::db::selectrow_arrayref = _DBI_SELECTROW_ARRAYREF;
125 16         34 *DBI::db::selectall_hashref = _DBI_SELECTALL_HASHREF;
126 16         26 *DBI::db::selectrow_hashref = _DBI_SELECTROW_HASHREF;
127            
128 16         25 $is_enabled = 0;
129              
130 16         37 return;
131             }
132              
133             sub import {
134 7     7   65 my ($class, $tag_mode) = @_;
135              
136 7         24 enable();
137 7 100       175 return if not defined $tag_mode;
138              
139 3         13 my @sensitive_tags = (
140             DB_TAG_SQL,
141             DB_TAG_BIND,
142             DB_TAG_USER,
143             DB_TAG_DBNAME,
144             );
145              
146 3 100       16 if ($tag_mode eq '-none') {
    100          
    50          
147 1         13 $class->hide_tags(DB_TAGS_ALL);
148             }
149             elsif ($tag_mode eq '-safe') {
150 1         3 $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         130 return;
159             }
160              
161 0     0   0 sub unimport { disable() }
162              
163             sub _tags_dbh {
164 187     187   470 my ($dbh) = @_;
165 187 100 66     1550 return if !blessed($dbh) or !$dbh->isa('DBI::db');
166              
167 177         1548 my $dbname = $dbh->{Name};
168 177 50       676 $dbname = $1 if $dbname =~ /dbname=([^;]+);/;
169              
170             return (
171             maybe DB_TAG_USER ,=> $dbh->{Username},
172 177         1117 maybe DB_TAG_DBNAME ,=> $dbname,
173             );
174             }
175              
176             sub _tags_sth {
177 177     177   395 my ($sth) = @_;
178 177         309 my (%tags, $sql);
179 177 100 66     1140 if (!blessed($sth) or !$sth->isa('DBI::st')) {
180 167         338 $sql = "$sth";
181             }
182             else {
183 10         49 %tags = _tags_dbh($sth->{Database});
184 10         57 $sql = $sth->{Statement};
185             }
186 177         399 $sql = _remove_sql_comments($sql);
187              
188 177         423 $tags{ (DB_TAG_SQL) } = $sql;
189              
190 177 100       407 if (my $summary = _gen_sql_summary($sql)) {
191 157         339 $tags{ (DB_TAG_SQL_SUMMARY) } = $summary;
192             }
193              
194 177         826 return %tags;
195             }
196              
197             sub _remove_sql_comments { # TODO: support engine-specific syntax properly
198 177     177   334 my ($sql) = @_;
199              
200 177         5638 $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 177         493 return $sql;
213             }
214              
215             sub _gen_sql_summary {
216 177     177   340 my ($sql) = @_;
217              
218             # comments are removed, so the first occurence should be the keyword
219 177         931 my ($type) = $sql =~ /\b(
220             insert | select | update
221             | delete | truncate | show
222             | alter | create | drop
223             )/ix;
224 177 100       505 return if not $type;
225              
226 157         295 my $table = '...';
227 157 100       1101 if ($sql =~ m{(?:from|into|update|truncate|drop|alter|table)\s+(`?)(\w+)\1}i) {
228 113         357 $table = $2;
229             }
230 157         662 return uc($type) . ": $table";
231             }
232              
233             sub _tags_bind_values {
234 177     177   374 my ($bind_ref) = @_;
235 177 100       550 return if not @$bind_ref;
236              
237 50 50       131 my $bind_str = join ',', map { defined $_ ? "`$_`" : 'undef' } @$bind_ref;
  94         399  
238 50         162 return (DB_TAG_BIND ,=> $bind_str);
239             }
240              
241             sub _tags_caller {
242 177     177   1293 my ($call_package, $call_filename, $call_line) = caller(1);
243 177         1152 my $call_sub = (caller(2))[3];
244             return (
245 177         1681 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 296     296   615 my ($tags) = @_;
258 296         860 delete @$tags{ keys %disabled_tags, keys %hidden_tags };
259 296         1412 return $tags;
260             }
261              
262             sub _tag_enabled {
263 119     119   296 my ($tag) = @_;
264 119         416 return !!_filter_tags({ $tag => 1 })->{$tag};
265             }
266              
267             sub hide_tags {
268 26     26 1 23278 my ($class, @tags) = @_;;
269 26 100       92 return if not @tags;
270              
271 20         62 undef @hidden_tags{@tags};
272 20         56 return;
273             }
274              
275             sub show_tags {
276 31     31 1 99601 my ($class, @tags) = @_;
277 31 100       102 return if not @tags;
278              
279 25         82 delete @hidden_tags{@tags};
280 25         76 return;
281             }
282              
283             sub hide_tags_temporarily {
284 8     8 1 8508 my $class = shift;
285 8         25 my @tags = grep { not exists $hidden_tags{$_} } @_;
  8         36  
286 8         34 $class->hide_tags(@tags);
287 8     8   40 Scope::Context->up->reap(sub { $class->show_tags(@tags) });
  8         778  
288             }
289              
290             sub show_tags_temporarily {
291 8     8 1 8614 my $class = shift;
292 8         23 my @tags = grep { exists $hidden_tags{$_} } @_;
  8         63  
293 8         29 $class->show_tags(@tags);
294 8     8   35 Scope::Context->up->reap(sub { $class->hide_tags(@tags) });
  8         783  
295             }
296              
297             sub _disable_tags {
298 3     3   19 my ($class, @tags) = @_;
299 3         12 undef @disabled_tags{@tags};
300 3         10 return;
301             }
302              
303             sub _enable_tags {
304 2     2   9 my ($class, @tags) = @_;
305 2         10 delete @disabled_tags{@tags};
306 2         13 return;
307             }
308              
309             sub disable_tags {
310 2     2 1 17 my $class = shift;
311 2         7 my @tags = grep { not exists $disabled_tags{$_} } @_;
  4         16  
312 2         13 $class->_disable_tags(@tags);
313 2     2   10 Scope::Context->up->reap(sub { $class->_enable_tags(@tags) });
  2         230  
314             }
315             }
316              
317             sub _add_tag {
318 119     119   337 my ($span, $tag, $value) = @_;
319 119 100       282 return unless _tag_enabled($tag);
320 117         613 $span->add_tag($tag => $value);
321             }
322              
323             sub _gen_wrapper {
324 63     63   111 my ($method, $args) = @_;
325 63         98 my $row_counter = $args->{row_counter};
326 63         78 my $sig_processor = $args->{signature};
327 63         75 my $count_condition = $args->{count_condition};
328 63         288 my $method_name = B::svref_2object($method)->GV->NAME;
329              
330             my $can_count_rows = sub {
331 157 100 100 157   879 defined $row_counter
332             and
333             !defined $count_condition || $count_condition->(@_)
334 63         340 };
335              
336             return sub {
337 288 100   288   1180077 goto $method if $is_currently_traced;
338 177         361 local $is_currently_traced = 1;
339              
340 177         523 my ($statement, @bind) = $sig_processor->(@_);
341 177         382 my $handle = shift;
342              
343 177         667 my $tracer = OpenTracing::GlobalTracer->get_global_tracer();
344 177         1157 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 177         2333511141 my $span = $scope->get_span();
355              
356 177         339 my $result;
357 177         314 my $wantarray = wantarray; # eval has its own
358 177         270 my $failed = !eval {
359 177 100       450 if ($wantarray) {
360 20         147 $result = [ $handle->$method(@_) ];
361             }
362             else {
363 157         1612 $result = $handle->$method(@_);
364             }
365 167         114893 1;
366             };
367 177         2685 my $error = $@;
368              
369 177 100 100     1770 if ($failed or defined $handle->err) {
    100          
370 20         81 $span->add_tag(error => 1);
371             }
372             elsif ($can_count_rows->($handle)) {
373 119 100       344 my $rows = sum(map { $row_counter->($_) } $wantarray ? @$result : $result);
  121         335  
374 119         379 _add_tag($span, DB_TAG_ROWS,=> $rows);
375             }
376 177         23789 $scope->close();
377              
378 177 100       24804 die $error if $failed;
379 167 100       3108 return $wantarray ? @$result : $result;
380             }
381 63         340 }
382              
383             sub enable_temporarily {
384 8 100   8 1 10082 return if $is_enabled;
385              
386 6         23 enable();
387 6         25 Scope::Context->up->reap(\&disable);
388             }
389              
390             sub disable_temporarily {
391 8 100   8 1 55968 return unless $is_enabled;
392              
393 4         18 disable();
394 4         29 Scope::Context->up->reap(\&enable);
395             }
396              
397             sub suspend {
398 2 50   2 1 19 return if $is_suspended;
399              
400 2         5 my $was_enabled = $is_enabled;
401 2         9 disable();
402 2         7 $is_suspended = 1;
403 2 50   2   9 Scope::Context->up->reap(sub { $is_suspended = 0; enable() if $was_enabled });
  2         19577  
  2         12  
404             }
405              
406             1;