Search by

khyzd / hyperf-guzzle-logger

khyzd

一个用于Hyperf 3的Guzzle包装器,可将每个HTTP请求和响应记录到数据库表中,并可选择每月/每年进行数据分片。

Package info

gitee.com/khyyyzd/hyperf-guzzle-logger.git

pkg:composer/khyzd/hyperf-guzzle-logger

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

v3.1.1 2026-11-06 07:55 UTC

This package is not auto-updated.

Last update: 2026-11-09 03:00:01 UTC


README

Hyperf 3 的 Guzzle 二次封装。所有通过本包发出的 HTTP 请求都会自动记录到数据库,并支持按月 / 按年自动分表。

  • 默认按月分表:guzzle_log_202611
  • 可切换为按年分表:guzzle_log_2026
  • 也可关闭分表:guzzle_log
  • 调用时通过 note 参数写备注,note 不会发送给上游
  • 自动记录 HTTP 状态码、响应体、耗时、请求参数
  • 请求参数按原样记录(json、form_params、multipart、headers、query 等)
  • 自动脱敏 Authorization、Cookie、token、password 等,以及 URL query 中的敏感参数
  • HTTP 4xx/5xx 和网络异常都会记录
  • 读取日志响应体不会破坏业务方拿到的响应
  • 数据库写入失败默认不阻断业务请求
  • 支持同步、异步(Guzzle Promise)请求
  • 支持自动建表、迁移和预建分表命令

环境要求

依赖版本
PHP>= 8.0
Hyperf3.0 / 3.1 / 3.2
Guzzle>= 7.5
数据库MySQL 5.7 / 8.0(自动建表仅支持 MySQL)
PHP 扩展ext-json、ext-mbstring

安装

# 安装最新的 3.1.x 正式版
composer require khyzd/hyperf-guzzle-logger:^3.1

如果需要固定到具体版本,可以使用 3.1.0。 本地开发、私有部署或 Composer 找不到包时,请参考下面的 path/vcs repository 配置。

本地 / 私有包安装

必须在业务 Hyperf 项目根目录执行 composer require,不能在 hyperf-guzzle-logger 包目录里执行。

如果不是从 Packagist 安装,在项目 composer.json 中添加:

{
    "repositories": [
        {
            "type": "path",
            "url": "/path/to/hyperf-guzzle-logger",
            "options": {
                "symlink": true,
                "versions": {
                    "khyzd/hyperf-guzzle-logger": "3.1.0"
                }
            }
        }
    ]
}

然后执行:

composer require khyzd/hyperf-guzzle-logger:^3.1

如果不设置 versions,也可以直接安装开发版本:

composer require khyzd/hyperf-guzzle-logger:@dev

发布到私有 Git 仓库后,则可以改用 vcs repository,并正常使用 ^3.1 版本约束。

Hyperf 会通过 extra.hyperf.config 自动注册 ConfigProvider,无需手动添加。

发布配置文件和基础表迁移:

php bin/hyperf.php vendor:publish khyzd/hyperf-guzzle-logger

发布后会得到:

  • config/autoload/guzzle_logger.php
  • migrations/2026_11_05_000000_create_guzzle_log_table.php

执行迁移(创建基础表 guzzle_log):

php bin/hyperf.php migrate

默认 auto_create_table = true,即使不执行迁移,第一次写入时也会自动创建目标分表。 如果需要关闭自动建表,请设置 auto_create_table = false 并自行创建分表。

配置

config/autoload/guzzle_logger.php:

<?php

use Khyzd\HyperfGuzzleLogger\Config\GuzzleLoggerConfig;

return [
    'enabled' => env('GUZZLE_LOGGER_ENABLED', true),

    // config/autoload/databases.php 中的连接名
    'connection' => env('GUZZLE_LOGGER_DB_CONNECTION', 'default'),

    // 基础表名
    'table' => env('GUZZLE_LOGGER_TABLE', 'guzzle_log'),

    // none | monthly | yearly
    'partition' => env('GUZZLE_LOGGER_PARTITION', GuzzleLoggerConfig::PARTITION_MONTHLY),

    // 自动建表(仅 MySQL)
    'auto_create_table' => env('GUZZLE_LOGGER_AUTO_CREATE_TABLE', true),

    // 分表和 created_at / updated_at 使用的时区,null 表示 PHP 默认时区
    'timezone' => env('GUZZLE_LOGGER_TIMEZONE', null),

    // 传递给 new GuzzleHttp\Client() 的默认配置,可按客户端覆盖
    'client' => [
        // 'timeout' => 10.0,
        // 'connect_timeout' => 5.0,
    ],

    // 这些 key 的值在 options 和 url query 中会替换为 ***
    'sensitive_keys' => [
        'authorization',
        'proxy-authorization',
        'cookie',
        'set-cookie',
        'x-api-key',
        'api-key',
        'apikey',
        'token',
        'access_token',
        'refresh_token',
        'auth',
        'password',
        'passwd',
        'secret',
        'client_secret',
    ],

    // 字节数上限,0 表示不截断;options 是 TEXT,建议不要超过 65535
    'max_options_length' => env('GUZZLE_LOGGER_MAX_OPTIONS_LENGTH', 65535),
    'max_response_length' => env('GUZZLE_LOGGER_MAX_RESPONSE_LENGTH', 1048576),

    // time 字段保留的小数位数(单位:秒)
    'time_precision' => 6,

    // 日志写入失败时是否抛出异常;默认 false,仅记录 warning
    'throw_on_log_failure' => env('GUZZLE_LOGGER_THROW_ON_LOG_FAILURE', false),

    // 是否对 url 中命中 sensitive_keys 的 query 参数值脱敏
    'redact_url_query' => env('GUZZLE_LOGGER_REDACT_URL_QUERY', true),
];

分表规则

分表依据「请求开始时间」和 timezone 配置决定:

partition表名示例
noneguzzle_log
monthlyguzzle_log_202611
yearlyguzzle_log_2026
  • 默认是 monthly。
  • 表名后缀格式为 Ym(月)或 Y(年)。
  • 如果 databases.php 配置了表前缀(prefix),包会自动加上前缀。
  • 切换分表模式后,旧数据仍保留在原表中,新请求写入新规则对应的表。

自动建表与预建分表

默认开启自动建表,每个 worker 首次写入某个分表时会检查并执行:

CREATE TABLE IF NOT EXISTS `guzzle_log_202611` (...);

多 worker 并发执行也是安全的(IF NOT EXISTS)。

也可以提前建表:

# 创建当前月 + 下个月
php bin/hyperf.php guzzle-logger:create-table

# 以 2026-12 为基准,创建 2026-12、2027-01、2027-02 三个月的表
php bin/hyperf.php guzzle-logger:create-table --date=2026-12-01 --next=2

# 同时创建基础表 guzzle_log
php bin/hyperf.php guzzle-logger:create-table --base

如果数据库账号没有 CREATE TABLE 权限,请关闭自动建表并由 DBA 建表:

CREATE TABLE guzzle_log_202611 LIKE guzzle_log;

数据表字段

字段说明
method请求方法,自动大写,最多 50 字符
url实际请求地址(包含 query string),命中脱敏规则的 query 值会替换为 ***,最多 255 字符
options原始 Guzzle options 的 JSON,不包含 note,敏感值已脱敏
return_codeHTTP 状态码;网络异常时为 0
response响应体;请求异常时为「异常信息 + 响应体」
time请求耗时,单位秒,默认保留 6 位小数,例如 0.123456
note调用时传入的备注,可为空,最多 255 字符
created_at请求开始时间(按配置时区)
updated_at请求结束、日志写入时间(按配置时区)

关于 options

Guzzle 在真正发请求前会把 headers、json、form_params、multipart 等选项转换并移除。 本包通过自定义的 LoggingClient 在转换前保存原始 options,因此日志里能看到调用方实际传的参数,例如:

$this->http->post('https://api.example.com/orders', [
    'json' => ['order_id' => 1001],
    'headers' => ['Authorization' => 'Bearer secret'],
    'timeout' => 5,
    'note' => '创建订单',
]);

写入的 options 大致为:

{
    "json": {"order_id": 1001},
    "headers": {"Authorization": "***"},
    "timeout": 5
}
  • note 会被单独写入 note 字段,不会出现在 options 中,也不会传给 Guzzle / 上游。
  • 记录的是本次调用传入的 options;通过 ClientFactory 创建客户端时设置的默认 headers 等客户端级配置不会合并进 options。
  • StreamInterface、resource、闭包等无法 JSON 化的值会被转换成描述信息。
  • 超过 max_options_length 时按字节截断,并追加 ...[truncated]。

关于响应体

  • 成功响应:记录响应体;读取后会将 stream 指针还原,业务代码仍可正常读取。
  • 非 seekable 响应:会复制为内存流,业务代码仍能读取。
  • stream = true 或指定了 sink 的请求:不读取响应体,记录 [response body not recorded for a streaming/sink request]。
  • HTTP 4xx/5xx:默认 http_errors = true 时 Guzzle 抛出异常,包会记录状态码和响应体后继续抛出原异常。
  • 网络异常(连接失败、超时等):return_code = 0,response 记录异常信息和可用响应体。

使用

依赖注入

GuzzleLoggerInterface 已绑定到默认实现,直接注入即可:

<?php

declare(strict_types=1);

namespace App\Service;

use Khyzd\HyperfGuzzleLogger\GuzzleLoggerInterface;

final class OrderService
{
    public function __construct(
        private GuzzleLoggerInterface $http,
    ) {
    }

    public function createOrder(array $payload): array
    {
        $response = $this->http->post('https://api.example.com/orders', [
            'json' => $payload,
            'headers' => [
                'Authorization' => 'Bearer your-token',
            ],
            'timeout' => 10,
            'note' => '创建订单',
        ]);

        return json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
    }
}

或者使用 Hyperf 注解:

use Hyperf\Di\Annotation\Inject;
use Khyzd\HyperfGuzzleLogger\GuzzleLoggerInterface;

#[Inject]
protected GuzzleLoggerInterface $http;

可用方法

$http->request('GET', $url, $options);
$http->get($url, $options);
$http->head($url, $options);
$http->post($url, $options);
$http->put($url, $options);
$http->patch($url, $options);
$http->delete($url, $options);
$http->options($url, $options);

$http->send($psr7Request, $options);
$http->sendAsync($psr7Request, $options);

$http->requestAsync('POST', $url, $options);
$http->getAsync($url, $options);
$http->postAsync($url, $options);
// ...

异步请求会在 Promise 完成时写日志:

$response = $http->postAsync('https://api.example.com/orders', [
    'json' => $payload,
    'note' => '异步创建订单',
])->wait();

直接使用 Guzzle Client

$client = $http->client();

$response = $client->get('https://api.example.com/users', [
    'note' => '拉取用户列表',
]);

client() 返回的是带日志中间件的 LoggingClient,直接通过它发起的请求同样会记录。

自定义 Client

需要独立的 base URI、超时、代理等配置时,可以注入 ClientFactory:

use Khyzd\HyperfGuzzleLogger\Client\ClientFactory;

final class ApiClient
{
    public function __construct(private ClientFactory $clientFactory)
    {
    }

    public function client(): \GuzzleHttp\ClientInterface
    {
        return $this->clientFactory->create([
            'base_uri' => 'https://api.example.com',
            'timeout' => 5.0,
            'headers' => [
                'X-App' => 'demo',
            ],
        ]);
    }
}

传入的配置会与 guzzle_logger.client 合并,日志中间件会自动附加。

注意:直接 new \GuzzleHttp\Client() 不会经过本包,也不会记录日志。 请使用 GuzzleLoggerInterface、ClientFactory 或 $http->client()。

日志写入失败处理

默认 throw_on_log_failure = false:

  • 数据库断开、表不存在、字段超长等日志异常不会影响 HTTP 响应。
  • 包会通过 Hyperf 框架内置的 StdoutLoggerInterface 输出一条 warning(通常由 Supervisor/Docker 收集到 stdout)。

如果希望日志失败时让业务请求也失败,设置:

GUZZLE_LOGGER_THROW_ON_LOG_FAILURE=true

性能与异步落库

默认在请求完成的 Promise 回调中同步写数据库,日志写入发生在业务方拿到响应之前。每次写入约增加一次 insert 的网络往返。

如果 QPS 较高,可以实现自己的 LogRecorderInterface,把日志投递到队列/Redis,由消费者异步落库:

<?php

declare(strict_types=1);

namespace App\Recorder;

use Khyzd\HyperfGuzzleLogger\Contract\LogRecorderInterface;
use Khyzd\HyperfGuzzleLogger\Dto\LogEntry;

final class QueueLogRecorder implements LogRecorderInterface
{
    public function record(LogEntry $entry): void
    {
        // $entry->method / url / options / returnCode / response / time / note
        // $entry->createdAt / updatedAt

        // 例如:投递到 Hyperf 异步队列
    }
}

在 config/autoload/dependencies.php 中替换绑定:

return [
    \Khyzd\HyperfGuzzleLogger\Contract\LogRecorderInterface::class => \App\Recorder\QueueLogRecorder::class,
];

事务注意事项

  • 日志写入使用 guzzle_logger.connection 指定的当前数据库连接。如果业务代码处于事务中,日志 insert 会加入该事务;业务事务回滚时,日志也会一起回滚。
  • 自动建表执行 CREATE TABLE,MySQL 会隐式提交当前事务。如果业务大量使用事务,建议:

    1. 设置 auto_create_table = false;
    2. 通过定时任务提前执行 guzzle-logger:create-table 创建当月/下月分表;
    3. 或使用自定义异步 LogRecorderInterface 独立落库。

安全说明

  • sensitive_keys 中的 key 在 options 和 url query string 中会被替换为 ***。
  • 匹配不区分大小写,并忽略 -、_、空格,因此 Authorization、X-Api-Key、access_token 等都能命中。
  • 支持嵌套参数、header、query、form_params 和 multipart 字段。
  • 手动传入的原始 body 字符串不会解析其中的敏感内容,只对结构化字段脱敏。
  • 默认脱敏 authorization、cookie、token、password、secret 以及 Guzzle 的 auth 选项。
  • 如果不需要对 URL 中的 query 参数脱敏,可设置 redact_url_query = false。
  • 日志表可能包含用户隐私或业务敏感数据,请按实际合规要求配置保留时长和脱敏字段。

在 Hyperf 项目中做冒烟测试

composer test 只能验证包内逻辑,要确认真实 Hyperf 项目中的 DI、建表和写入都正常,建议做一次端到端冒烟测试。

1. 用 path repository 临时安装

把下面的 url 换成这个包的实际路径,然后在 Hyperf 项目的 composer.json 中加入:

{
    "repositories": [
        {
            "type": "path",
            "url": "/path/to/hyperf-guzzle-logger",
            "options": {
                "symlink": true
            }
        }
    ]
}

然后执行:

composer require khyzd/hyperf-guzzle-logger:@dev
composer dump-autoload

确认包已经安装:

composer show khyzd/hyperf-guzzle-logger

2. 发布并修改配置

php bin/hyperf.php vendor:publish khyzd/hyperf-guzzle-logger

第一次验证建议先用单表,避免查错表:

// config/autoload/guzzle_logger.php
return [
    'connection' => 'default',
    'table' => 'guzzle_log',
    'partition' => 'none',
    'auto_create_table' => true,
    'timezone' => 'Asia/Shanghai',
];

确保 config/autoload/databases.php 中的 default 连接可正常使用。

3. 添加临时测试命令

创建 app/Command/GuzzleLoggerSmokeCommand.php:

<?php

declare(strict_types=1);

namespace App\Command;

use Hyperf\Command\Command;
use Khyzd\HyperfGuzzleLogger\GuzzleLoggerInterface;

final class GuzzleLoggerSmokeCommand extends Command
{
    protected ?string $name = 'test:guzzle-logger';

    public function __construct(private GuzzleLoggerInterface $http)
    {
        parent::__construct();
    }

    public function handle(): int
    {
        $response = $this->http->get('https://example.com', [
            'headers' => [
                'Authorization' => 'Bearer smoke-secret',
            ],
            'query' => [
                'access_token' => 'query-secret',
                'page' => 1,
            ],
            'note' => 'hyperf-smoke-test',
            'timeout' => 10,
        ]);

        $this->line('HTTP status: ' . $response->getStatusCode());
        $this->line('Response: ' . substr((string) $response->getBody(), 0, 200));

        return self::SUCCESS;
    }
}

把命令加入 config/autoload/commands.php:

return [
    \App\Command\GuzzleLoggerSmokeCommand::class,
];

4. 执行并检查数据库

php bin/hyperf.php test:guzzle-logger

然后查询:

SHOW TABLES LIKE 'guzzle_log%';

SELECT id, method, url, return_code, note, time, created_at
FROM guzzle_log
ORDER BY id DESC
LIMIT 1;

SELECT options, response
FROM guzzle_log
ORDER BY id DESC
LIMIT 1\G

检查以下内容:

  • 命令正常输出 HTTP 200,且没有 DI 报错。
  • 表中新增了一行。
  • method 为 GET,url 为 https://example.com?...。
  • return_code 为 200,note 为 hyperf-smoke-test。
  • options 中包含 headers、query,不包含 note。
  • Authorization 和 access_token 的值显示为 ***。
  • time 为数字,created_at 为当前时间。

5. 再验证分表

把配置改为:

'partition' => 'monthly',

再次执行命令,然后查询当前月份的表,例如:

SHOW TABLES LIKE 'guzzle_log_%';

SELECT id, url, return_code, note
FROM guzzle_log_202611
ORDER BY id DESC
LIMIT 1;

表名按请求时间和 timezone 生成,实际月份以运行时为准。

6. 没有写入数据库时怎么排查

  • 确认 enabled 为 true,且修改的是 config/autoload/guzzle_logger.php。
  • 确认 connection 指向 config/autoload/databases.php 中真实存在的连接。
  • 确认查询的是正确的表:partition=none 查 guzzle_log,monthly 查 guzzle_log_YYYYMM。
  • 如果 auto_create_table=true 但数据库账号没有 CREATE TABLE 权限,执行:
    php bin/hyperf.php guzzle-logger:create-table
    

    或关闭自动建表后手动创建/迁移。

  • 查看命令输出和 runtime/logs,日志写入失败会输出 Guzzle request log could not be written... warning。
  • 修改代码后执行 composer dump-autoload,并重启 Hyperf 进程。

7. 验证异常记录

把测试地址换成会返回 500 的接口(或项目内一个 500 路由),此时 Guzzle 会抛异常:

try {
    $this->http->get('https://httpbin.org/status/500', ['note' => 'error-test']);
} catch (\Throwable $e) {
    $this->line('Exception: ' . $e->getMessage());
}

数据库中应出现 return_code = 500 的记录,response 包含异常信息和响应体。

测试

composer install
composer test

当前测试覆盖配置解析、分表规则、DDL、参数脱敏、响应体保护、成功/异常/异步请求、日志失败降级等。

测试环境为 PHP 8.3 + Hyperf 3.2;代码同时兼容 PHP 8.0 / Hyperf 3.0。

限制

  • 自动建表和迁移使用内置的 MySQL DDL;如需自定义索引/字段,请设置 auto_create_table = false 并自行建表。
  • 自动建表仅支持 MySQL;其他数据库请设置 auto_create_table = false 并自行建表。
  • time 字段以「秒」为单位的字符串保存。
  • created_at / updated_at 按配置时区格式化。如果 MySQL 会话时区与 PHP 时区不同,请注意 timestamp 字段的转换行为。
  • options 列是 TEXT,上限 65535 字节;默认会截断。
  • response 默认最多记录 1 MB,可通过 max_response_length 调整,0 表示不限制。
  • stream / sink 请求不会记录响应体,避免破坏流式响应。
  • 日志写入是同步的;高并发场景建议使用自定义异步 Recorder。
  • 迁移回滚只会删除基础表;已经创建的分表不会被自动删除。

License

MIT