File Coverage

blib/lib/Mojolicious/Plugin/SlapbirdAPM.pm
Criterion Covered Total %
statement 48 130 36.9
branch 0 30 0.0
condition 0 10 0.0
subroutine 16 27 59.2
pod 1 1 100.0
total 65 198 32.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::SlapbirdAPM;
2              
3 1     1   135178 use strict;
  1         25  
  1         43  
4 1     1   6 use warnings;
  1         2  
  1         78  
5              
6 1     1   816 use Mojo::Base 'Mojolicious::Plugin';
  1         14378  
  1         5  
7 1     1   2212 use Mojo::UserAgent;
  1         494674  
  1         13  
8 1     1   58 use Mojo::IOLoop;
  1         2  
  1         8  
9 1     1   38 use Time::HiRes qw(time);
  1         2  
  1         11  
10 1     1   827 use Try::Tiny;
  1         1720  
  1         77  
11 1     1   647 use Const::Fast;
  1         2729  
  1         6  
12 1     1   64 use Carp;
  1         2  
  1         38  
13 1     1   386 use IPC::Open2;
  1         2893  
  1         73  
14 1     1   442 use SlapbirdAPM::Trace;
  1         3  
  1         29  
15 1     1   524 use System::Info;
  1         18035  
  1         57  
16 1     1   748 use SlapbirdAPM::DBIx::Tracer;
  1         5  
  1         54  
17 1     1   9 use POSIX ();
  1         2  
  1         38  
18 1     1   819 use namespace::clean;
  1         18780  
  1         8  
19              
20             $Carp::Internal{__PACKAGE__} = 1;
21              
22             const my $SLAPBIRD_APM_URI => $ENV{SLAPBIRD_APM_DEV}
23             ? $ENV{SLAPBIRD_APM_URI} . '/apm'
24             : 'https://slapbirdapm.com/apm';
25             const my $SLAPBIRD_APM_NAME_URI => $ENV{SLAPBIRD_APM_DEV}
26             ? $ENV{SLAPBIRD_APM_URI} . '/apm/name'
27             : 'https://slapbirdapm.com/apm/name';
28             const my $UA => Mojo::UserAgent->new();
29              
30             my $should_request = 1;
31             my $next_timestamp;
32             my $in_request = 0;
33              
34             sub _call_home {
35 0     0     my ( $json, $key, $app, $quiet ) = @_;
36             try {
37 0     0     my $result = $UA->post(
38             $SLAPBIRD_APM_URI,
39             { 'x-slapbird-apm' => $key },
40             json => $json
41             )->result;
42 0 0         if ( !$result->is_success ) {
43 0 0         if ( $result->code eq 429 ) {
44 0           $should_request = 0;
45 0           my $t = time;
46 0           $next_timestamp = $t + ( 86400 - $t );
47 0 0         $app->log->warn(
48             "You've hit your maximum number of requests for today. Please visit slapbirdapm.com to upgrade your plan."
49             ) unless $quiet;
50 0           return;
51             }
52             }
53             }
54             catch {
55             $app->log->warn(
56             'Unable to communicate with Slapbird, this request has not been tracked: '
57             . $json->{request_id}
58 0     0     . ' got error '
59             . shift );
60              
61             }
62 0           }
63              
64             {
65              
66             package Mojolicious::Plugin::SlapbirdAPM::Tracer;
67 1     1   1021 use Time::HiRes qw(time);
  1         2  
  1         11  
68              
69             sub new {
70 0     0     my ( $class, %args ) = @_;
71 0           return bless \%args, $class;
72             }
73              
74             sub DESTROY {
75 0     0     my ($self) = @_;
76 0           my $stack = delete $self->{stack};
77 0           push @$stack, { %$self, end_time => time * 1_000 };
78             }
79              
80             1;
81             }
82              
83             sub register {
84 0     0 1   my ( $self, $app, $conf ) = @_;
85 0   0       my $key = $conf->{key} // $ENV{SLAPBIRDAPM_API_KEY};
86 0 0         my $topology = exists $conf->{topology} ? $conf->{topology} : 1;
87 0           my $ignored_headers = $conf->{ignored_headers};
88 0           my $no_trace = $conf->{no_trace};
89 0           my $quiet = $conf->{quiet};
90 0           my $stack = [];
91              
92 0 0         Carp::croak(
93             'Please provide your SlapbirdAPM key via the SLAPBIRDAPM_API_KEY env variable, or as part of the plugin declaration'
94             ) if !$key;
95              
96             $app->routes->get(
97 0     0     '/slapbird/health_check' => sub { shift->render( text => 'OK' ) } );
  0            
98              
99             $app->hook(
100             around_dispatch => sub {
101 0     0     my ( $next, $c ) = @_;
102              
103 0 0 0       if ( $next_timestamp && ( $next_timestamp >= time ) ) {
104 0           $should_request = 1;
105 0           undef $next_timestamp;
106             }
107              
108 0 0         if ( !$should_request ) {
109 0           $in_request = 0;
110 0           $next->();
111 0           return 1;
112             }
113              
114 0           my $start_time = time * 1_000;
115 0           my $controller_name = ref($c);
116 0           my $error;
117              
118 0           $in_request = 1;
119              
120 0           $stack = [];
121 0           my $queries = [];
122              
123             try {
124             my $tracer = SlapbirdAPM::DBIx::Tracer->new(
125             sub {
126 0           my %args = @_;
127 0 0         if ($in_request) {
128             push @$queries,
129 0           { sql => $args{sql}, total_time => $args{time} };
130             }
131             }
132 0           );
133              
134 0           $next->();
135             }
136             catch {
137 0           $error = $_;
138 0           };
139              
140 0           my $end_time = time * 1_000;
141              
142 0           my $pid = fork();
143 0 0         return 1 if $pid;
144              
145 0           my $response_headers = $c->res->headers->to_hash;
146 0           my $request_headers = $c->req->headers->to_hash;
147              
148 0           for (@$ignored_headers) {
149 0           delete $response_headers->{$_};
150 0           delete $request_headers->{$_};
151             }
152              
153             _call_home(
154             {
155 0 0 0       type => 'mojo',
156             method => $c->req->method,
157             end_point => $c->req->url->to_abs->path,
158             start_time => $start_time,
159             end_time => $end_time,
160             response_code => $c->res->code ? $c->res->code : 500,
161             response_size => $c->res->headers->content_length,
162             response_headers => $c->res->headers->to_hash,
163             request_id => $c->req->request_id,
164             request_size => $c->req->headers->content_length,
165             request_headers => $c->req->headers->to_hash,
166             error => $error,
167             requestor => $c->req->headers->header('x-slapbird-name')
168             // 'UNKNOWN',
169             handler => $controller_name,
170             stack => $stack,
171             os => System::Info->new->os,
172             queries => $queries,
173             num_queries => scalar @$queries
174             },
175             $key, $app, $quiet
176             );
177              
178 0           $in_request = 0;
179              
180 0 0         die $error if $error;
181              
182 0           return POSIX::_exit(0);
183             }
184 0           );
185              
186 0           my $name;
187             try {
188 0     0     my $result =
189             Mojo::UserAgent->new->get(
190             $SLAPBIRD_APM_NAME_URI => { 'x-slapbird-apm' => $key } )->result();
191              
192 0 0         Carp::croak('API key invalid!') if ( !$result->is_success );
193              
194 0           $name = $result->json()->{name};
195              
196             # _enable_mojo_ua_tracking($name) if $topology;
197             }
198             catch {
199 0     0     chomp( my $msg = '' . $_ );
200 0 0         $app->log->warn(
201             'Unable to communicate with slapbird for service name. Service topology will not work for this application: '
202             . $msg )
203             if $topology;
204 0           };
205              
206 0 0         return if $no_trace;
207              
208             $app->hook(
209             before_server_start => sub {
210 0     0     $Carp::Verbose = 1;
211             SlapbirdAPM::Trace->callback(
212             sub {
213 0           my ( $name, $args, $sub ) = @_;
214              
215 0 0         if ( !$in_request ) {
216 0           return $sub->(@$args);
217             }
218              
219 0           my $tracer = Mojolicious::Plugin::SlapbirdAPM::Tracer->new(
220             name => $name,
221             start_time => time * 1_000,
222             stack => $stack
223             );
224              
225             try {
226 0           return $sub->(@$args);
227             }
228             catch {
229 0           Carp::croak($_);
230 0           };
231             }
232 0           );
233              
234             my @modules = (
235             qw(
236             Mojolicious Mojolicious::Controller Mojo::UserAgent
237             Mojo::Base Mojo::File Mojo::Exception Mojo::IOLoop
238             Mojo::Pg Mojo::mysql Mojo::SQLite Mojo::JSON
239             Mojo::Server DBI DBI::db DBI::st DBI::DBD DBD::Pg DBD::mysql DBIx::Classs
240             DBIx::Class::ResultSet DBIx::Class::Result
241 0   0       ), @{ $conf->{trace_modules} // [] }
  0            
242             );
243              
244 0           SlapbirdAPM::Trace->trace_pkgs(@modules);
245             }
246 0           );
247              
248 0           $app->log->info('Slapbird configured and active on this application.');
249             }
250              
251             1;