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,
|
AddTimingMiddleware,
|
||||||
APIKeyRequiredMiddleware,
|
APIKeyRequiredMiddleware,
|
||||||
NoAccessLogMiddleware,
|
NoAccessLogMiddleware,
|
||||||
ParseJsonBodyMiddleware,
|
ParseJsonBodyMiddleware};
|
||||||
ProfilerMiddleware};
|
|
||||||
|
|
||||||
return static fn(): array => [
|
return static fn(): array => [
|
||||||
fn() => new AddTimingMiddleware(),
|
fn() => new AddTimingMiddleware(),
|
||||||
fn() => new ProfilerMiddleware(),
|
|
||||||
fn() => new APIKeyRequiredMiddleware(),
|
fn() => new APIKeyRequiredMiddleware(),
|
||||||
fn() => new ParseJsonBodyMiddleware(),
|
fn() => new ParseJsonBodyMiddleware(),
|
||||||
fn() => new NoAccessLogMiddleware(),
|
fn() => new NoAccessLogMiddleware(),
|
||||||
|
|||||||
@@ -151,12 +151,9 @@ return (function () {
|
|||||||
];
|
];
|
||||||
|
|
||||||
$config['profiler'] = [
|
$config['profiler'] = [
|
||||||
'sampler' => (int)env('WS_PROFILER_SAMPLER', 3000),
|
|
||||||
'save' => (bool)env('WS_PROFILER_SAVE', true),
|
'save' => (bool)env('WS_PROFILER_SAVE', true),
|
||||||
'path' => env('WS_PROFILER_PATH', fn() => ag($config, 'tmpDir') . '/profiler'),
|
'path' => env('WS_PROFILER_PATH', fn() => ag($config, 'tmpDir') . '/profiler'),
|
||||||
'collector' => env('WS_PROFILER_COLLECTOR', null),
|
'collector' => env('WS_PROFILER_COLLECTOR', null),
|
||||||
'flags' => ['PROFILER_CPU_PROFILING', 'PROFILER_MEMORY_PROFILING'],
|
|
||||||
'config' => []
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$config['cache'] = [
|
$config['cache'] = [
|
||||||
|
|||||||
@@ -176,11 +176,6 @@ return (function () {
|
|||||||
'description' => 'The XHProf data collector URL to send the profiler data to.',
|
'description' => 'The XHProf data collector URL to send the profiler data to.',
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'key' => 'WS_PROFILER_SAMPLER',
|
|
||||||
'description' => 'The XHProf sampler value.',
|
|
||||||
'type' => 'int',
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'key' => 'WS_PROFILER_SAVE',
|
'key' => 'WS_PROFILER_SAVE',
|
||||||
'description' => 'Save the profiler data to disk.',
|
'description' => 'Save the profiler data to disk.',
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ declare(strict_types=1);
|
|||||||
use App\Command;
|
use App\Command;
|
||||||
use App\Libs\Emitter;
|
use App\Libs\Emitter;
|
||||||
use App\Libs\Enums\Http\Status;
|
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);
|
error_reporting(E_ALL);
|
||||||
ini_set('error_reporting', 'On');
|
ini_set('error_reporting', 'On');
|
||||||
@@ -51,14 +55,19 @@ set_exception_handler(function (Throwable $e) {
|
|||||||
exit(Command::FAILURE);
|
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.
|
// -- In case the frontend proxy does not generate request unique id.
|
||||||
if (!isset($_SERVER['X_REQUEST_ID'])) {
|
if (!isset($_SERVER['X_REQUEST_ID'])) {
|
||||||
$_SERVER['X_REQUEST_ID'] = bin2hex(random_bytes(16));
|
$_SERVER['X_REQUEST_ID'] = bin2hex(random_bytes(16));
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new App\Libs\Initializer()->boot();
|
$app = new App\Libs\Initializer()->boot();
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
|
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
|
||||||
|
|
||||||
$out(
|
$out(
|
||||||
@@ -77,12 +86,12 @@ try {
|
|||||||
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
exit(Command::FAILURE);
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new Emitter()($app->http());
|
new Emitter()($app->http($request));
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
|
$out = fn($message) => inContainer() ? fwrite(STDERR, $message) : syslog(LOG_ERR, $message);
|
||||||
|
|
||||||
$out(
|
$out(
|
||||||
@@ -100,7 +109,12 @@ try {
|
|||||||
if (!headers_sent()) {
|
if (!headers_sent()) {
|
||||||
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
http_response_code(Status::SERVICE_UNAVAILABLE->value);
|
||||||
}
|
}
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
exit(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\Method;
|
||||||
use App\Libs\Enums\Http\Status;
|
use App\Libs\Enums\Http\Status;
|
||||||
use App\libs\Events\DataEvent;
|
use App\libs\Events\DataEvent;
|
||||||
use App\Libs\Middlewares\ProfilerMiddleware;
|
use App\Libs\Profiler;
|
||||||
use App\Libs\Stream;
|
use App\Libs\Stream;
|
||||||
use App\Libs\Uri;
|
use App\Libs\Uri;
|
||||||
use App\Model\Events\EventListener;
|
use App\Model\Events\EventListener;
|
||||||
@@ -113,7 +113,7 @@ final readonly class ProcessProfileEvent
|
|||||||
'meta.SERVER.REMOTE_USER' => true,
|
'meta.SERVER.REMOTE_USER' => true,
|
||||||
'meta.SERVER.UNIQUE_ID' => true,
|
'meta.SERVER.UNIQUE_ID' => true,
|
||||||
'meta.get.apikey' => true,
|
'meta.get.apikey' => true,
|
||||||
'meta.get.' . ProfilerMiddleware::QUERY_NAME => false,
|
'meta.get.' . Profiler::QUERY_NAME => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($maskKeys as $key => $mask) {
|
foreach ($maskKeys as $key => $mask) {
|
||||||
|
|||||||
Reference in New Issue
Block a user