SlapbirdAPM-Agent-Plack/lib/Plack/Middleware/SlapbirdAPM.pm
package Plack::Middleware::SlapbirdAPM;
use strict;
use warnings;
use parent qw( Plack::Middleware );
use Time::HiRes;
use Try::Tiny;
use Plack::Request;
use Plack::Response;
use Const::Fast;
use JSON::MaybeXS;
use LWP::UserAgent;
use Carp ();
use System::Info;
use Time::HiRes qw(time);
use SlapbirdAPM::Plack::DBIx::Tracer;
use POSIX ();
use namespace::clean;
$Carp::Internal{__PACKAGE__} = 1;
use Plack::Util::Accessor qw(key quiet ignored_headers);
const my $SLAPBIRD_APM_URI => $ENV{SLAPBIRD_APM_DEV}
? $ENV{SLAPBIRD_APM_URI} . '/apm'
: 'https://slapbirdapm.com/apm';
const my $OS => System::Info->new->os;
sub _unfold_headers {
my ( $self, $headers ) = @_;
my %headers = (@$headers);
if ( $self->ignored_headers && ref( $self->ignored_headers ) eq 'ARRAY' ) {
delete $headers{$_} for ( @{ $self->ignored_headers } );
}
return \%headers;
}
sub _call_home {
my ( $self, $request, $response, $env, $start_time, $end_time, $queries,
$error )
= @_;
my $pid = fork();
return if $pid;
try {
my %response;
$response{type} = 'plack';
$response{method} = $request->method;
$response{end_point} = $request->uri->path;
$response{start_time} = $start_time;
$response{end_time} = $end_time;
$response{response_code} = $response->status;
$response{response_headers} =
$self->_unfold_headers(
$response->headers->psgi_flatten_without_sort() );
$response{response_size} = $response->content_length;
$response{request_id} = undef;
$response{request_size} = $request->content_length;
$response{request_headers} =
$self->_unfold_headers(
$request->headers->psgi_flatten_without_sort() );
$response{error} = $error;
$response{os} = $OS;
$response{requestor} = $request->header('x-slapbird-name');
$response{num_queries} = scalar @$queries;
$response{queries} = $queries;
$response{handler} = undef;
my $ua = LWP::UserAgent->new();
my $slapbird_response;
$slapbird_response = $ua->post(
$SLAPBIRD_APM_URI,
'Content-Type' => 'application/json',
'x-slapbird-apm' => $self->key,
Content => encode_json( \%response )
);
if ( !$slapbird_response->is_success ) {
if ( $slapbird_response->code eq 429 ) {
Carp::carp(
"You've hit your maximum number of requests for today. Please visit slapbirdapm.com to upgrade your plan."
) unless $self->quiet;
}
Carp::carp(
'Unable to communicate with Slapbird, this request has not been tracked got status code '
. $slapbird_response->code );
}
}
catch {
Carp::carp(
'Unable to communicate with Slapbird, this request has not been tracked got error: '
. $_ );
POSIX::_exit(0);
};
# We have to use POSIX::_exit(0) instead of exit(0) to not destroy database handles.
return POSIX::_exit(0);
}
sub call {
my ( $self, $env ) = @_;
$self->{key} //= $ENV{SLAPBIRDAPM_API_KEY};
if ( !$self->key ) {
Carp::carp(
'SlapbirdAPM key not set, cannot communicate with SlapbirdAPM. Pass key => "MY KEY", or set the SLAPBIRDAPM_API_KEY environment variable.'
);
return $self->app->($env);
}
my $request = Plack::Request->new($env);
return [ 200, [ 'Content-Type' => 'text/plain' ], 'OK' ]
if $request->uri->path eq '/slapbird/health_check/'
|| $request->uri->path eq '/slapbird/health_check';
my $start_time = time * 1_000;
my $error;
my $response;
my $plack_response;
my $queries = [];
my $dbi_tracer = SlapbirdAPM::Plack::DBIx::Tracer->new(
sub {
my %args = @_;
push @$queries, { sql => $args{sql}, total_time => $args{time} };
}
);
try {
$plack_response = $self->app->($env)
}
catch {
$error = $_;
};
my $end_time = time * 1_000;
try {
if ( ref($plack_response) && ref($plack_response) eq 'ARRAY' ) {
$response = Plack::Response->new(@$plack_response);
$self->_call_home( $request, $response,
$env, $start_time, $end_time, $queries, $error );
return $response->finalize;
}
else {
return $self->response_cb(
$plack_response,
sub {
my $res = shift;
$response = Plack::Response->new(@$res);
$self->_call_home( $request, $response,
$env, $start_time, $end_time, $queries, $error );
}
);
}
if ($error) {
Carp::croak($error);
}
}
catch {
Carp::croak($_);
};
}
1;