File Coverage

blib/lib/CGI/Application/Plugin/OpenTracing.pm
Criterion Covered Total %
statement 270 276 97.8
branch 52 62 83.8
condition 9 12 75.0
subroutine 53 54 98.1
pod 1 6 16.6
total 385 410 93.9


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