在 Hyperf 生命周期内记录完整日志

最近在Hyperf作者 limingxinleoGithub中看到了关于使用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 协议》,转载必须注明作者和本文链接
故地有明月, 何慕异乡圆.
wenber
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!