File Coverage

blib/lib/CGI/Application/Plugin/OpenTracing.pm
Criterion Covered Total %
statement 281 287 97.9
branch 52 62 83.8
condition 9 12 75.0
subroutine 57 58 98.2
pod 1 6 16.6
total 400 425 94.1


line stmt bran cond sub pod time code
1             package CGI::Application::Plugin::OpenTracing;
2              
3 5     5   1313588 use strict;
  5         35  
  5         130  
4 5     5   24 use warnings;
  5         11  
  5         175  
5              
6             our $VERSION = 'v0.103.2';
7              
8 5     5   1558 use syntax 'maybe';
  5         80563  
  5         27  
9              
10 5     5   15726 use OpenTracing::Implementation;
  5         12254  
  5         29  
11 5     5   137 use OpenTracing::GlobalTracer;
  5         9  
  5         20  
12              
13 5     5   303 use Carp qw( croak carp );
  5         9  
  5         237  
14 5     5   464 use HTTP::Headers;
  5         3403  
  5         152  
15 5     5   413 use HTTP::Status;
  5         3727  
  5         1353  
16 5     5   2343 use NEXT;
  5         11621  
  5         162  
17 5     5   38 use Scalar::Util qw( refaddr );
  5         11  
  5         236  
18 5     5   416 use Sub::Name;
  5         457  
  5         208  
19 5     5   30 use Time::HiRes qw( gettimeofday );
  5         10  
  5         53  
20              
21 5     5   849 use constant CGI_LOAD_TMPL => 'cgi_application_load_tmpl';
  5         22  
  5         350  
22 5     5   31 use constant CGI_REQUEST => 'cgi_application_request';
  5         10  
  5         249  
23 5     5   31 use constant CGI_RUN => 'cgi_application_run';
  5         8  
  5         268  
24 5     5   29 use constant CGI_SETUP => 'cgi_application_setup';
  5         10  
  5         207  
25 5     5   29 use constant CGI_TEARDOWN => 'cgi_application_teardown';
  5         17  
  5         1218  
26              
27             our $implementation_import_name;
28             our @implementation_import_opts;
29              
30             our $TAG_JOIN_CHAR = ',';
31              
32             sub import {
33 5     5   50 my $package = shift;
34            
35 5         14 ( $implementation_import_name, @implementation_import_opts ) = @_;
36 5 100 33     23 $ENV{OPENTRACING_DEBUG} && carp "OpenTracing Implementation not defined during import\n"
37             unless defined $implementation_import_name;
38            
39 5         38 my $caller = caller;
40 5         139 $caller->add_callback( init => \&init );
41 5         86 $caller->add_callback( prerun => \&prerun );
42 5         57 $caller->add_callback( postrun => \&postrun );
43 5         55 $caller->add_callback( load_tmpl => \&load_tmpl );
44 5         52 $caller->add_callback( teardown => \&teardown );
45 5         53 $caller->add_callback( error => \&error );
46            
47              
48 5         48 my $full_name = $caller . '::run';
49 5     5   35 my $run_glob = do { no strict 'refs'; \*$full_name };
  5         12  
  5         449  
  5         11  
  5         20  
50             my $run_orig
51             = defined &$run_glob
52             ? \&run_glob
53 5 50   27   64 : subname $full_name => sub { my $self = shift; $self->NEXT::run(@_) };
  27         67  
  27         298  
54 5     5   31 no warnings 'redefine';
  5         9  
  5         12216  
55 5         16 *$run_glob = _wrap_run($run_orig);
56              
57 5         127 return;
58             }
59              
60             sub _wrap_run {
61 5     5   11 my ($orig) = @_;
62              
63             return sub {
64 27     27   1224 my $cgi_app = shift;
65              
66 27         46 my $res;
67 27         62 my $wantarray = wantarray; # eval has its own
68 27         48 my $ok = eval {
69 27 50       87 if ($wantarray) {
70 0         0 $res = [ $cgi_app->$orig(@_) ];
71             }
72             else {
73 27         80 $res = $cgi_app->$orig(@_);
74             }
75 24         693 1;
76             };
77 27 50       1678 return $wantarray ? @$res : $res if $ok;
    100          
78              
79 3         9 my $error = $@;
80              
81 3         10 my $request_span = _plugin_get_scope($cgi_app, CGI_REQUEST)->get_span;
82 3         18 $request_span->add_tag('http.status_code' => 500);
83              
84 3         378 _cascade_set_failed_spans($cgi_app, $error);
85              
86 3         63 die $error;
87 5         36 };
88             }
89              
90             sub _cascade_set_failed_spans {
91 5     5   14 my ($cgi_app, $error, $root_span) = @_;
92 5 100       25 my $root_addr = refaddr($root_span) if defined $root_span;
93              
94 5         10 my $tracer = _plugin_get_tracer($cgi_app);
95 5         27 while (my $scope = $tracer->get_scope_manager->get_active_scope()) {
96 10         881 my $span = $scope->get_span();
97 10 100 100     47 last if defined $root_addr and $root_addr eq refaddr($span);
98              
99 8         27 $span->add_tags(error => 1, message => $error);
100 8         872 $scope->close();
101             }
102 5         327 return;
103             }
104              
105             sub init {
106 27     27 0 514692 my $cgi_app = shift;
107            
108 27         132 _plugin_init_opentracing_implementation( $cgi_app );
109            
110 27         109 my %request_tags = _get_request_tags($cgi_app);
111 27         135 my %query_params = _get_query_params($cgi_app);
112 27         96 my %form_data = _get_form_data($cgi_app);
113 27         104 my $context = _tracer_extract_context( $cgi_app );
114            
115 27         15643 _plugin_start_active_span( $cgi_app, CGI_REQUEST, child_of => $context );
116 27         169 _plugin_add_tags( $cgi_app, CGI_REQUEST, %request_tags );
117 27         3493 _plugin_add_tags( $cgi_app, CGI_REQUEST, %query_params );
118 27         2714 _plugin_add_tags( $cgi_app, CGI_REQUEST, %form_data );
119 27         2768 _plugin_start_active_span( $cgi_app, CGI_SETUP );
120            
121             return
122 27         152 }
123              
124              
125              
126             sub prerun {
127 27     27 0 8751 my $cgi_app = shift;
128            
129 27         101 my %runmode_tags = _get_runmode_tags($cgi_app);
130 27         99 my %baggage_items = _get_baggage_items($cgi_app);
131            
132 27         115 _plugin_add_baggage_items( $cgi_app, CGI_SETUP, %baggage_items );
133 27         2886 _plugin_close_scope( $cgi_app, CGI_SETUP );
134 27         4397 _plugin_add_baggage_items( $cgi_app, CGI_REQUEST, %baggage_items );
135 27         2304 _plugin_add_tags( $cgi_app, CGI_REQUEST, %runmode_tags );
136 27         2971 _plugin_start_active_span( $cgi_app, CGI_RUN );
137            
138             return
139 27         102 }
140              
141              
142              
143             sub postrun {
144 24     24 0 4639 my $cgi_app = shift;
145            
146 24         76 _plugin_close_scope( $cgi_app, CGI_RUN );
147 24         3235 _plugin_start_active_span( $cgi_app, CGI_TEARDOWN );
148            
149             return
150 24         78 }
151              
152              
153              
154             sub load_tmpl {
155 0     0 0 0 my $cgi_app = shift;
156            
157 0         0 _plugin_close_scope( $cgi_app, CGI_LOAD_TMPL );
158            
159             return
160 0         0 }
161              
162              
163              
164             sub teardown {
165 24     24 1 7224 my $cgi_app = shift;
166            
167 24         113 my %http_status_tags = _get_http_status_tags($cgi_app);
168            
169 24         90 _plugin_close_scope( $cgi_app, CGI_TEARDOWN );
170 24         3162 _plugin_add_tags( $cgi_app, CGI_REQUEST, %http_status_tags );
171 24         2890 _plugin_close_scope( $cgi_app, CGI_REQUEST );
172            
173             return
174 24         2508 }
175              
176              
177              
178             sub error {
179 4     4 0 2308 my ($cgi_app, $error) = @_;
180 4 100       20 return if not $cgi_app->error_mode(); # we're dying
181              
182             # run span should continue
183 2         22 my $root = _plugin_get_scope($cgi_app, CGI_RUN)->get_span;
184 2         9 _cascade_set_failed_spans($cgi_app, $error, $root);
185            
186 2         5 return;
187             }
188              
189              
190              
191             sub _init_global_tracer {
192 27     27   62 my $cgi_app = shift;
193            
194 27         98 my @bootstrap_options = _get_bootstrap_options($cgi_app);
195            
196 27 50       308 my $bootstrapped_tracer =
197             $implementation_import_name ?
198             OpenTracing::Implementation->bootstrap_tracer(
199             $implementation_import_name,
200             @implementation_import_opts,
201             @bootstrap_options,
202             )
203             :
204             OpenTracing::Implementation->bootstrap_default_tracer(
205             @implementation_import_opts,
206             @bootstrap_options,
207             )
208             ;
209            
210 27         1951571 OpenTracing::GlobalTracer->set_global_tracer( $bootstrapped_tracer );
211            
212             return
213 27         11370 }
214              
215              
216              
217             sub _cgi_get_run_mode {
218 27     27   51 my $cgi_app = shift;
219            
220 27         139 my $run_mode = $cgi_app->get_current_runmode();
221            
222 27         212 return $run_mode
223             }
224              
225              
226              
227             sub _cgi_get_run_method {
228 27     27   76 my $cgi_app = shift;
229            
230 27         66 my $run_mode = $cgi_app->get_current_runmode();
231 27         185 my $run_methode = { $cgi_app->run_modes }->{ $run_mode };
232            
233 27         331 return $run_methode
234             }
235              
236              
237              
238             sub _cgi_get_http_method {
239 27     27   75 my $cgi_app = shift;
240            
241 27         175 my $query = $cgi_app->query();
242            
243 27         124798 return $query->request_method();
244             }
245              
246              
247             sub _cgi_get_http_headers { # TODO: extract headers from CGI request
248 27     27   60 my $cgi_app = shift;
249 27         233 return HTTP::Headers->new();
250             }
251              
252              
253             sub _cgi_get_http_url {
254 27     27   198 my $cgi_app = shift;
255            
256 27         106 my $query = $cgi_app->query();
257            
258 27         320 return $query->url(-path => 1);
259             }
260              
261              
262              
263             =for not_implemented
264             sub get_opentracing_global_tracer {
265             OpenTracing::GlobalTracer->get_global_tracer()
266             }
267             =cut
268              
269              
270              
271             sub _get_request_tags {
272 27     27   66 my $cgi_app = shift;
273            
274 27         129 my %tags = (
275             'component' => 'CGI::Application',
276             maybe 'http.method' => _cgi_get_http_method($cgi_app),
277             maybe 'http.url' => _cgi_get_http_url($cgi_app),
278             );
279            
280              
281 27         10593 return %tags
282             }
283              
284             sub _gen_tag_processor {
285 38     38   106 my $cgi_app = shift;
286            
287 38     32   178 my $joiner = sub { join $TAG_JOIN_CHAR, @_ };
  32         89  
288            
289 38         93 my (@specs, $fallback);
290 38         113 foreach my $spec_gen (@_) {
291 76 100       228 next if not defined $spec_gen;
292            
293 28         99 my ($spec, $spec_fallback) = _gen_spec($spec_gen->());
294 28   100     135 $fallback ||= $spec_fallback;
295 28         71 push @specs, $spec;
296             }
297 38   66     172 $fallback ||= $joiner;
298            
299             return sub {
300 73     73   148 my ($cgi_app, $name, $values) = @_;
301            
302 73         111 my $processor = $fallback;
303 73         149 foreach my $spec (@specs) {
304 62         180 my ($matched, $spec_processor) = $spec->($name);
305 62 100       134 $processor = $spec_processor if $matched;
306             }
307            
308 73 100       154 return if not defined $processor;
309 64 100       203 return $processor if not ref $processor;
310              
311 54 50       165 if (ref $processor eq 'CODE') {
312 54         144 my $processed = $processor->(@$values);
313 54 100       263 $processed = $joiner->(@$processed) if ref $processed eq 'ARRAY';
314 54         124 return $processed;
315             }
316            
317 0         0 croak "Invalid processor for param `$name`: ", ref $processor;
318 38         171 };
319             }
320              
321             sub _gen_spec {
322 28     28   212 my @def = @_;
323            
324 28         49 my $fallback;
325 28 100       88 $fallback = pop @def if @def % 2 != 0;
326            
327 28         48 my (%direct_match, @regex);
328 28         104 while (my ($cond, $processor) = splice @def, 0, 2) {
329 33 100       88 if (ref $cond eq 'Regexp') {
330 1         5 push @regex, [ $cond => $processor ];
331             }
332             else {
333 32 100       81 foreach my $name (ref $cond eq 'ARRAY' ? @$cond : $cond) {
334 33         139 $direct_match{$name} = $processor;
335             }
336             }
337             }
338             my $spec = sub {
339 62     62   111 my ($name) = @_;
340            
341             # return match state separately to differentiate from undef processors
342 62 100       160 return (1, $direct_match{$name}) if exists $direct_match{$name};
343            
344 34         78 foreach (@regex) {
345 3         7 my ($re, $processor) = @$_;
346 3 50       21 return (1, $processor) if $name =~ $re;
347             }
348 31         53 return;
349 28         104 };
350            
351 28         74 return ($spec, $fallback);
352             }
353              
354             sub _get_query_params {
355 27     27   61 my $cgi_app = shift;
356            
357 27         298 my $processor = _gen_tag_processor($cgi_app,
358             $cgi_app->can('opentracing_process_tags_query_params'),
359             $cgi_app->can('opentracing_process_tags'),
360             );
361            
362 27         56 my %processed_params;
363            
364 27         92 my $query = $cgi_app->query();
365 27         300 foreach my $param ($query->url_param()) {
366 48 50       2354 next unless defined $param; # huh ???
367 48         129 my @values = $query->url_param($param);
368 48         776 my $processed_value = $cgi_app->$processor($param, \@values);
369 48 100       129 next unless defined $processed_value;
370            
371 41         127 $processed_params{"http.query.$param"} = $processed_value;
372             }
373 27         644 return %processed_params;
374             }
375              
376             sub _get_form_data {
377 27     27   83 my $cgi_app = shift;
378 27         131 my $query = $cgi_app->query();
379 27 100       259 return unless _has_form_data($query);
380            
381 11         82 my $processor = _gen_tag_processor($cgi_app,
382             $cgi_app->can('opentracing_process_tags_form_fields'),
383             $cgi_app->can('opentracing_process_tags'),
384             );
385            
386 11         25 my %processed_params = ();
387            
388 11         38 my %params = $cgi_app->query->Vars();
389 11         1799 while (my ($param_name, $param_value) = each %params) {
390 25         86 my $processed_value = $cgi_app->$processor(
391             $param_name, [ split /\0/, $param_value ]
392             );
393 25 100       71 next unless defined $processed_value;
394 21         99 $processed_params{"http.form.$param_name"} = $processed_value
395             }
396            
397 11         116 return %processed_params;
398             }
399              
400             sub _has_form_data {
401 27     27   70 my ($query) = @_;
402 27         96 my $content_type = $query->content_type();
403 27 100       175 return if not defined $content_type;
404 11 50       43 return 1 if $content_type =~ m{\Amultipart/form-data};
405 11 50       83 return 1 if $content_type =~ m{\Aapplication/x-www-form-urlencoded};
406 0         0 return;
407             }
408              
409             sub _get_runmode_tags {
410 27     27   53 my $cgi_app = shift;
411            
412 27         99 my %tags = (
413             maybe 'run_mode' => _cgi_get_run_mode($cgi_app),
414             maybe 'run_method' => _cgi_get_run_method($cgi_app),
415             );
416 27         104 return %tags
417             }
418              
419             sub _get_http_status_tags {
420 24     24   57 my $cgi_app = shift;
421            
422 24         87 my %headers = $cgi_app->header_props();
423             my $status = $headers{-status} or return (
424 24 100       491 'http.status_code' => '200',
425             );
426 3         22 my $status_code = [ $status =~ /^\s*(\d{3})/ ]->[0];
427 3         9 my $status_mess = [ $status =~ /^\s*\d{3}\s*(.+)\s*$/ ]->[0];
428            
429 3 50       22 $status_mess = HTTP::Status::status_message($status_code)
430             unless defined $status_mess;
431            
432 3         29 my %tags = (
433             maybe 'http.status_code' => $status_code,
434             maybe 'http.status_message' => $status_mess,
435             );
436 3         13 return %tags
437             }
438              
439              
440             sub _get_bootstrap_options {
441 27     27   60 my $cgi_app = shift;
442            
443 27 100       260 return unless $cgi_app->can('opentracing_bootstrap_options');
444            
445 1         4 my @bootstrap_options = $cgi_app->opentracing_bootstrap_options( );
446            
447             return @bootstrap_options
448 1         7 }
449              
450              
451              
452             sub _get_baggage_items {
453 27     27   49 my $cgi_app = shift;
454            
455 27 100       222 return unless $cgi_app->can('opentracing_baggage_items');
456            
457 1         15 my %baggage_items = $cgi_app->opentracing_baggage_items( );
458            
459            
460 1         464 return %baggage_items
461             }
462              
463              
464              
465             sub _tracer_extract_context {
466 27     27   49 my $cgi_app = shift;
467            
468 27         77 my $http_headers = _cgi_get_http_headers($cgi_app);
469 27         328 my $tracer = _plugin_get_tracer( $cgi_app );
470            
471 27         139 return $tracer->extract_context($http_headers)
472             }
473              
474             sub _plugin_get_tracer {
475 137     137   237 my $cgi_app = shift;
476             return $cgi_app->{__PLUGINS}{OPENTRACING}{TRACER}
477 137         319 }
478              
479             sub _plugin_init_opentracing_implementation {
480 27     27   71 my $cgi_app = shift;
481            
482 27         112 _init_global_tracer($cgi_app);
483             # unless OpenTracing::GlobalTracer->is_registered;
484 27         116 my $tracer = OpenTracing::GlobalTracer->get_global_tracer;
485            
486 27         216 $cgi_app->{__PLUGINS}{OPENTRACING}{TRACER} = $tracer;
487             }
488              
489             sub _plugin_start_active_span {
490 105     105   220 my $cgi_app = shift;
491 105         168 my $operation_name = shift;
492 105         207 my %params = @_;
493 105         222 my $scope_name = uc $operation_name;
494            
495 105         336 my $scope =
496             _tracer_start_active_span( $cgi_app, $operation_name, %params );
497            
498 105         2420678546 $cgi_app->{__PLUGINS}{OPENTRACING}{SCOPE}{$scope_name} = $scope;
499             }
500              
501             sub _tracer_start_active_span {
502 105     105   172 my $cgi_app = shift;
503 105         162 my $operation_name = shift;
504 105         181 my %params = @_;
505            
506 105         194 my $tracer = _plugin_get_tracer($cgi_app);
507 105         361 $tracer->start_active_span( $operation_name, %params );
508             }
509              
510             sub _plugin_add_tags {
511 132     132   240 my $cgi_app = shift;
512 132         197 my $operation_name = shift;
513 132         360 my %tags = @_;
514 132         256 my $scope_name = uc $operation_name;
515            
516 132         625 $cgi_app->{__PLUGINS}{OPENTRACING}{SCOPE}{$scope_name}
517             ->get_span->add_tags(%tags);
518             }
519              
520             sub _plugin_add_baggage_items {
521 54     54   101 my $cgi_app = shift;
522 54         89 my $operation_name = shift;
523 54         118 my %baggage_items = @_;
524 54         109 my $scope_name = uc $operation_name;
525            
526 54         290 $cgi_app->{__PLUGINS}{OPENTRACING}{SCOPE}{$scope_name}
527             ->get_span->add_baggage_items( %baggage_items );
528             }
529              
530             sub _plugin_close_scope {
531 99     99   167 my $cgi_app = shift;
532 99         176 my $operation_name = shift;
533 99         195 my $scope_name = uc $operation_name;
534            
535 99         423 $cgi_app->{__PLUGINS}{OPENTRACING}{SCOPE}{$scope_name}->close
536             }
537              
538             sub _plugin_get_scope {
539 5     5   11 my $cgi_app = shift;
540 5         10 my $scope_name = shift;
541 5         24 return $cgi_app->{__PLUGINS}{OPENTRACING}{SCOPE}{uc $scope_name};
542             }
543              
544              
545             1;