Skip to content

Commit aa8add2

Browse files
authored
Merge pull request #7830 from ProcessMaker/epic/FOUR-20327
FOUR-20327 [EPIC] Implement Baseline Server Timing Headers
2 parents f728483 + ed89380 commit aa8add2

File tree

6 files changed

+378
-1
lines changed

6 files changed

+378
-1
lines changed

ProcessMaker/Http/Kernel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ProcessMaker\Http;
44

55
use Illuminate\Foundation\Http\Kernel as HttpKernel;
6+
use ProcessMaker\Http\Middleware\ServerTimingMiddleware;
67

78
class Kernel extends HttpKernel
89
{
@@ -20,6 +21,7 @@ class Kernel extends HttpKernel
2021
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
2122
Middleware\TrustProxies::class,
2223
Middleware\BrowserCache::class,
24+
ServerTimingMiddleware::class,
2325
];
2426

2527
/**
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use ProcessMaker\Providers\ProcessMakerServiceProvider;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class ServerTimingMiddleware
11+
{
12+
// Minimum time in ms to include a package in the Server-Timing header
13+
private static $minPackageTime;
14+
15+
public function __construct()
16+
{
17+
self::$minPackageTime = config('app.server_timing.min_package_time');
18+
}
19+
/**
20+
* Handle an incoming request.
21+
*
22+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
23+
*/
24+
public function handle(Request $request, Closure $next): Response
25+
{
26+
if (!config('app.server_timing.enabled')) {
27+
return $next($request);
28+
}
29+
30+
// Start time for controller execution
31+
$startController = microtime(true);
32+
33+
// Process the request
34+
$response = $next($request);
35+
36+
// Calculate execution times
37+
$controllerTime = (microtime(true) - $startController) * 1000; // Convert to ms
38+
// Fetch service provider boot time
39+
$serviceProviderTime = ProcessMakerServiceProvider::getBootTime() ?? 0;
40+
// Fetch query time
41+
$queryTime = ProcessMakerServiceProvider::getQueryTime() ?? 0;
42+
43+
$serverTiming = [
44+
"provider;dur={$serviceProviderTime}",
45+
"controller;dur={$controllerTime}",
46+
"db;dur={$queryTime}",
47+
];
48+
49+
$hasLaravelStart = defined('LARAVEL_START');
50+
if ($hasLaravelStart) {
51+
$bootTiming = ($startController - \LARAVEL_START) * 1000; // Convert to ms
52+
array_unshift($serverTiming, "boot;dur={$bootTiming}");
53+
}
54+
55+
$packageTimes = ProcessMakerServiceProvider::getPackageBootTiming();
56+
57+
foreach ($packageTimes as $package => $timing) {
58+
$time = ($timing['end'] - $timing['start']) * 1000;
59+
60+
// Only include packages that took more than MIN_PACKAGE_TIME ms
61+
if ($time > self::$minPackageTime) {
62+
$serverTiming[] = "{$package};dur={$time}";
63+
}
64+
}
65+
66+
// Add Server-Timing headers
67+
$response->headers->set('Server-Timing', $serverTiming);
68+
69+
return $response;
70+
}
71+
}

ProcessMaker/Providers/ProcessMakerServiceProvider.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use Illuminate\Notifications\Events\BroadcastNotificationCreated;
1111
use Illuminate\Notifications\Events\NotificationSent;
1212
use Illuminate\Support\Facades;
13+
use Illuminate\Support\Facades\DB;
14+
use Illuminate\Support\Facades\Log;
1315
use Illuminate\Support\Facades\URL;
1416
use Laravel\Dusk\DuskServiceProvider;
1517
use Laravel\Horizon\Horizon;
@@ -35,8 +37,20 @@
3537
*/
3638
class ProcessMakerServiceProvider extends ServiceProvider
3739
{
40+
// Track the start time for service providers boot
41+
private static $bootStart;
42+
// Track the boot time for service providers
43+
private static $bootTime;
44+
// Track the boot time for each package
45+
private static $packageBootTiming = [];
46+
// Track the query time for each request
47+
private static $queryTime = 0;
48+
3849
public function boot(): void
3950
{
51+
// Track the start time for service providers boot
52+
self::$bootStart = microtime(true);
53+
4054
$this->app->singleton(Menu::class, function ($app) {
4155
return new MenuManager();
4256
});
@@ -52,10 +66,20 @@ public function boot(): void
5266
$this->setupFactories();
5367

5468
parent::boot();
69+
70+
// Hook after service providers boot
71+
self::$bootTime = (microtime(true) - self::$bootStart) * 1000; // Convert to milliseconds
5572
}
5673

5774
public function register(): void
5875
{
76+
if (config('app.server_timing.enabled')) {
77+
// Listen to query events and accumulate query execution time
78+
DB::listen(function ($query) {
79+
self::$queryTime += $query->time;
80+
});
81+
}
82+
5983
// Dusk, if env is appropriate
6084
// TODO Remove Dusk references and remove from composer dependencies
6185
if (!$this->app->environment('production')) {
@@ -358,4 +382,71 @@ public static function forceHttps(): void
358382
URL::forceScheme('https');
359383
}
360384
}
385+
386+
/**
387+
* Get the boot time for service providers.
388+
*
389+
* @return float|null
390+
*/
391+
public static function getBootTime(): ?float
392+
{
393+
return self::$bootTime;
394+
}
395+
396+
/**
397+
* Get the query time for the request.
398+
*
399+
* @return float
400+
*/
401+
public static function getQueryTime(): float
402+
{
403+
return self::$queryTime;
404+
}
405+
406+
/**
407+
* Set the boot time for service providers.
408+
*
409+
* @param string $package
410+
* @param float $time
411+
*/
412+
public static function setPackageBootStart(string $package, float $time): void
413+
{
414+
if ($time < 0) {
415+
Log::info("Server Timing: Invalid boot time for package: {$package}, time: {$time}");
416+
417+
$time = 0;
418+
}
419+
420+
self::$packageBootTiming[$package] = [
421+
'start' => $time,
422+
'end' => null,
423+
];
424+
}
425+
426+
/**
427+
* Set the boot time for service providers.
428+
*
429+
*
430+
* @param float $time
431+
*/
432+
public static function setPackageBootedTime(string $package, $time): void
433+
{
434+
if (!isset(self::$packageBootTiming[$package]) || $time < 0) {
435+
Log::info("Server Timing: Invalid booted time for package: {$package}, time: {$time}");
436+
437+
return;
438+
}
439+
440+
self::$packageBootTiming[$package]['end'] = $time;
441+
}
442+
443+
/**
444+
* Get the boot time for service providers.
445+
*
446+
* @return array
447+
*/
448+
public static function getPackageBootTiming(): array
449+
{
450+
return self::$packageBootTiming;
451+
}
361452
}

ProcessMaker/Traits/PluginServiceProviderTrait.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use ProcessMaker\Managers\IndexManager;
1212
use ProcessMaker\Managers\LoginManager;
1313
use ProcessMaker\Managers\PackageManager;
14+
use ProcessMaker\Providers\ProcessMakerServiceProvider;
1415

1516
/**
1617
* Add functionality to control a PM plug-in
@@ -21,6 +22,58 @@ trait PluginServiceProviderTrait
2122

2223
private $scriptBuilderScripts = [];
2324

25+
private static $bootStart = null;
26+
27+
private static $bootTime;
28+
29+
public function __construct($app)
30+
{
31+
parent::__construct($app);
32+
33+
$this->bootServerTiming();
34+
}
35+
36+
/**
37+
* The `bootServerTiming` function sets up timing measurements for the booting and booted events of the packages
38+
*
39+
* @return void If the condition `config('app.server_timing.enabled')` is false, nothing is being returned as the
40+
* function will exit early.
41+
*/
42+
protected function bootServerTiming(): void
43+
{
44+
if (!config('app.server_timing.enabled')) {
45+
return;
46+
}
47+
48+
$package = $this->getPackageName();
49+
50+
$this->booting(function () use ($package) {
51+
self::$bootStart = microtime(true);
52+
53+
ProcessMakerServiceProvider::setPackageBootStart($package, self::$bootStart);
54+
});
55+
56+
$this->booted(function () use ($package) {
57+
self::$bootTime = microtime(true);
58+
59+
ProcessMakerServiceProvider::setPackageBootedTime($package, self::$bootTime);
60+
});
61+
}
62+
63+
/**
64+
* Get the package name for the Server Timing header
65+
*
66+
* @return string
67+
*/
68+
protected function getPackageName(): string
69+
{
70+
if (defined('static::name')) {
71+
return ucfirst(\Str::camel(static::name));
72+
}
73+
74+
return substr(static::class, strrpos(static::class, '\\') + 1);
75+
}
76+
2477
/**
2578
* Boot the PM plug-in.
2679
*/

config/app.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@
246246
// Process Request security log rate limit: 1 per day (86400 seconds)
247247
'process_request_errors_rate_limit' => env('PROCESS_REQUEST_ERRORS_RATE_LIMIT', 1),
248248
'process_request_errors_rate_limit_duration' => env('PROCESS_REQUEST_ERRORS_RATE_LIMIT_DURATION', 86400),
249-
249+
250250
'default_colors' => [
251251
'primary' => '#2773F3',
252252
'secondary' => '#728092',
@@ -266,4 +266,9 @@
266266
'vault_token' => env('ENCRYPTED_DATA_VAULT_TOKEN', ''),
267267
'vault_transit_key' => env('ENCRYPTED_DATA_VAULT_TRANSIT_KEY', ''),
268268
],
269+
270+
'server_timing' => [
271+
'enabled' => env('SERVER_TIMING_ENABLED', true),
272+
'min_package_time' => env('SERVER_TIMING_MIN_PACKAGE_TIME', 5), // Minimum time in milliseconds
273+
],
269274
];

0 commit comments

Comments
 (0)