File Coverage

blib/lib/Dancer2/Plugin/SlapbirdAPM.pm
Criterion Covered Total %
statement 49 138 35.5
branch 1 24 4.1
condition 0 2 0.0
subroutine 16 28 57.1
pod 0 1 0.0
total 66 193 34.2


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::SlapbirdAPM;
2              
3 1     1   1252875 use strict;
  1         3  
  1         42  
4 1     1   6 use warnings;
  1         2  
  1         63  
5              
6 1     1   1025 use LWP::UserAgent ();
  1         37593  
  1         25  
7 1     1   468 use Const::Fast qw(const);
  1         2079  
  1         7  
8 1     1   500 use SlapbirdAPM::Dancer2::Trace ();
  1         3  
  1         21  
9 1     1   5 use Time::HiRes qw(time);
  1         1  
  1         8  
10 1     1   80 use Try::Tiny;
  1         3  
  1         50  
11 1     1   4 use JSON::MaybeXS ();
  1         2  
  1         10  
12 1     1   561 use Dancer2::Plugin;
  1         11949  
  1         7  
13 1     1   2787 use LWP::UserAgent;
  1         1  
  1         34  
14 1     1   501 use System::Info;
  1         31095  
  1         76  
15 1     1   997 use SlapbirdAPM::Dancer2::DBIx::Tracer;
  1         4  
  1         43  
16 1     1   9 use POSIX ();
  1         2  
  1         55  
17 1     1   8 use feature 'say';
  1         2  
  1         775  
18              
19             our $VERSION = $SlapbirdAPM::Agent::Dancer2::VERSION;
20              
21             $Carp::Internal{__PACKAGE__} = 1;
22              
23             const my $OS => System::Info->new->os;
24              
25             const my $SLAPBIRD_APM_URI => $ENV{SLAPBIRD_APM_DEV}
26             ? $ENV{SLAPBIRD_APM_URI} . '/apm'
27             : 'https://slapbirdapm.com/apm';
28              
29             has key => (
30             is => 'ro',
31             default => sub { $_[0]->config->{key} // $ENV{SLAPBIRDAPM_API_KEY} }
32             );
33              
34             has topology => (
35             is => 'ro',
36             default => sub { $_[0]->config->{topology} // 1 }
37             );
38              
39             has quiet => (
40             is => 'ro',
41             default => sub { $_[0]->config->{quiet} // 0 }
42             );
43              
44             has trace => (
45             is => 'ro',
46             default => sub { $_[0]->config->{trace} // 1 }
47             );
48              
49             has ignored_headers => (
50             is => 'ro',
51             default => sub { $_[0]->config->{ignored_headers} // [] }
52             );
53              
54             has trace_modules => (
55             is => 'ro',
56             default => sub { $_[0]->config->{trace_modules} // [] }
57             );
58              
59             has _ua => (
60             is => 'ro',
61             default => sub { return LWP::UserAgent->new( timeout => 5 ) }
62             );
63              
64             my $queries = [];
65             my $in_request = 0;
66             my $should_request = 0;
67              
68             SlapbirdAPM::Dancer2::DBIx::Tracer->trace(
69             sub {
70             my %args = @_;
71             if ($in_request) {
72             push @$queries, { sql => $args{sql}, total_time => $args{time} };
73             }
74             }
75             );
76              
77             {
78              
79             package Dancer2::Plugin::SlapbirdAPM::Tracer;
80              
81 1     1   8 use Time::HiRes qw(time);
  1         2  
  1         7  
82              
83             sub new {
84 0     0   0 my ( $class, %args ) = @_;
85 0         0 return bless \%args, $class;
86             }
87              
88             sub DESTROY {
89 0     0   0 my ($self) = @_;
90 0         0 my $stack = delete $self->{stack};
91 0         0 push @$stack, { %$self, end_time => time * 1_000 };
92             }
93              
94             1;
95             }
96              
97             sub _unfold_headers {
98 0     0   0 my ( $self, $headers ) = @_;
99 0         0 $headers->remove_header( $self->ignored_headers->@* );
100 0         0 my %headers = ( $headers->psgi_flatten->@* );
101 0         0 return \%headers;
102             }
103              
104             sub _call_home {
105 0     0   0 my ( $self, $dancer2_request, $dancer2_response, $start_time,
106             $end_time, $stack, $error )
107             = @_;
108              
109 0         0 my $pid = fork();
110 0 0       0 return if $pid;
111              
112             try {
113 0 0   0   0 if ( ref($dancer2_response) eq 'Dancer2::Core::Response::Delayed' ) {
114 0         0 $dancer2_response->to_psgi;
115 0         0 $dancer2_response = $dancer2_response->response;
116             }
117              
118 0         0 my %response;
119 0         0 $response{type} = 'dancer2';
120 0         0 $response{method} = $dancer2_request->method;
121 0         0 $response{end_point} = $dancer2_request->path;
122 0         0 $response{start_time} = $start_time;
123 0         0 $response{end_time} = $end_time;
124 0         0 $response{response_code} = $dancer2_response->status;
125             $response{response_headers} =
126 0         0 $self->_unfold_headers( $dancer2_response->headers );
127 0         0 $response{response_size} = $dancer2_response->header('Content-Length');
128 0         0 $response{request_id} = undef;
129 0         0 $response{request_size} = $dancer2_request->header('Content-Length');
130             $response{request_headers} =
131 0         0 $self->_unfold_headers( $dancer2_request->headers );
132 0         0 $response{error} = $error;
133 0   0     0 $response{error} //= undef;
134 0         0 $response{os} = $OS;
135 0         0 $response{requestor} = $dancer2_request->header('x-slapbird-name');
136 0         0 $response{handler} = undef;
137 0         0 $response{stack} = $stack;
138 0         0 $response{num_queries} = scalar @$queries;
139 0         0 $response{queries} = $queries;
140              
141 0         0 my $ua = LWP::UserAgent->new();
142 0         0 my $slapbird_response = $ua->post(
143             $SLAPBIRD_APM_URI,
144             'Content-Type' => 'application/json',
145             'x-slapbird-apm' => $self->key,
146             Content => JSON::MaybeXS::encode_json( \%response )
147             );
148              
149 0 0       0 if ( !$slapbird_response->is_success ) {
150 0 0       0 if ( $slapbird_response->code eq 429 ) {
151 0 0       0 say STDERR
152             "You've hit your maximum number of requests for today. Please visit slapbirdapm.com to upgrade your plan."
153             unless $self->quiet;
154 0         0 exit 0;
155             }
156 0 0       0 say STDERR
157             'Unable to communicate with Slapbird, this request has not been tracked got status code '
158             . $slapbird_response->code
159             unless $self->quiet;
160             }
161              
162             }
163             catch {
164 0 0   0   0 say STDERR
165             'Unable to communicate with Slapbird, this request has not been tracked got error: '
166             . $_
167             unless $self->quiet;
168 0         0 exit 0;
169 0         0 };
170              
171             # We need to use POSIX::_exit(0) to not destroy database handles from the parent.
172 0         0 return POSIX::_exit(0);
173             }
174              
175             sub BUILD {
176 1     1 0 146 my ($self) = @_;
177              
178 1 50       11 if ( not defined $self->key ) {
179 1         41 say STDERR
180             'No SlapbirdAPM API key set, set the SLAPBIRDAPM_API_KEY environment variable, or set key in the plugin properties';
181 1         13 return;
182             }
183              
184 0           my $stack = [];
185              
186 0 0         if ( $self->trace ) {
187             SlapbirdAPM::Dancer2::Trace->callback(
188             sub {
189 0     0     my %args = @_;
190              
191 0           my $name = $args{name};
192 0           my $sub = $args{sub};
193 0           my $args = $args{args};
194              
195 0 0         if ( !$in_request ) {
196 0           return $sub->(@$args);
197             }
198              
199 0           my $tracer = Dancer2::Plugin::SlapbirdAPM::Tracer->new(
200             name => $name,
201             start_time => time * 1_000,
202             stack => $stack
203             );
204              
205             try {
206 0           return $sub->(@$args);
207             }
208             catch {
209 0           Carp::croak($_);
210 0           };
211             }
212 0           );
213              
214             my @modules = (
215             qw(Dancer2 Dancer2::Core Dancer2::Core::App),
216 0           @{ $self->trace_modules }
  0            
217             );
218              
219 0           SlapbirdAPM::Dancer2::Trace->trace_pkgs(@modules);
220             }
221              
222 0           my $request;
223             my $start_time;
224 0           my $end_time;
225 0           my $error;
226             $self->app->add_hook(
227             Dancer2::Core::Hook->new(
228             name => 'before',
229             code => sub {
230 0     0     $start_time = time * 1_000;
231 0           my ($app) = @_;
232 0           $in_request = 1;
233 0           $stack = [];
234 0           $queries = [];
235 0           $request = $app->request;
236             }
237             )
238 0           );
239              
240             $self->app->add_hook(
241             Dancer2::Core::Hook->new(
242             name => 'on_route_exception',
243             code => sub {
244 0     0     ( undef, $error ) = @_;
245             }
246             )
247 0           );
248              
249             $self->app->add_hook(
250             Dancer2::Core::Hook->new(
251             name => 'before_error',
252             code => sub {
253 0     0     $error = shift->message;
254             }
255             )
256 0           );
257              
258             $self->app->add_hook(
259             Dancer2::Core::Hook->new(
260             name => 'after_error',
261             code => sub {
262 0 0   0     return unless $in_request;
263 0           $end_time = time * 1_000;
264 0           my ($response) = @_;
265 0           $self->_call_home(
266             $request, $response, $start_time,
267             $end_time, $stack, $error
268             );
269 0           $in_request = 0;
270             }
271             )
272 0           );
273              
274             $self->app->add_hook(
275             Dancer2::Core::Hook->new(
276             name => 'after',
277             code => sub {
278 0 0   0     return unless $in_request;
279 0           $end_time = time * 1_000;
280 0           my ($response) = @_;
281 0           $self->_call_home( $request, $response, $start_time,
282             $end_time, $stack, undef );
283 0           $in_request = 0;
284             }
285             )
286 0           );
287              
288 0           return $self;
289             }
290              
291             1;