re-designed how we profile the http API to include bigger scope
This commit is contained in:
@@ -6,12 +6,10 @@ use App\Libs\Middlewares\{AddCorsMiddleware,
|
||||
AddTimingMiddleware,
|
||||
APIKeyRequiredMiddleware,
|
||||
NoAccessLogMiddleware,
|
||||
ParseJsonBodyMiddleware,
|
||||
ProfilerMiddleware};
|
||||
ParseJsonBodyMiddleware};
|
||||
|
||||
return static fn(): array => [
|
||||
fn() => new AddTimingMiddleware(),
|
||||
fn() => new ProfilerMiddleware(),
|
||||
fn() => new APIKeyRequiredMiddleware(),
|
||||
fn() => new ParseJsonBodyMiddleware(),
|
||||
fn() => new NoAccessLogMiddleware(),
|
||||
|
||||
@@ -151,12 +151,9 @@ return (function () {
|
||||
];
|
||||
|
||||
$config['profiler'] = [
|
||||
'sampler' => (int)env('WS_PROFILER_SAMPLER', 3000),
|
||||
'save' => (bool)env('WS_PROFILER_SAVE', true),
|
||||
'path' => env('WS_PROFILER_PATH', fn() => ag($config, 'tmpDir') . '/profiler'),
|
||||
'collector' => env('WS_PROFILER_COLLECTOR', null),
|
||||
'flags' => ['PROFILER_CPU_PROFILING', 'PROFILER_MEMORY_PROFILING'],
|
||||
'config' => []
|
||||
];
|
||||
|
||||
$config['cache'] = [
|
||||
|
||||
@@ -176,11 +176,6 @@ return (function () {
|
||||
'description' => 'The XHProf data collector URL to send the profiler data to.',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'key' => 'WS_PROFILER_SAMPLER',
|
||||
'description' => 'The XHProf sampler value.',
|
||||
'type' => 'int',
|
||||
],
|
||||
[
|
||||
'key' => 'WS_PROFILER_SAVE',
|
||||
'description' => 'Save the profiler data to disk.',
|
||||
|
||||
@@ -5,6 +5,10 @@ declare(strict_types=1);
|
||||
use App\Command;
|
||||
use App\Libs\Emitter;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Profiler;
|
||||
use App\Listeners\ProcessProfileEvent;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('error_reporting', 'On');
|
||||
@@ -51,6 +55,11 @@ set_exception_handler(function (Throwable $e) {
|
||||
exit(Command::FAILURE);
|
||||
});
|
||||
|
||||
$factory = new Psr17Factory();
|
||||
$request = new ServerRequestCreator($factory, $factory, $factory, $factory)->fromGlobals();
|
||||
$profiler = new Profiler(callback: fn(array $data) => queueEvent(ProcessProfileEvent::NAME, $data));
|
||||
|
||||
$exitCode = $profiler->process(function () use ($request) {
|
||||
try {
|
||||
// -- In case the frontend proxy does not generate request unique id.
|
||||
if (!isset($_SERVER['X_REQUEST_ID'])) {
|
||||
@@ -77,11 +86,11 @@ try {
|
||||
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
||||
}
|
||||
|
||||
exit(Command::FAILURE);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
new Emitter()($app->http());
|
||||
new Emitter()($app->http($request));
|
||||
} catch (Throwable $e) {
|
||||
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
|
||||
|
||||
@@ -100,7 +109,12 @@ try {
|
||||
if (!headers_sent()) {
|
||||
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
||||
}
|
||||
|
||||
exit(Command::FAILURE);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}, $request);
|
||||
|
||||
if (Command::SUCCESS !== $exitCode) {
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libs\Middlewares;
|
||||
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
use App\Listeners\ProcessProfileEvent;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Psr\Http\Server\MiddlewareInterface as iMiddleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as iHandler;
|
||||
use Random\RandomException;
|
||||
use Throwable;
|
||||
|
||||
readonly final class ProfilerMiddleware implements iMiddleware
|
||||
{
|
||||
public const string QUERY_NAME = '_profile';
|
||||
public const string HEADER_NAME = 'X-Profile';
|
||||
|
||||
public function process(iRequest $request, iHandler $handler): iResponse
|
||||
{
|
||||
if (false === extension_loaded('xhprof') || false === class_exists('Xhgui\Profiler\Profiler')) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
if (Method::GET !== Method::tryFrom($request->getMethod())) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$config = Config::get('profiler', []);
|
||||
|
||||
if (false === $this->sample($request, $config)) {
|
||||
return $handler->handle($request->withHeader('X-Profiled', 'No'));
|
||||
}
|
||||
|
||||
try {
|
||||
$profiler = new \Xhgui\Profiler\Profiler(ag($config, 'config', []));
|
||||
$profiler->enable(ag($config, 'flags', []));
|
||||
} catch (Throwable) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
try {
|
||||
$data = $profiler->disable();
|
||||
} catch (Throwable) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = ag_set(
|
||||
$data,
|
||||
'meta.id',
|
||||
ag($request->getServerParams(), 'X_REQUEST_ID', fn() => generateUUID())
|
||||
);
|
||||
|
||||
queueEvent(ProcessProfileEvent::NAME, $data);
|
||||
return $response->withHeader('X-Profiled', 'Yes');
|
||||
}
|
||||
|
||||
private function sample(iRequest $request, array $config): bool
|
||||
{
|
||||
if (true === $request->hasHeader(self::HEADER_NAME)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === ag_exists($request->getQueryParams(), self::QUERY_NAME)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$max = (int)ag($config, 'sampler', 1000);
|
||||
try {
|
||||
return 1 === random_int(1, $max);
|
||||
} catch (RandomException) {
|
||||
return 1 === rand(1, $max);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/Libs/Profiler.php
Normal file
116
src/Libs/Profiler.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libs;
|
||||
|
||||
use App\Libs\Enums\Http\Method;
|
||||
use Closure;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Random\RandomException;
|
||||
use Throwable;
|
||||
|
||||
readonly final class Profiler
|
||||
{
|
||||
public const string QUERY_NAME = '_profile';
|
||||
public const string HEADER_NAME = 'X-Profile';
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Closure{data:array, response:mixed} $callback the callback to receive the profile data.
|
||||
* @param int $sample The sample rate.
|
||||
* @param array $config The profiler configuration.
|
||||
* @param array $flags The profiler flags.
|
||||
*/
|
||||
public function __construct(
|
||||
private Closure $callback,
|
||||
private int $sample = 5000,
|
||||
private array $config = [],
|
||||
private array $flags = ['PROFILER_CPU_PROFILING', 'PROFILER_MEMORY_PROFILING'],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the function and profile it.
|
||||
*
|
||||
* @param callable $func The function to profile.
|
||||
* @param iRequest|null $request The request object.
|
||||
*
|
||||
* @return mixed The result of the function.
|
||||
*/
|
||||
public function process(callable $func, iRequest|null $request = null): mixed
|
||||
{
|
||||
if (false === extension_loaded('xhprof') || false === class_exists('Xhgui\Profiler\Profiler')) {
|
||||
return $func();
|
||||
}
|
||||
|
||||
if (null === $request) {
|
||||
$factory = new Psr17Factory();
|
||||
$request = new ServerRequestCreator($factory, $factory, $factory, $factory)->fromGlobals();
|
||||
}
|
||||
|
||||
if (Method::GET !== Method::tryFrom($request->getMethod())) {
|
||||
return $func();
|
||||
}
|
||||
|
||||
if (false === $this->sample($request)) {
|
||||
return $func();
|
||||
}
|
||||
|
||||
try {
|
||||
$profiler = new \Xhgui\Profiler\Profiler($this->config);
|
||||
$profiler->enable($this->flags);
|
||||
} catch (Throwable) {
|
||||
return $func();
|
||||
}
|
||||
|
||||
$response = $func();
|
||||
|
||||
try {
|
||||
$data = $profiler->disable();
|
||||
} catch (Throwable) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = ag_set(
|
||||
$data,
|
||||
'meta.id',
|
||||
ag($request->getServerParams(), 'X_REQUEST_ID', fn() => generateUUID())
|
||||
);
|
||||
|
||||
($this->callback)($data, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request should be profiled.
|
||||
*
|
||||
* @param iRequest $request The request object.
|
||||
*
|
||||
* @return bool True if the request should be profiled, false otherwise.
|
||||
*/
|
||||
private function sample(iRequest $request): bool
|
||||
{
|
||||
if (true === $request->hasHeader(self::HEADER_NAME)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (true === ag_exists($request->getQueryParams(), self::QUERY_NAME)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return 1 === random_int(1, $this->sample);
|
||||
} catch (RandomException) {
|
||||
return 1 === rand(1, $this->sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use App\Libs\Config;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\libs\Events\DataEvent;
|
||||
use App\Libs\Middlewares\ProfilerMiddleware;
|
||||
use App\Libs\Profiler;
|
||||
use App\Libs\Stream;
|
||||
use App\Libs\Uri;
|
||||
use App\Model\Events\EventListener;
|
||||
@@ -113,7 +113,7 @@ final readonly class ProcessProfileEvent
|
||||
'meta.SERVER.REMOTE_USER' => true,
|
||||
'meta.SERVER.UNIQUE_ID' => true,
|
||||
'meta.get.apikey' => true,
|
||||
'meta.get.' . ProfilerMiddleware::QUERY_NAME => false,
|
||||
'meta.get.' . Profiler::QUERY_NAME => false,
|
||||
];
|
||||
|
||||
foreach ($maskKeys as $key => $mask) {
|
||||
|
||||
Reference in New Issue
Block a user