File Coverage

blib/lib/DBIx/OpenTracing.pm
Criterion Covered Total %
statement 226 228 99.1
branch 55 60 91.6
condition 15 18 83.3
subroutine 55 56 98.2
pod 10 10 100.0
total 361 372 97.0


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