Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
426fda5
feat: Add Tracing Middleware
HazAT Jun 23, 2020
0adbc2d
feat: Auto prepend middleware
HazAT Jun 24, 2020
fc60db2
ref: Change code comments
HazAT Jun 24, 2020
7f3968c
ref: Use terminate to send transaction
HazAT Jun 24, 2020
96b2a7d
Rename view engine decorator
stayallive Jun 24, 2020
75458c7
Improve transaction context name/description
stayallive Jun 24, 2020
724d450
Prevent crashes when using missing defines
stayallive Jun 24, 2020
c4cae4a
Do not remove all leading `/` keep 1
stayallive Jun 24, 2020
f9df5c6
Add fallback autoload + bootstrap span
stayallive Jun 24, 2020
bef593a
Set the transaction name from the event handler
stayallive Jun 24, 2020
dce0671
Cleanup query span
stayallive Jun 24, 2020
2a9d46e
Prevent errors on Laravel 5.3 and below
stayallive Jun 24, 2020
3211547
CS
stayallive Jun 24, 2020
c2126e1
feat: Use correct route and add data
HazAT Jun 25, 2020
3655416
ref: Add name
HazAT Jun 25, 2020
85bdee0
fix: Route naming
HazAT Jun 25, 2020
2108830
feat: Start transaction in serviceProvider
HazAT Jun 25, 2020
e161cd8
ref: Rename to view.render
HazAT Jun 25, 2020
7f0aef0
ref: Move back to starting transaction in middleware
HazAT Jun 29, 2020
1e48546
ref: Small refactor
HazAT Jun 29, 2020
745bd72
feat: Update composer.json
HazAT Jul 6, 2020
4b4b025
ref: Add traces_sample_rate
HazAT Jul 6, 2020
e1b1f1c
Refactor active span retrieval
stayallive Jul 20, 2020
4408ad8
Guard against the active span not being set
stayallive Jul 20, 2020
29c3e90
Docblock updates
stayallive Jul 20, 2020
405638a
Correctly return rendered view without span
stayallive Jul 20, 2020
44bc54e
Move tracing related code to the Tracing namespace
stayallive Jul 20, 2020
b7cf7ab
Improve the route name detection order
stayallive Jul 20, 2020
ccd8376
Do not use route names ending with a `.`
stayallive Jul 20, 2020
ef4ca20
Fix not wrapping the view enines when resolver is already resolved
stayallive Jul 21, 2020
bb698cf
feat: Rework code to use transaction on the hub
HazAT Aug 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"require": {
"php": "^7.1",
"illuminate/support": "5.0 - 5.8 | ^6.0 | ^7.0",
"sentry/sdk": "^2.1"
"sentry/sdk": "3.x-dev"
},
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"phpunit/phpunit": "^8.0",
"laravel/framework": "^6.0",
Expand Down Expand Up @@ -53,7 +55,8 @@
},
"laravel": {
"providers": [
"Sentry\\Laravel\\ServiceProvider"
"Sentry\\Laravel\\ServiceProvider",
"Sentry\\Laravel\\Tracing\\ServiceProvider"
],
"aliases": {
"Sentry": "Sentry\\Laravel\\Facade"
Expand Down
1 change: 1 addition & 0 deletions config/sentry.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
// @see: https://docs.sentry.io/error-reporting/configuration/?platform=php#send-default-pii
'send_default_pii' => false,

'traces_sample_rate' => 0,
];
39 changes: 20 additions & 19 deletions src/Sentry/Laravel/EventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
use Illuminate\Queue\QueueManager;
use Illuminate\Routing\Events\RouteMatched;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
use RuntimeException;
use Sentry\Breadcrumb;
use Sentry\SentrySdk;
use Sentry\State\Scope;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\Transaction;

class EventHandler
{
Expand Down Expand Up @@ -194,26 +195,16 @@ public function __call($method, $arguments)
*/
protected function routerMatchedHandler(Route $route)
{
$routeName = null;
$routeName = Integration::extractNameForRoute($route) ?? '<unlabeled transaction>';

if ($route->getName()) {
// someaction (route name/alias)
$routeName = $route->getName();
$transaction = SentrySdk::getCurrentHub()->getTransaction();

// Laravel 7 route caching generates a route names if the user didn't specify one
// theirselfs to optimize route matching. These route names are useless to the
// developer so if we encounter a generated route name we discard the value
if (Str::startsWith($routeName, 'generated::')) {
$routeName = null;
}
}

if (empty($routeName) && $route->getActionName()) {
// SomeController@someAction (controller action)
$routeName = $route->getActionName();
} elseif (empty($routeName) || $routeName === 'Closure') {
// /someaction // Fallback to the url
$routeName = $route->uri();
if ($transaction instanceof Transaction) {
$transaction->setName($routeName);
$transaction->setData([
'action' => $route->getActionName(),
'name' => $route->getName()
]);
}

Integration::addBreadcrumb(new Breadcrumb(
Expand Down Expand Up @@ -287,6 +278,16 @@ private function addQueryBreadcrumb($query, $bindings, $time, $connectionName)
$data['bindings'] = $bindings;
}

$transaction = SentrySdk::getCurrentHub()->getTransaction();
if (null !== $transaction) {
$context = new SpanContext();
$context->op = 'sql.query';
$context->description = $query;
$context->startTimestamp = microtime(true) - $time / 1000;
$context->endTimestamp = $context->startTimestamp + $time / 1000;
$transaction->startChild($context);
}

Integration::addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
Expand Down
83 changes: 81 additions & 2 deletions src/Sentry/Laravel/Integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Sentry\Laravel;

use Illuminate\Routing\Route;
use Illuminate\Support\Str;
use Sentry\FlushableClientInterface;
use Sentry\SentrySdk;
use Sentry\Tracing\Span;
use function Sentry\addBreadcrumb;
use function Sentry\configureScope;
use Sentry\Breadcrumb;
Expand All @@ -30,7 +33,9 @@ public function setupOnce(): void
return $event;
}

$event->setTransaction($self->getTransaction());
if (null === $event->getTransaction()) {
$event->setTransaction($self->getTransaction());
}

return $event;
});
Expand Down Expand Up @@ -71,7 +76,7 @@ public static function configureScope(callable $callback): void
/**
* @return null|string
*/
public static function getTransaction()
public static function getTransaction(): ?string
{
return self::$transaction;
}
Expand All @@ -98,4 +103,78 @@ public static function flushEvents(): void
$client->flush();
}
}

/**
* Extract the readable name for a route.
*
* @param \Illuminate\Routing\Route $route
*
* @return string|null
*/
public static function extractNameForRoute(Route $route): ?string
{
$routeName = null;

if (empty($routeName) && $route->getName()) {
// someaction (route name/alias)
$routeName = $route->getName();

// Laravel 7 route caching generates a route names if the user didn't specify one
// theirselfs to optimize route matching. These route names are useless to the
// developer so if we encounter a generated route name we discard the value
if (Str::startsWith($routeName, 'generated::')) {
$routeName = null;
}

// If the route name ends with a `.` we assume an incomplete group name prefix
// we discard this value since it will most likely not mean anything to the
// developer and will be duplicated by other unnamed routes in the group
if (Str::endsWith($routeName, '.')) {
$routeName = null;
}
}

if (empty($routeName) && $route->getActionName()) {
// SomeController@someAction (controller action)
$routeName = ltrim($route->getActionName(), '\\');
}

if (empty($routeName) || $routeName === 'Closure') {
// /someaction // Fallback to the url
$routeName = '/' . ltrim($route->uri(), '/');
}

return $routeName;
}

/**
* Retrieve the meta tags with tracing information to link this request to front-end requests.
*
* @return string
*/
public static function sentryTracingMeta(): string
{
$span = self::currentTracingSpan();

if ($span === null) {
return '';
}

$content = sprintf('<meta name="sentry-trace" content="%s"/>', $span->toTraceparent());
// $content .= sprintf('<meta name="sentry-trace-data" content="%s"/>', $span->getDescription());

return $content;
}

/**
* Get the current active tracing span from the scope.
*
* @return \Sentry\Tracing\Span|null
*
* @internal This is used internally as an easy way to retrieve the current active tracing span.
*/
public static function currentTracingSpan(): ?Span
{
return SentrySdk::getCurrentHub()->getSpan();
}
}
1 change: 1 addition & 0 deletions src/Sentry/Laravel/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Sentry\Laravel;

use Illuminate\Contracts\Http\Kernel as HttpKernelInterface;
use Sentry\SentrySdk;
use Sentry\State\Hub;
use Sentry\ClientBuilder;
Expand Down
117 changes: 117 additions & 0 deletions src/Sentry/Laravel/Tracing/Middleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Sentry\Laravel\Tracing;

use Closure;
use Illuminate\Http\Request;
use Sentry\SentrySdk;
use Sentry\State\Hub;
use Sentry\State\Scope;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\TransactionContext;

class Middleware
{
/**
* The current active transaction.
*
* @var \Sentry\Tracing\Transaction|null
*/
protected $transaction;

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if (app()->bound('sentry')) {
$this->startTransaction($request, app('sentry'));
}

return $next($request);
}

/**
* Handle the application termination.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
*
* @return void
*/
public function terminate($request, $response): void
{
if ($this->transaction !== null && app()->bound('sentry')) {
$this->transaction->finish();
}
}

private function startTransaction(Request $request, Hub $sentry): void
{
$path = '/' . ltrim($request->path(), '/');
$fallbackTime = microtime(true);
$sentryTraceHeader = $request->header('sentry-trace');

$context = $sentryTraceHeader
? TransactionContext::fromTraceparent($sentryTraceHeader)
: new TransactionContext;

$context->op = 'http.server';
$context->name = $path;
$context->data = [
'url' => $path,
'method' => strtoupper($request->method()),
];
$context->startTimestamp = $request->server('REQUEST_TIME_FLOAT', $fallbackTime);

$this->transaction = $sentry->startTransaction($context);

// Setting the Transaction on the Hub
SentrySdk::getCurrentHub()->setSpan($this->transaction);

if (!$this->addBootTimeSpans()) {
// @TODO: We might want to move this together with the `RouteMatches` listener to some central place and or do this from the `EventHandler`
app()->booted(function () use ($request, $fallbackTime): void {
$spanContextStart = new SpanContext();
$spanContextStart->op = 'app.bootstrap';
$spanContextStart->startTimestamp = defined('LARAVEL_START') ? LARAVEL_START : $request->server('REQUEST_TIME_FLOAT', $fallbackTime);
$spanContextStart->endTimestamp = microtime(true);
$this->transaction->startChild($spanContextStart);
});
}
}

private function addBootTimeSpans(): bool
{
if (!defined('LARAVEL_START') || !LARAVEL_START) {
return false;
}

if (!defined('SENTRY_AUTOLOAD') || !SENTRY_AUTOLOAD) {
return false;
}

if (!defined('SENTRY_BOOTSTRAP') || !SENTRY_BOOTSTRAP) {
return false;
}

$spanContextStart = new SpanContext();
$spanContextStart->op = 'autoload';
$spanContextStart->startTimestamp = LARAVEL_START;
$spanContextStart->endTimestamp = SENTRY_AUTOLOAD;
$this->transaction->startChild($spanContextStart);

$spanContextStart = new SpanContext();
$spanContextStart->op = 'bootstrap';
$spanContextStart->startTimestamp = SENTRY_AUTOLOAD;
$spanContextStart->endTimestamp = SENTRY_BOOTSTRAP;
$this->transaction->startChild($spanContextStart);

return true;
}
}
63 changes: 63 additions & 0 deletions src/Sentry/Laravel/Tracing/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Sentry\Laravel\Tracing;

use Illuminate\Contracts\Http\Kernel as HttpKernelInterface;
use Illuminate\Contracts\View\Engine;
use Illuminate\Contracts\View\View;
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
use Illuminate\View\Engines\EngineResolver;
use Illuminate\View\Factory as ViewFactory;

class ServiceProvider extends IlluminateServiceProvider
{
public function boot(): void
{
if ($this->app->bound(HttpKernelInterface::class)) {
/** @var \Illuminate\Contracts\Http\Kernel $httpKernel */
$httpKernel = $this->app->make(HttpKernelInterface::class);

$httpKernel->prependMiddleware(Middleware::class);
}
}

public function register(): void
{
$this->app->singleton(Middleware::class);

$viewEngineWrapper = function (EngineResolver $engineResolver): void {
foreach (['file', 'php', 'blade'] as $engineName) {
try {
$realEngine = $engineResolver->resolve($engineName);

$engineResolver->register($engineName, function () use ($realEngine) {
return $this->wrapViewEngine($realEngine);
});
} catch (InvalidArgumentException $e) {
// The `file` engine was introduced in Laravel 5.4. On lower Laravel versions
// resolving that driver will throw an `InvalidArgumentException`. We can
// ignore this exception because we can't wrap drivers that don't exist
}
}
};

if ($this->app->resolved('view.engine.resolver')) {
$viewEngineWrapper($this->app->make('view.engine.resolver'));
} else {
$this->app->afterResolving('view.engine.resolver', $viewEngineWrapper);
}
}

private function wrapViewEngine(Engine $realEngine): Engine
{
/** @var ViewFactory $viewFactory */
$viewFactory = $this->app->make('view');

/** @noinspection UnusedFunctionResultInspection */
$viewFactory->composer('*', static function (View $view) use ($viewFactory) : void {
$viewFactory->share(ViewEngineDecorator::SHARED_KEY, $view->name());
});

return new ViewEngineDecorator($realEngine, $viewFactory);
}
}
Loading