File Coverage

blib/lib/OpenTelemetry/Instrumentation/LWP/UserAgent.pm
Criterion Covered Total %
statement 106 107 99.0
branch 18 20 90.0
condition 6 6 100.0
subroutine 20 20 100.0
pod 2 3 66.6
total 152 156 97.4


line stmt bran cond sub pod time code
1             package OpenTelemetry::Instrumentation::LWP::UserAgent;
2             # ABSTRACT: OpenTelemetry instrumentation for LWP::UserAgent
3              
4             our $VERSION = '0.033';
5              
6 2     2   238322 use strict;
  2         6  
  2         107  
7 2     2   11 use warnings;
  2         4  
  2         110  
8 2     2   376 use experimental 'signatures';
  2         3120  
  2         14  
9              
10 2     2   785 use Class::Inspector;
  2         2412  
  2         67  
11 2     2   404 use Class::Method::Modifiers 'install_modifier';
  2         1383  
  2         132  
12 2     2   399 use Feature::Compat::Try;
  2         301  
  2         12  
13 2     2   484 use Syntax::Keyword::Dynamically;
  2         1992  
  2         13  
14 2     2   150 use List::Util 'none';
  2         4  
  2         169  
15 2     2   459 use OpenTelemetry::Constants qw( SPAN_KIND_CLIENT SPAN_STATUS_ERROR );
  2         5  
  2         15  
16 2     2   2350 use OpenTelemetry::Context;
  2         9  
  2         116  
17 2     2   620 use OpenTelemetry::Trace;
  2         6  
  2         88  
18 2     2   405 use OpenTelemetry;
  2         7  
  2         17  
19              
20 2     2   628 use parent 'OpenTelemetry::Instrumentation';
  2         5  
  2         15  
21              
22 1     1 1 385906 sub dependencies { 'LWP::UserAgent' }
23              
24 11     11   18173 my sub get_headers ( $have, $want, $prefix ) {
  11         27  
  11         21  
  11         26  
  11         20  
25 11 100       97 return unless @$want;
26              
27 3         6 my %attributes;
28             $have->scan( sub ( $key, $value ) {
29 13         26 $key =~ tr/-/_/;
30 13 100       67 return if none { $key =~ $_ } @$want;
  34         184  
31 7   100     24 push @{ $attributes{ $prefix . '.' . lc $key } //= [] }, $value;
  7         59  
32 3         34 });
33              
34 3         52 %attributes;
35             }
36              
37             my ( $original, $loaded );
38              
39 4     4 0 1907 sub uninstall ( $class ) {
  4         16  
  4         9  
40 4 100       20 return unless $loaded;
41 2     2   721 no strict 'refs';
  2         5  
  2         74  
42 2     2   10 no warnings 'redefine';
  2         4  
  2         2891  
43 3         124 delete $Class::Method::Modifiers::MODIFIER_CACHE{'LWP::UserAgent'}{simple_request};
44 3         12 *{'LWP::UserAgent::simple_request'} = $original;
  3         94  
45 3         9 undef $loaded;
46 3         13 return;
47             }
48              
49 5     5 1 1509 sub install ( $class, %config ) {
  5         14  
  5         14  
  5         11  
50 5 50       23 return if $loaded;
51 5 100       42 return unless Class::Inspector->loaded('LWP::UserAgent');
52              
53             my @wanted_request_headers = map qr/^\Q$_\E$/i, map tr/-/_/r,
54 4   100     250 @{ delete $config{request_headers} // [] };
  4         111  
55              
56             my @wanted_response_headers = map qr/^\Q$_\E$/i, map tr/-/_/r,
57 4   100     14 @{ delete $config{response_headers} // [] };
  4         50  
58              
59 4         14 $original = \&LWP::UserAgent::simple_request;
60             install_modifier 'LWP::UserAgent' => around => simple_request => sub {
61 4     4   25372 my ( $code, $self, $request, @rest ) = @_;
62              
63 4         24 my $uri = $request->uri->clone;
64 4         164 my $method = $request->method;
65 4         71 my $length = length $request->content;
66              
67 4 100       101 $uri->userinfo('REDACTED:REDACTED') if $uri->userinfo;
68              
69 4 100       635 my $span = OpenTelemetry->tracer_provider->tracer(
70             name => __PACKAGE__,
71             version => $VERSION,
72             )->create_span(
73             name => $method,
74             kind => SPAN_KIND_CLIENT,
75             attributes => {
76             # As per https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md
77             'http.request.method' => $method,
78             'network.protocol.name' => 'http',
79             'network.protocol.version' => '1.1',
80             'network.transport' => 'tcp',
81             'server.address' => $uri->host,
82             'server.port' => $uri->port,
83             'url.full' => "$uri", # redacted
84             'user_agent.original' => $self->agent,
85              
86             get_headers(
87             $self->default_headers,
88             \@wanted_request_headers,
89             'http.request.header'
90             ),
91              
92             get_headers(
93             $request->headers,
94             \@wanted_request_headers,
95             'http.request.header'
96             ),
97              
98             $length ? ( 'http.request.body.size' => $length ) : (),
99             },
100             );
101              
102 4         17259 dynamically OpenTelemetry::Context->current
103             = OpenTelemetry::Trace->context_with_span($span);
104              
105             OpenTelemetry->propagator->inject(
106             $request,
107             undef,
108 1         33 sub { shift->header(@_) },
109 4         31 );
110              
111 4         33 try {
112 4         23 my $response = $self->$code( $request, @rest );
113 3         545 $span->set_attribute( 'http.response.status_code' => $response->code );
114              
115             # TODO: this should include retries
116 3 50       127 if ( my $count = $response->redirects ) {
117 0         0 $span->set_attribute( 'http.resend_count' => $count )
118             }
119              
120 3         90 my $length = $response->header('content-length');
121 3 100       189 $span->set_attribute( 'http.response.body.size' => $length )
122             if defined $length;
123              
124 3 100       37 $span->set_status( SPAN_STATUS_ERROR, $response->code )
125             unless $response->is_success;
126              
127 3         72 $span->set_attribute(
128             get_headers(
129             $response->headers,
130             \@wanted_response_headers,
131             'http.response.header'
132             )
133             );
134              
135 3         71 return $response;
136             }
137             catch ($error) {
138 1         35 my ($description) = split /\n/, $error =~ s/^\s+|\s+$//gr, 2;
139 1         10 $description =~ s/ at \S+ line \d+\.$//a;
140              
141 1         6 $span->record_exception($error);
142 1         21 $span->set_status( SPAN_STATUS_ERROR, $description );
143              
144 1         19 die $error;
145             }
146             finally {
147 4         16 $span->end;
148             }
149 4         58 };
150              
151 4         2109 return $loaded = 1;
152             }
153              
154             1;