diff --git a/.gitignore b/.gitignore index fde868e..f3ec038 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.phar composer.lock /vendor/ +/.idea diff --git a/src/Providers/ElasticApmServiceProvider.php b/src/Providers/ElasticApmServiceProvider.php index 75f9c77..184f2c7 100644 --- a/src/Providers/ElasticApmServiceProvider.php +++ b/src/Providers/ElasticApmServiceProvider.php @@ -4,7 +4,11 @@ use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise\PromiseInterface; +use Illuminate\Database\Events\ConnectionEvent; use Illuminate\Database\Events\QueryExecuted; +use Illuminate\Database\Events\TransactionBeginning; +use Illuminate\Database\Events\TransactionCommitted; +use Illuminate\Database\Events\TransactionRolledBack; use Illuminate\Redis\Events\CommandExecuted; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; @@ -32,6 +36,9 @@ class ElasticApmServiceProvider extends ServiceProvider /** @var bool */ private static $isSampled = true; + // We need to save an array of transaction starts because we need to handle nested transactions + private static $dbTransactionStartsByDB = []; + /** * Bootstrap the application services. * @@ -47,6 +54,7 @@ public function boot() if (config('elastic-apm.active') === true && config('elastic-apm.spans.querylog.enabled') !== false && self::$isSampled) { $this->listenForQueries(); + $this->listenForTransactions(); $this->listenForRedisCommands(); } } @@ -243,6 +251,44 @@ protected function listenForQueries() }); } + protected function listenForTransactions() + { + $this->app->events->listen(TransactionBeginning::class, function (TransactionBeginning $transactionBeginning) { + self::$dbTransactionStartsByDB[$transactionBeginning->connection->getDatabaseName()][] = microtime(true); + }); + $this->app->events->listen([TransactionCommitted::class, TransactionRolledBack::class], function ( + ConnectionEvent $connectionEvent + ) { + $dbName = $connectionEvent->connection->getDatabaseName(); + $transactionStart = array_pop(self::$dbTransactionStartsByDB[$dbName]); + + $stackTrace = $this->getStackTrace(); + + // @see https://www.elastic.co/guide/en/apm/server/master/span-api.html + $query = [ + 'name' => $connectionEvent instanceof TransactionCommitted ? 'TRANSACTION COMMIT' : 'TRANSACTION ROLLBACK', + 'action' => 'connection', + 'type' => 'db', + 'subtype' => 'mysql', + + 'start' => $transactionStart, + 'duration' => round((microtime(true) - $transactionStart) * 1000, 3), + 'stacktrace' => $stackTrace, + + // @see https://github.com/elastic/apm-server/blob/master/docs/fields.asciidoc#apm-span-fields + 'context' => [ + 'db' => [ + 'instance' => $dbName, + 'type' => 'sql', + 'user' => $connectionEvent->connection->getConfig('username'), + ], + ], + ]; + + app('apm-spans-log')->push($query); + }); + } + protected function listenForRedisCommands() { Redis::enableEvents(); @@ -272,52 +318,52 @@ protected function listenForRedisCommands() }); } - public static function getGuzzleMiddleware() : callable - { - return Middleware::tap( - function(RequestInterface $request, array $options) { - self::$lastHttpRequestStart = microtime(true); - }, - function (RequestInterface $request, array $options, PromiseInterface $promise) { - // leave early if monitoring is disabled or when this transaction is not sampled - if (config('elastic-apm.active') !== true || config('elastic-apm.spans.httplog.enabled') !== true || !self::$isSampled) { - return; - } - - /* @var $response \GuzzleHttp\Psr7\Response */ - try { - $response = $promise->wait(true); - } - catch (RequestException $ex) { - $response = $ex->getResponse(); - } - - $requestTime = (microtime(true) - self::$lastHttpRequestStart) * 1000; // in miliseconds - - $method = $request->getMethod(); - $host = $request->getUri()->getHost(); - - $requestEntry = [ - // e.g. GET foo.example.net - 'name' => "{$method} {$host}", - 'type' => 'external', - 'subtype' => 'http', - - 'start' => round(microtime(true) - $requestTime / 1000, 3), - 'duration' => round($requestTime, 3), - - 'context' => [ - "http" => [ - // https://www.elastic.co/guide/en/apm/server/current/span-api.html - "method" => $request->getMethod(), - "url" => $request->getUri()->__toString(), - 'status_code' => $response ? $response->getStatusCode() : 0, - ] - ] - ]; - - app('apm-spans-log')->push($requestEntry); - } - ); - } + public static function getGuzzleMiddleware() : callable + { + return Middleware::tap( + function(RequestInterface $request, array $options) { + self::$lastHttpRequestStart = microtime(true); + }, + function (RequestInterface $request, array $options, PromiseInterface $promise) { + // leave early if monitoring is disabled or when this transaction is not sampled + if (config('elastic-apm.active') !== true || config('elastic-apm.spans.httplog.enabled') !== true || !self::$isSampled) { + return; + } + + /* @var $response \GuzzleHttp\Psr7\Response */ + try { + $response = $promise->wait(true); + } + catch (RequestException $ex) { + $response = $ex->getResponse(); + } + + $requestTime = (microtime(true) - self::$lastHttpRequestStart) * 1000; // in miliseconds + + $method = $request->getMethod(); + $host = $request->getUri()->getHost(); + + $requestEntry = [ + // e.g. GET foo.example.net + 'name' => "{$method} {$host}", + 'type' => 'external', + 'subtype' => 'http', + + 'start' => round(microtime(true) - $requestTime / 1000, 3), + 'duration' => round($requestTime, 3), + + 'context' => [ + "http" => [ + // https://www.elastic.co/guide/en/apm/server/current/span-api.html + "method" => $request->getMethod(), + "url" => $request->getUri()->__toString(), + 'status_code' => $response ? $response->getStatusCode() : 0, + ] + ] + ]; + + app('apm-spans-log')->push($requestEntry); + } + ); + } }