Skip to content

Commit

Permalink
Merge pull request #8 from bethinkpl/IT-2387-listen-transactions
Browse files Browse the repository at this point in the history
IT-2387 | Listen for DB Transactions
  • Loading branch information
rogatty authored Apr 8, 2020
2 parents 17c5ada + 6274bdf commit 6bebef1
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
composer.phar
composer.lock
/vendor/
/.idea
142 changes: 94 additions & 48 deletions src/Providers/ElasticApmServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
);
}
}

0 comments on commit 6bebef1

Please sign in to comment.