在 Hyperf 生命周期内记录完整日志
最近在Hyperf
作者 limingxinleo 的Github
中看到了关于使用Swoole defer
机制进行生命周期内的完整日志记录, 所以改造了目前自己的日志采集操作.
Debug中间件示例
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Kernel\Log\AppendRequestProcessor;
use App\Kernel\Log\Log;
use App\Report\Notifier;
use App\Support\Trait\HasUser;
use Carbon\Carbon;
use Hyperf\Context\Context;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Utils\Codec\Json;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use function microtime;
class DebugMiddleware implements MiddlewareInterface
{
use HasUser;
public function __construct(protected ContainerInterface $container)
{
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
*
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @return \Psr\Http\Message\ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->collecter($request);
defer(function () {
try {
$this->record();
} catch (Throwable $e) {
Log::get('debug.middleware')->error($e->getMessage());
}
});
return $handler->handle($request);
}
public function record(): void
{
$endTime = microtime(true);
$context = Context::get(AppendRequestProcessor::LOG_LIFECYCLE_KEY) ?? [];
$duration = round(($endTime - $context['trigger_time']) * 1000);
$context['duration'] = $duration.'ms';
$context['trigger_time'] = Carbon::createFromTimestamp($context['trigger_time'])->toDateTimeString();
$response = Context::get(ResponseInterface::class);
$context['response'] = $this->getResponseToArray($response);
isset($context['exception']) && make(Notifier::class)->exceptionReport($context, $context['exception']);
if ($duration >= 500) {
Log::get('request')->error(Context::get(AppendRequestProcessor::LOG_REQUEST_ID_KEY), $context);
} else {
Log::get('request')->debug(Context::get(AppendRequestProcessor::LOG_COROUTINE_ID_KEY), $context);
}
}
/**
* @param \Psr\Http\Message\ServerRequestInterface|null $request
*
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @return void
*/
protected function collecter(?ServerRequestInterface $request): void
{
$startTime = microtime(true);
$context = [
'app_name' => config('app_name'),
'app_env' => config('app_env'),
'trigger_time' => $startTime,
'usage_memory' => round(memory_get_peak_usage(true) / 1024 / 1024, 1).'M',
'url' => $request?->url(),
'uri' => $request?->getUri()->getPath(),
'method' => $request?->getMethod(),
'duration' => '',
];
$context['headers'] = $this->getHeaders($request);
$context['query'] = $request?->getQueryParams();
$context['request'] = $this->container->get(RequestInterface::class)->post();
$context = array_merge($context, $this->getUser());
Context::set(AppendRequestProcessor::LOG_LIFECYCLE_KEY, $context);
}
protected function getUser(): array
{
if (self::isLogin()) {
$user = self::user();
return [
'user' => [
'id' => $user->id,
'nickname' => $user->nickname,
'role' => $user->role,
],
];
}
return [];
}
protected function getHeaders(?ServerRequestInterface $request): array
{
if ($request === null) {
return [];
}
$onlyHeaderKeys = [
'content-type',
'user-agent',
'sign',
'token',
'x-token',
'authorization',
'x-real-ip',
'x-forwarded-for',
'cookie',
];
$logHeaders = [];
foreach ($onlyHeaderKeys as $value) {
if ($request->getHeaderLine($value)) {
$logHeaders[$value] = $request->getHeaderLine($value);
}
}
return array_filter($logHeaders);
}
protected function getResponseToArray(?ResponseInterface $response): array
{
if ($response === null) {
return [];
}
$type = $response->getHeaderLine('content-type');
if (str_contains($type, 'application/json')) {
$data = Json::decode($response->getBody()->getContents());
} else {
$data = (array)$response->getBody();
}
if (isset($data['debug']) || isset($data['soar'])) {
unset($data['debug'], $data['soar']);
}
return $data;
}
}
一个自定义的统一响应类Response
<?php
declare(strict_types=1);
namespace App\Kernel\Http;
use App\Constants\BusCode;
use App\Constants\HttpCode;
use App\Kernel\Log\AppendRequestProcessor;
use Hyperf\Context\Context;
use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Paginator\AbstractPaginator;
use Hyperf\Resource\Json\JsonResource;
use Hyperf\Resource\Json\ResourceCollection;
use Hyperf\Utils\Contracts\Arrayable;
use JetBrains\PhpStorm\ArrayShape;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
class Response
{
protected ResponseInterface $response;
public function __construct(protected ContainerInterface $container)
{
$this->response = $container->get(ResponseInterface::class);
}
public function success(
mixed $data = [],
string $message = 'success',
int $code = HttpCode::OK
): PsrResponseInterface {
if ($data instanceof ResourceCollection) {
$data = $this->formatResourceCollection(...func_get_args());
}
if ($data instanceof AbstractPaginator) {
$data = $this->formatPaginated($data);
}
if ($data instanceof JsonResource) {
$data = $this->formatResource(...func_get_args());
}
if ($data instanceof Arrayable) {
$data = $data->toArray();
}
return $this->formatting($code, $message, $code, $data);
}
protected function formatPaginated(AbstractPaginator $resource): array
{
$paginated = $resource->toArray();
$data['data'] = $paginated['data'];
$paginationInformation = $this->formatPaginatedData($paginated);
return array_merge_recursive($data, $paginationInformation);
}
protected function formatResource(JsonResource $resource): array
{
return array_merge_recursive(
$resource->resolve(),
$resource->with(),
$resource->additional
);
}
protected function formatResourceCollection(ResourceCollection $resource): array
{
$data = array_merge_recursive(
$resource->resolve(),
$resource->with(),
$resource->additional
);
if ($resource->resource instanceof AbstractPaginator) {
$paginated = $resource->resource->toArray();
$paginationInformation = $this->formatPaginatedData($paginated);
$data = array_merge_recursive($data, $paginationInformation);
}
return $data;
}
#[ArrayShape(['meta' => 'array', 'links' => 'array'])]
protected function formatPaginatedData(array $paginated): array
{
return [
'meta' => [
'to' => $paginated['to'] ?? 0,
'per_page' => $paginated['per_page'] ?? 0,
'current_page' => $paginated['current_page'] ?? 0,
'path' => $paginated['path'] ?? '',
'from' => $paginated['from'] ?? 0,
],
'links' => [
'first' => $paginated['first_page_url'] ?? '',
'last' => $paginated['last_page_url'] ?? '',
'next' => $paginated['next_page_url'] ?? '',
'prev' => $paginated['prev_page_url'] ?? '',
],
];
}
public function fail(
int $status = BusCode::SUCCESS,
string $message = '',
array $errors = [],
int $code = HttpCode::OK,
): PsrResponseInterface {
if (empty($message)) {
$message = BusCode::getMessage($status) ?? 'bus error';
}
if (!config('app_debug')) {
$errors = [];
}
return $this->formatting($code, $message, $status, errors: $errors);
}
protected function formatting(
int $code,
string $message,
int $status = BusCode::SUCCESS,
$data = [],
array $errors = []
): PsrResponseInterface {
$body = [
'request_id' => Context::get(AppendRequestProcessor::LOG_REQUEST_ID_KEY),
'status' => $status,
'message' => $message,
];
!empty($errors) && $body['debug'] = $errors;
if (!empty($data)) {
$body = isset($data['data']) ? array_merge($body, $data) : array_merge($body, ['data' => $data]);
}
$this->withAddedHeaders(['content-type' => 'application/json; charset=utf-8']);
$response = $this->response->withStatus($code)->json($body);
Context::set(PsrResponseInterface::class, $response);
return $response;
}
public function withAddedHeaders(array $headers): Response
{
$config = config('response.headers');
$headers = array_merge($config, $headers);
$response = $this->response;
foreach ($headers as $key => $value) {
$response = $response->withHeader($key, $value);
}
// Context::set(PsrResponseInterface::class, $response);
$this->response = $response;
return $this;
}
public function redirect($url, int $status = 302): PsrResponseInterface
{
return $this->response
->withAddedHeader('Location', (string)$url)
->withStatus($status);
}
public function cookie(Cookie $cookie): Response
{
$response = $this->response->withCookie($cookie);
Context::set(PsrResponseInterface::class, $response);
return $this;
}
public function getResponse()
{
return $this->response;
}
}
在你项目内的最后一个(即停止异常冒泡的异常处理器中加入)
$context = Context::get(AppendRequestProcessor::LOG_LIFECYCLE_KEY);
$context['exception'] = $throwable;
Context::set(AppendRequestProcessor::LOG_LIFECYCLE_KEY, $context);
然后将Debug中间件放在middleware配置文件中的第一个.
控制器基类
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Kernel\Http\Response;
abstract class AbstractController
{
#[Inject]
protected Response $response;
}
返回响应
$this->response->success();
$this->response->fail();
本作品采用《CC 协议》,转载必须注明作者和本文链接