File Coverage

blib/lib/Plack/Middleware/OpenTelemetry.pm
Criterion Covered Total %
statement 95 98 96.9
branch 16 18 88.8
condition 12 14 85.7
subroutine 18 18 100.0
pod 2 3 66.6
total 143 151 94.7


line stmt bran cond sub pod time code
1             package Plack::Middleware::OpenTelemetry;
2             $Plack::Middleware::OpenTelemetry::VERSION = '0.252280';
3             # ABSTRACT: Plack middleware to setup OpenTelemetry tracing
4              
5 3     3   3306208 use v5.30.0;
  3         40  
6 3     3   23 use strict;
  3         5  
  3         99  
7 3     3   15 use warnings;
  3         7  
  3         241  
8 3     3   20 use experimental 'signatures';
  3         8  
  3         30  
9 3     3   698 use parent qw(Plack::Middleware);
  3         7  
  3         29  
10 3     3   1253 use Plack;
  3         438  
  3         135  
11 3     3   21 use Plack::Util::Accessor qw(resource_attributes include_client_errors);
  3         6  
  3         32  
12 3     3   257 use OpenTelemetry -all;
  3         11  
  3         52  
13 3     3   4843 use OpenTelemetry::Constants qw( SPAN_KIND_SERVER SPAN_STATUS_ERROR SPAN_STATUS_OK );
  3         9  
  3         30  
14 3     3   3219 use OpenTelemetry::Common 'config';
  3         7  
  3         250  
15 3     3   21 use Syntax::Keyword::Dynamically;
  3         6  
  3         30  
16 3     3   265 use Feature::Compat::Try;
  3         6  
  3         38  
17 3     3   1119 use URI;
  3         10341  
  3         5774  
18              
19             sub prepare_app {
20 14     14 1 82072 my $self = shift;
21             }
22              
23             sub call {
24 19     19 1 115159 my ($self, $env) = @_;
25              
26 19         95 my $tracer =
27             otel_tracer_provider->tracer(schema_url => 'https://opentelemetry.io/schemas/1.21.0');
28              
29 19         19053 my $method = $env->{REQUEST_METHOD};
30 19   66     156 my $scheme = $env->{HTTP_X_FORWARDED_PROTO} || $env->{"psgi.url_scheme"};
31 19         144 my $url = URI->new($scheme . '://' . $env->{HTTP_HOST} . $env->{REQUEST_URI});
32              
33 39         64 my $context = otel_propagator->extract(
34             $env, undef,
35 39     39   5878 sub ($carrier, $key) {
  39         81  
  39         50  
36 39         150 $carrier->{'HTTP_' . uc $key};
37             },
38 19         18221 );
39              
40 19         194 my $resource;
41 19 100       105 if (my $a = $self->resource_attributes) {
42 1         11 $resource = OpenTelemetry::SDK::Resource->new()
43             ->merge(OpenTelemetry::SDK::Resource->new(empty => 1, attributes => $a));
44             }
45              
46             my $span = $tracer->create_span(
47             name => "$method request",
48              
49             ($resource ? (resource => $resource) : ()),
50              
51             parent => $context,
52             kind => SPAN_KIND_SERVER,
53             attributes => {
54             "plack.version" => "$Plack::VERSION",
55              
56             # https://opentelemetry.io/docs/specs/semconv/http/http-spans/
57             # https://opentelemetry.io/blog/2023/http-conventions-declared-stable/
58             "client.address" => $env->{REMOTE_ADDR},
59             "http.request.method" => $method,
60             "user_agent.original" => ($env->{HTTP_USER_AGENT} || ''),
61             "server.address" => $env->{HTTP_HOST},
62 19 100 100     1911 "url.full" => $url->as_string,
    100          
63             "url.scheme" => $scheme,
64             "url.path" => $url->path,
65             ($url->query ? ("url.query" => $url->query) : ()),
66              
67             # todo: "http.request.body.size"
68              
69             },
70             );
71              
72 19         30568 $context = otel_context_with_span($span, $context);
73 19         1509 dynamically otel_current_context = $context;
74              
75 19         421 try {
76 19         55 my $res = eval { $self->app->($env) };
  19         94  
77              
78 19 100       29704 if ($@) {
79 1         53 warn "request returned: $@";
80 1         7 die $@;
81             }
82              
83 18 100 66     225 if (ref($res) && ref($res) eq 'ARRAY') {
84 17         73 $self->set_status_code($span, $res);
85 17         186 my $content_length = Plack::Util::content_length($res->[2]);
86 17         305 $span->set_attribute("http.response.body.size", $content_length);
87 17         1901 $span->end();
88 17         44624 return $res;
89             }
90              
91             return $self->response_cb(
92             $res,
93             sub {
94 1     1   87 my $res = shift;
95 1         6 $self->set_status_code($span, $res);
96 1         16 $span->set_attribute("plack.callback", "true");
97              
98 1         164 my $content_length = Plack::Util::content_length($res->[2]);
99 1 50       11 if (defined $content_length) {
100 0         0 $span->set_attribute("http.response.body.size", $content_length);
101 0         0 $span->end();
102 0         0 return;
103             }
104              
105 1         3 $content_length = 0;
106              
107             return sub {
108 3         248 my $chunk = shift;
109 3 100       13 unless (defined $chunk) {
110 1         9 $span->set_attribute("http.response.body.size", $content_length);
111 1         171 $span->end();
112             }
113 3         967 $content_length += length($chunk);
114 3         21 return $chunk;
115             }
116 1         9 }
117 1         20 );
118             }
119             catch ($error) {
120 1         30 warn "got request error: $error";
121 1         5 my $message = $error;
122 1         7 $span->record_exception($error)->set_attribute('http.response.status_code' => 500)
123             ->set_status(SPAN_STATUS_ERROR, $message)->end;
124 1         1606 die $error;
125             }
126             }
127              
128 18     18 0 39 sub set_status_code ($self, $span, $res) {
  18         35  
  18         29  
  18         26  
  18         26  
129 18 50       57 my $status_code = $res->[0] or return;
130 18         102 $span->set_attribute("http.response.status_code", $status_code);
131 18 100 100     2716 if ( $status_code >= 400 and $self->include_client_errors
      100        
132             or $status_code >= 500)
133             {
134 2         19 $span->set_status(SPAN_STATUS_ERROR);
135             }
136             }
137              
138             1;
139              
140             =encoding utf8
141              
142             =head1 NAME
143              
144             Plack::Middleware::OpenTelemetry - Plack middleware to setup OpenTelemetry spans
145              
146             =head1 VERSION
147              
148             version 0.252280
149              
150             =head1 SYNOPSIS
151              
152             builder {
153             enable "Plack::Middleware::OpenTelemetry",
154             include_client_errors => 0;
155             };
156              
157             # With custom resource attributes
158             builder {
159             enable "Plack::Middleware::OpenTelemetry",
160             include_client_errors => 1,
161             resource_attributes => {
162             'service.version' => '1.0.0',
163             'deployment.environment' => 'production',
164             };
165             };
166              
167             =head1 DESCRIPTION
168              
169             C will setup an C
170             span for the request.
171              
172             The middleware automatically:
173              
174             =over
175              
176             =item * Creates OpenTelemetry spans for HTTP requests
177              
178             =item * Extracts W3C trace context from incoming requests
179              
180             =item * Sets standard HTTP semantic attributes following OpenTelemetry conventions
181              
182             =item * Handles both synchronous and streaming responses
183              
184             =item * Records exceptions and sets appropriate span status
185              
186             =back
187              
188             The following HTTP attributes are set on spans:
189              
190             =over
191              
192             =item * C - HTTP method
193              
194             =item * C - HTTP status code
195              
196             =item * C - Client IP address
197              
198             =item * C - Server hostname
199              
200             =item * C - Full request URL
201              
202             =item * C - URL scheme (http/https)
203              
204             =item * C - URL path
205              
206             =item * C - URL query string (if present)
207              
208             =item * C - User-Agent header
209              
210             =item * C - Response body size
211              
212             =back
213              
214             =head1 PARAMETERS
215              
216             =over
217              
218             =item include_client_errors
219              
220             By default client errors (HTTP status 400-499) don't set span status to
221             "error". Enable this option to include them as errors.
222              
223             =item resource_attributes
224              
225             Hash reference of custom resource attributes to add to the OpenTelemetry resource.
226             These will be merged with the default resource attributes.
227              
228             Example:
229             enable "Plack::Middleware::OpenTelemetry",
230             resource_attributes => {
231             'service.version' => '1.0.0',
232             'deployment.environment' => 'production',
233             };
234              
235             =back
236              
237             =head1 ENVIRONMENT VARIABLES
238              
239             The middleware respects standard OpenTelemetry environment variables:
240              
241             =over
242              
243             =item OTEL_TRACES_EXPORTER
244              
245             Exporter type (console, otlp, etc.)
246              
247             =item OTEL_SERVICE_NAME
248              
249             Service name for spans
250              
251             =item OTEL_RESOURCE_ATTRIBUTES
252              
253             Additional resource attributes
254              
255             =back
256              
257             =head1 NOTES
258              
259             The L plackup server is recommended:
260             C
261              
262             =head1 SEE ALSO
263              
264             L, L
265              
266             =head1 AUTHOR
267              
268             Ask Bjørn Hansen
269              
270             =head1 COPYRIGHT AND LICENSE
271              
272             This software is copyright (c) 2023 by Ask Bjørn Hansen.
273              
274             This is free software; you can redistribute it and/or modify it under
275             the MIT software license.
276              
277             =cut