re-designed how we profile the http API to include bigger scope

This commit is contained in:
ArabCoders
2025-01-31 19:14:45 +03:00
parent 44d3b32f3c
commit 7c57c5045a
7 changed files with 179 additions and 143 deletions

View File

@@ -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(),

View File

@@ -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'] = [

View File

@@ -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.',

View File

@@ -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,14 +55,19 @@ set_exception_handler(function (Throwable $e) {
exit(Command::FAILURE);
});
try {
$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'])) {
$_SERVER['X_REQUEST_ID'] = bin2hex(random_bytes(16));
}
$app = new App\Libs\Initializer()->boot();
} catch (Throwable $e) {
} catch (Throwable $e) {
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
$out(
@@ -77,12 +86,12 @@ try {
http_response_code(Status::SERVICE_UNAVAILABLE->value);
}
exit(Command::FAILURE);
}
return Command::FAILURE;
}
try {
new Emitter()($app->http());
} catch (Throwable $e) {
try {
new Emitter()($app->http($request));
} catch (Throwable $e) {
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
$out(
@@ -100,7 +109,12 @@ try {
if (!headers_sent()) {
http_response_code(Status::SERVICE_UNAVAILABLE->value);
}
return Command::FAILURE;
}
exit(Command::FAILURE);
return Command::SUCCESS;
}, $request);
if (Command::SUCCESS !== $exitCode) {
exit($exitCode);
}

View File

@@ -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
View 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);
}
}
}

View File

@@ -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) {