Skip to content

Commit 69b666a

Browse files
HazATstayallive
andauthored
feat: Add Tracing (#358)
* feat: Add Tracing Middleware Record sql queries as spans * feat: Auto prepend middleware Instrument view renders * ref: Change code comments * ref: Use terminate to send transaction * Rename view engine decorator * Improve transaction context name/description * Prevent crashes when using missing defines * Do not remove all leading `/` keep 1 * Add fallback autoload + bootstrap span * Set the transaction name from the event handler * Cleanup query span * Prevent errors on Laravel 5.3 and below * CS * feat: Use correct route and add data * ref: Add name * fix: Route naming * feat: Start transaction in serviceProvider Move all renders as a child * ref: Rename to view.render * ref: Move back to starting transaction in middleware Use fromTraceparent function to start a trace * ref: Small refactor * feat: Update composer.json * ref: Add traces_sample_rate * Refactor active span retrieval * Guard against the active span not being set * Docblock updates * Correctly return rendered view without span * Move tracing related code to the Tracing namespace * Improve the route name detection order * Do not use route names ending with a `.` * Fix not wrapping the view enines when resolver is already resolved * feat: Rework code to use transaction on the hub Co-authored-by: Alex Bouma <[email protected]>
1 parent 2dfcf9e commit 69b666a

File tree

8 files changed

+347
-23
lines changed

8 files changed

+347
-23
lines changed

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
"require": {
1515
"php": "^7.1",
1616
"illuminate/support": "5.0 - 5.8 | ^6.0 | ^7.0",
17-
"sentry/sdk": "^2.1"
17+
"sentry/sdk": "3.x-dev"
1818
},
19+
"minimum-stability": "dev",
20+
"prefer-stable": true,
1921
"require-dev": {
2022
"phpunit/phpunit": "^8.0",
2123
"laravel/framework": "^6.0",
@@ -53,7 +55,8 @@
5355
},
5456
"laravel": {
5557
"providers": [
56-
"Sentry\\Laravel\\ServiceProvider"
58+
"Sentry\\Laravel\\ServiceProvider",
59+
"Sentry\\Laravel\\Tracing\\ServiceProvider"
5760
],
5861
"aliases": {
5962
"Sentry": "Sentry\\Laravel\\Facade"

config/sentry.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@
2727
// @see: https://docs.sentry.io/error-reporting/configuration/?platform=php#send-default-pii
2828
'send_default_pii' => false,
2929

30+
'traces_sample_rate' => 0,
3031
];

src/Sentry/Laravel/EventHandler.php

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
use Illuminate\Queue\QueueManager;
1717
use Illuminate\Routing\Events\RouteMatched;
1818
use Illuminate\Routing\Route;
19-
use Illuminate\Support\Str;
2019
use RuntimeException;
2120
use Sentry\Breadcrumb;
2221
use Sentry\SentrySdk;
2322
use Sentry\State\Scope;
23+
use Sentry\Tracing\SpanContext;
24+
use Sentry\Tracing\Transaction;
2425

2526
class EventHandler
2627
{
@@ -194,26 +195,16 @@ public function __call($method, $arguments)
194195
*/
195196
protected function routerMatchedHandler(Route $route)
196197
{
197-
$routeName = null;
198+
$routeName = Integration::extractNameForRoute($route) ?? '<unlabeled transaction>';
198199

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

203-
// Laravel 7 route caching generates a route names if the user didn't specify one
204-
// theirselfs to optimize route matching. These route names are useless to the
205-
// developer so if we encounter a generated route name we discard the value
206-
if (Str::startsWith($routeName, 'generated::')) {
207-
$routeName = null;
208-
}
209-
}
210-
211-
if (empty($routeName) && $route->getActionName()) {
212-
// SomeController@someAction (controller action)
213-
$routeName = $route->getActionName();
214-
} elseif (empty($routeName) || $routeName === 'Closure') {
215-
// /someaction // Fallback to the url
216-
$routeName = $route->uri();
202+
if ($transaction instanceof Transaction) {
203+
$transaction->setName($routeName);
204+
$transaction->setData([
205+
'action' => $route->getActionName(),
206+
'name' => $route->getName()
207+
]);
217208
}
218209

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

281+
$transaction = SentrySdk::getCurrentHub()->getTransaction();
282+
if (null !== $transaction) {
283+
$context = new SpanContext();
284+
$context->op = 'sql.query';
285+
$context->description = $query;
286+
$context->startTimestamp = microtime(true) - $time / 1000;
287+
$context->endTimestamp = $context->startTimestamp + $time / 1000;
288+
$transaction->startChild($context);
289+
}
290+
290291
Integration::addBreadcrumb(new Breadcrumb(
291292
Breadcrumb::LEVEL_INFO,
292293
Breadcrumb::TYPE_DEFAULT,

src/Sentry/Laravel/Integration.php

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace Sentry\Laravel;
44

5+
use Illuminate\Routing\Route;
6+
use Illuminate\Support\Str;
57
use Sentry\FlushableClientInterface;
68
use Sentry\SentrySdk;
9+
use Sentry\Tracing\Span;
710
use function Sentry\addBreadcrumb;
811
use function Sentry\configureScope;
912
use Sentry\Breadcrumb;
@@ -30,7 +33,9 @@ public function setupOnce(): void
3033
return $event;
3134
}
3235

33-
$event->setTransaction($self->getTransaction());
36+
if (null === $event->getTransaction()) {
37+
$event->setTransaction($self->getTransaction());
38+
}
3439

3540
return $event;
3641
});
@@ -71,7 +76,7 @@ public static function configureScope(callable $callback): void
7176
/**
7277
* @return null|string
7378
*/
74-
public static function getTransaction()
79+
public static function getTransaction(): ?string
7580
{
7681
return self::$transaction;
7782
}
@@ -98,4 +103,78 @@ public static function flushEvents(): void
98103
$client->flush();
99104
}
100105
}
106+
107+
/**
108+
* Extract the readable name for a route.
109+
*
110+
* @param \Illuminate\Routing\Route $route
111+
*
112+
* @return string|null
113+
*/
114+
public static function extractNameForRoute(Route $route): ?string
115+
{
116+
$routeName = null;
117+
118+
if (empty($routeName) && $route->getName()) {
119+
// someaction (route name/alias)
120+
$routeName = $route->getName();
121+
122+
// Laravel 7 route caching generates a route names if the user didn't specify one
123+
// theirselfs to optimize route matching. These route names are useless to the
124+
// developer so if we encounter a generated route name we discard the value
125+
if (Str::startsWith($routeName, 'generated::')) {
126+
$routeName = null;
127+
}
128+
129+
// If the route name ends with a `.` we assume an incomplete group name prefix
130+
// we discard this value since it will most likely not mean anything to the
131+
// developer and will be duplicated by other unnamed routes in the group
132+
if (Str::endsWith($routeName, '.')) {
133+
$routeName = null;
134+
}
135+
}
136+
137+
if (empty($routeName) && $route->getActionName()) {
138+
// SomeController@someAction (controller action)
139+
$routeName = ltrim($route->getActionName(), '\\');
140+
}
141+
142+
if (empty($routeName) || $routeName === 'Closure') {
143+
// /someaction // Fallback to the url
144+
$routeName = '/' . ltrim($route->uri(), '/');
145+
}
146+
147+
return $routeName;
148+
}
149+
150+
/**
151+
* Retrieve the meta tags with tracing information to link this request to front-end requests.
152+
*
153+
* @return string
154+
*/
155+
public static function sentryTracingMeta(): string
156+
{
157+
$span = self::currentTracingSpan();
158+
159+
if ($span === null) {
160+
return '';
161+
}
162+
163+
$content = sprintf('<meta name="sentry-trace" content="%s"/>', $span->toTraceparent());
164+
// $content .= sprintf('<meta name="sentry-trace-data" content="%s"/>', $span->getDescription());
165+
166+
return $content;
167+
}
168+
169+
/**
170+
* Get the current active tracing span from the scope.
171+
*
172+
* @return \Sentry\Tracing\Span|null
173+
*
174+
* @internal This is used internally as an easy way to retrieve the current active tracing span.
175+
*/
176+
public static function currentTracingSpan(): ?Span
177+
{
178+
return SentrySdk::getCurrentHub()->getSpan();
179+
}
101180
}

src/Sentry/Laravel/ServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Sentry\Laravel;
44

5+
use Illuminate\Contracts\Http\Kernel as HttpKernelInterface;
56
use Sentry\SentrySdk;
67
use Sentry\State\Hub;
78
use Sentry\ClientBuilder;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Tracing;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Sentry\SentrySdk;
8+
use Sentry\State\Hub;
9+
use Sentry\State\Scope;
10+
use Sentry\Tracing\SpanContext;
11+
use Sentry\Tracing\TransactionContext;
12+
13+
class Middleware
14+
{
15+
/**
16+
* The current active transaction.
17+
*
18+
* @var \Sentry\Tracing\Transaction|null
19+
*/
20+
protected $transaction;
21+
22+
/**
23+
* Handle an incoming request.
24+
*
25+
* @param \Illuminate\Http\Request $request
26+
* @param \Closure $next
27+
*
28+
* @return mixed
29+
*/
30+
public function handle($request, Closure $next)
31+
{
32+
if (app()->bound('sentry')) {
33+
$this->startTransaction($request, app('sentry'));
34+
}
35+
36+
return $next($request);
37+
}
38+
39+
/**
40+
* Handle the application termination.
41+
*
42+
* @param \Illuminate\Http\Request $request
43+
* @param \Illuminate\Http\Response $response
44+
*
45+
* @return void
46+
*/
47+
public function terminate($request, $response): void
48+
{
49+
if ($this->transaction !== null && app()->bound('sentry')) {
50+
$this->transaction->finish();
51+
}
52+
}
53+
54+
private function startTransaction(Request $request, Hub $sentry): void
55+
{
56+
$path = '/' . ltrim($request->path(), '/');
57+
$fallbackTime = microtime(true);
58+
$sentryTraceHeader = $request->header('sentry-trace');
59+
60+
$context = $sentryTraceHeader
61+
? TransactionContext::fromTraceparent($sentryTraceHeader)
62+
: new TransactionContext;
63+
64+
$context->op = 'http.server';
65+
$context->name = $path;
66+
$context->data = [
67+
'url' => $path,
68+
'method' => strtoupper($request->method()),
69+
];
70+
$context->startTimestamp = $request->server('REQUEST_TIME_FLOAT', $fallbackTime);
71+
72+
$this->transaction = $sentry->startTransaction($context);
73+
74+
// Setting the Transaction on the Hub
75+
SentrySdk::getCurrentHub()->setSpan($this->transaction);
76+
77+
if (!$this->addBootTimeSpans()) {
78+
// @TODO: We might want to move this together with the `RouteMatches` listener to some central place and or do this from the `EventHandler`
79+
app()->booted(function () use ($request, $fallbackTime): void {
80+
$spanContextStart = new SpanContext();
81+
$spanContextStart->op = 'app.bootstrap';
82+
$spanContextStart->startTimestamp = defined('LARAVEL_START') ? LARAVEL_START : $request->server('REQUEST_TIME_FLOAT', $fallbackTime);
83+
$spanContextStart->endTimestamp = microtime(true);
84+
$this->transaction->startChild($spanContextStart);
85+
});
86+
}
87+
}
88+
89+
private function addBootTimeSpans(): bool
90+
{
91+
if (!defined('LARAVEL_START') || !LARAVEL_START) {
92+
return false;
93+
}
94+
95+
if (!defined('SENTRY_AUTOLOAD') || !SENTRY_AUTOLOAD) {
96+
return false;
97+
}
98+
99+
if (!defined('SENTRY_BOOTSTRAP') || !SENTRY_BOOTSTRAP) {
100+
return false;
101+
}
102+
103+
$spanContextStart = new SpanContext();
104+
$spanContextStart->op = 'autoload';
105+
$spanContextStart->startTimestamp = LARAVEL_START;
106+
$spanContextStart->endTimestamp = SENTRY_AUTOLOAD;
107+
$this->transaction->startChild($spanContextStart);
108+
109+
$spanContextStart = new SpanContext();
110+
$spanContextStart->op = 'bootstrap';
111+
$spanContextStart->startTimestamp = SENTRY_AUTOLOAD;
112+
$spanContextStart->endTimestamp = SENTRY_BOOTSTRAP;
113+
$this->transaction->startChild($spanContextStart);
114+
115+
return true;
116+
}
117+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Tracing;
4+
5+
use Illuminate\Contracts\Http\Kernel as HttpKernelInterface;
6+
use Illuminate\Contracts\View\Engine;
7+
use Illuminate\Contracts\View\View;
8+
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
9+
use Illuminate\View\Engines\EngineResolver;
10+
use Illuminate\View\Factory as ViewFactory;
11+
12+
class ServiceProvider extends IlluminateServiceProvider
13+
{
14+
public function boot(): void
15+
{
16+
if ($this->app->bound(HttpKernelInterface::class)) {
17+
/** @var \Illuminate\Contracts\Http\Kernel $httpKernel */
18+
$httpKernel = $this->app->make(HttpKernelInterface::class);
19+
20+
$httpKernel->prependMiddleware(Middleware::class);
21+
}
22+
}
23+
24+
public function register(): void
25+
{
26+
$this->app->singleton(Middleware::class);
27+
28+
$viewEngineWrapper = function (EngineResolver $engineResolver): void {
29+
foreach (['file', 'php', 'blade'] as $engineName) {
30+
try {
31+
$realEngine = $engineResolver->resolve($engineName);
32+
33+
$engineResolver->register($engineName, function () use ($realEngine) {
34+
return $this->wrapViewEngine($realEngine);
35+
});
36+
} catch (InvalidArgumentException $e) {
37+
// The `file` engine was introduced in Laravel 5.4. On lower Laravel versions
38+
// resolving that driver will throw an `InvalidArgumentException`. We can
39+
// ignore this exception because we can't wrap drivers that don't exist
40+
}
41+
}
42+
};
43+
44+
if ($this->app->resolved('view.engine.resolver')) {
45+
$viewEngineWrapper($this->app->make('view.engine.resolver'));
46+
} else {
47+
$this->app->afterResolving('view.engine.resolver', $viewEngineWrapper);
48+
}
49+
}
50+
51+
private function wrapViewEngine(Engine $realEngine): Engine
52+
{
53+
/** @var ViewFactory $viewFactory */
54+
$viewFactory = $this->app->make('view');
55+
56+
/** @noinspection UnusedFunctionResultInspection */
57+
$viewFactory->composer('*', static function (View $view) use ($viewFactory) : void {
58+
$viewFactory->share(ViewEngineDecorator::SHARED_KEY, $view->name());
59+
});
60+
61+
return new ViewEngineDecorator($realEngine, $viewFactory);
62+
}
63+
}

0 commit comments

Comments
 (0)