khyzd / hyperf-guzzle-logger
一个用于Hyperf 3的Guzzle包装器,可将每个HTTP请求和响应记录到数据库表中,并可选择每月/每年进行数据分片。
Requires
- php: >=8.0
- ext-json: *
- ext-mbstring: *
- guzzlehttp/guzzle: ^7.5
- hyperf/command: ^3.0
- hyperf/config: ^3.0
- hyperf/contract: ^3.0
- hyperf/db-connection: ^3.0
- psr/container: ^1.1 || ^2.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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 |
| Hyperf | 3.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.phpmigrations/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 | 表名示例 |
|---|---|
none | guzzle_log |
monthly | guzzle_log_202611 |
yearly | guzzle_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_code | HTTP 状态码;网络异常时为 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 会隐式提交当前事务。如果业务大量使用事务,建议:- 设置
auto_create_table = false; - 通过定时任务提前执行
guzzle-logger:create-table创建当月/下月分表; - 或使用自定义异步
LogRecorderInterface独立落库。
- 设置
安全说明
sensitive_keys中的 key 在options和urlquery 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