File Coverage

blib/lib/Mojolicious/Plugin/OpenTelemetry.pm
Criterion Covered Total %
statement 62 65 95.3
branch 16 22 72.7
condition 8 17 47.0
subroutine 7 7 100.0
pod 1 1 100.0
total 94 112 83.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::OpenTelemetry;
2             # ABSTRACT: An OpenTelemetry integration for Mojolicious
3              
4             our $VERSION = '0.005';
5              
6 1     1   1874283 use Mojo::Base 'Mojolicious::Plugin', -signatures;
  1         3  
  1         13  
7              
8 1     1   499 use Feature::Compat::Try;
  1         3  
  1         11  
9 1     1   107 use OpenTelemetry -all;
  1         2  
  1         14  
10 1     1   1876 use OpenTelemetry::Constants -span;
  1         4  
  1         12  
11 1     1   2123 use Syntax::Keyword::Dynamically;
  1         3  
  1         9  
12              
13 1     1 1 56 sub register ( $, $app, $config, @ ) {
  1         2  
  1         47  
  1         3  
14 1   33     13 $config->{tracer}{name} //= otel_config('SERVICE_NAME') // $app->moniker;
      33        
15              
16 16     16   466694 $app->hook( around_action => sub ( $next, $c, $action, $last, @ ) {
  16         48  
  16         32  
  16         42  
  16         39  
  16         33  
17 16 100       67 return $next->() unless $last;
18              
19 15         106 my $tracer = otel_tracer_provider->tracer( %{ $config->{tracer} } );
  15         350  
20              
21 15         582 my $tx = $c->tx;
22 15         122 my $req = $tx->req;
23 15         150 my $url = $req->url->to_abs;
24 15         4120 my $route = $c->match->endpoint->to_string;
25 15         1005 my $query = $url->query->to_string;
26 15         657 my $method = $req->method;
27 15         128 my $headers = $req->headers;
28 15         216 my $agent = $headers->user_agent;
29              
30             # https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
31 15         164 my $hostport;
32 15 100       73 if ( my $fwd = $headers->header('forwarded') ) {
33 4         75 my ($first) = split ',', $fwd, 2;
34 4 50 33     50 $hostport = $1 // $2 if $first =~ /host=(?:"([^"]+)"|([^;]+))/;
35             }
36              
37 15   66     252 $hostport //= $headers->header('x-forwarded-proto')
      66        
38             // $headers->header('host');
39              
40 15         447 my ( $host, $port ) = $hostport =~ /(.*?)(?::([0-9]+))?$/g;
41              
42             my $context = otel_propagator->extract(
43             $headers,
44             undef,
45 0         0 sub ( $carrier, $key ) { $carrier->header($key) },
46 15         81 );
47              
48 15 50       1283 my $span = $tracer->create_span(
    100          
    50          
    100          
49             name => $method . ' ' . $route,
50             kind => SPAN_KIND_SERVER,
51             parent => $context,
52             attributes => {
53             'http.request.method' => $method,
54             'network.protocol.version' => $req->version,
55             'url.path' => $url->path->to_string,
56             'url.scheme' => $url->scheme,
57             'http.route' => $route,
58             'client.address' => $tx->remote_address,
59             'client.port' => $tx->remote_port,
60             $host ? ( 'server.address' => $host ) : (),
61             $port ? ( 'server.port' => $port ) : (),
62             $agent ? ( 'user_agent.original' => $agent ) : (),
63             $query ? ( 'url.query' => $query ) : (),
64             },
65             );
66              
67 15         37367 dynamically otel_current_context
68             = otel_context_with_span( $span, $context );
69              
70 15         2181 try {
71 15         38 my @result;
72 15         33 my $want = wantarray;
73              
74 15 50       50 if ($want) { @result = $next->() }
  0         0  
75 15         57 else { $result[0] = $next->() }
76              
77 14 100       8607 my $promise = $result[0]->can('then')
78             ? $result[0]
79             : Mojo::Promise->resolve(1);
80              
81             $promise->then( sub {
82 14         116621 my $code = $tx->res->code;
83              
84             # The status of server spans must be left unset if the
85             # response is a 4XX error
86             # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status
87 14 50       183 if ( $code >= 500 ) {
88 0         0 $span->set_status(SPAN_STATUS_ERROR);
89             }
90              
91             $span
92 14         83 ->set_attribute( 'http.response.status_code' => $code )
93             ->end;
94 14         2937 })->wait;
95              
96 14 50       2180 return $want ? @result : $result[0];
97             }
98             catch ($error) {
99 1         829 my ($message) = split /\n/, "$error", 2;
100 1         54 $message =~ s/ at \S+ line \d+\.$//a;
101              
102 1   50     6 $span
103             ->record_exception($error)
104             ->set_status( SPAN_STATUS_ERROR, $message )
105             ->set_attribute(
106             'error.type' => ref $error || 'string',
107             'http.response.status_code' => 500,
108             )
109             ->end;
110              
111 1         291 die $error;
112             }
113 1         75 });
114             }
115              
116             1;