File Coverage

blib/lib/Mojolicious/Plugin/OpenTelemetry.pm
Criterion Covered Total %
statement 66 67 98.5
branch 8 12 66.6
condition 10 19 52.6
subroutine 8 8 100.0
pod 1 1 100.0
total 93 107 86.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.006';
5              
6 1     1   1856658 use Mojo::Base 'Mojolicious::Plugin', -signatures;
  1         4  
  1         6  
7              
8 1     1   251 use Feature::Compat::Try;
  1         1  
  1         10  
9 1     1   66 use OpenTelemetry -all;
  1         1  
  1         16  
10 1     1   1024 use OpenTelemetry::Constants -span;
  1         2  
  1         7  
11 1     1   1086 use Syntax::Keyword::Dynamically;
  1         2  
  1         9  
12              
13 12     12 1 329402 sub register ( $, $app, $config, @ ) {
  12         81  
  12         67  
  12         31  
14 12   33     167 $config->{tracer}{name} //= otel_config('SERVICE_NAME') // $app->moniker;
      33        
15              
16             # First, around_dispatch sets up the span and populates what it can
17 19     19   247868 $app->hook( around_dispatch => sub ( $next, $c ) {
  19         52  
  19         43  
  19         41  
18 19         117 my $tracer = otel_tracer_provider->tracer( %{ $config->{tracer} } );
  19         344  
19              
20 19         729 my $tx = $c->tx;
21 19         146 my $req = $tx->req;
22 19         118 my $url = $req->url->to_abs;
23 19         3962 my $query = $url->query->to_string;
24 19         908 my $method = $req->method;
25 19         139 my $headers = $req->headers;
26 19         244 my $agent = $headers->user_agent;
27              
28             # https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
29 19         176 my $hostport;
30 19 100       85 if ( my $fwd = $headers->header('forwarded') ) {
31 4         61 my ($first) = split ',', $fwd, 2;
32 4 50 33     50 $hostport = $1 // $2 if $first =~ /host=(?:"([^"]+)"|([^;]+))/;
33             }
34              
35 19   66     243 $hostport //= $headers->header('x-forwarded-proto')
      66        
36             // $headers->header('host');
37              
38 19         572 my ( $host, $port ) = $hostport =~ /(.*?)(?::([0-9]+))?$/g;
39              
40             my $context = otel_propagator->extract(
41             $headers,
42             undef,
43 0         0 sub ( $carrier, $key ) { $carrier->header($key) },
44 19         107 );
45              
46 19 50       1493 my $span = $tracer->create_span(
    100          
    50          
    50          
47             name => $method,
48             kind => SPAN_KIND_SERVER,
49             parent => $context,
50             attributes => {
51             'http.request.method' => $method,
52             'network.protocol.version' => $req->version,
53             'url.path' => $url->path->to_string,
54             'url.scheme' => $url->scheme,
55             'client.address' => $tx->remote_address,
56             'client.port' => $tx->remote_port,
57             $host ? ( 'server.address' => $host ) : (),
58             $port ? ( 'server.port' => $port ) : (),
59             $agent ? ( 'user_agent.original' => $agent ) : (),
60             $query ? ( 'url.query' => $query ) : (),
61             },
62             );
63              
64             # dynamically works across Future::AsyncAwait boundaries, so we don't need to
65             # store the span inside the current Mojolicious controller.
66 19         56587 dynamically otel_current_context
67             = otel_context_with_span( $span, $context );
68              
69             # Now that we have a new span/context, we can update Mojolicious's data
70             # for convenience.
71             # This sets the ID that gets logged by the $c->log helper
72 19         2755 $req->request_id( $span->context->hex_span_id );
73              
74             # When the transaction is finished, get the response data
75             # XXX: For websockets, this will be when the websocket closes, which may not
76             # be ideal: It would miss the HTTP handshake 101 response.
77             $c->tx->on(finish => sub ($tx) {
78 19         112 $span->set_attribute('http.response.status_code', $tx->res->code);
79 19         733 $span->end;
80 19         5488 });
81              
82             # around_dispatch handles exceptions
83 19         390 try {
84 19         67 $next->();
85             }
86             catch ($error) {
87 1         573 my ($message) = split /\n/, "$error", 2;
88 1         47 $message =~ s/ at \S+ line \d+\.$//a;
89              
90 1   50     6 $span
91             ->record_exception($error)
92             ->set_status( SPAN_STATUS_ERROR, $message )
93             ->set_attribute(
94             'error.type' => ref $error || 'string',
95             );
96 1         44 die $error;
97             }
98 12         1193 });
99              
100             # around_action fills in more attributes, since it now knows what action is being taken
101 19     19   75807 $app->hook( around_action => sub( $next, $c, $action, $last, @ ) {
  19         42  
  19         36  
  19         35  
  19         33  
  19         28  
102             # We don't check $last because this may still be the last action we do.
103             # If, for example, an intermediate action throws an exception or otherwise
104             # interrupts the dispatch cycle.
105 19         55 my $tx = $c->tx;
106              
107             # Mojolicious normalizes routes to remove the trailing slash, which is fine
108             # until it's the only thing in the route.
109 19   100     98 my $route = $c->match->endpoint->to_string || '/';
110              
111 19         1090 my $span = otel_span_from_context;
112 19         1175 $span->set_name( $tx->req->method . ' ' . $route );
113 19         437 $span->set_attribute( 'http.route' => $route );
114              
115 19         459 $next->();
116 12         242 });
117             }
118              
119             1;