goletter / hyperf-sls-log
基于Hyperf3.1中的阿里云SLS服务扩展
Installs: 3
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/goletter/hyperf-sls-log
Requires
- php: >=8.1
- ext-json: *
- ext-zlib: *
- hyperf/contract: ~3.1.0
- hyperf/guzzle: ~3.1.0
- psr/container: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.9
- hyperf/config: ~3.1.0
- hyperf/di: ~3.1.0
- hyperf/event: ~3.1.0
- hyperf/framework: ~3.1.0
- hyperf/process: ~3.1.0
- hyperf/utils: ~3.1.0
- malukenho/docheader: ^0.1.6
- mockery/mockery: ^1.0
- monolog/monolog: ^2.0
README
- 基于Hyperf3.1中的阿里云SLS服务扩展
运行环境
确保操作环境满足以下要求:
- PHP >= 8.1
- Swoole PHP extension >= 5.4, and Disabled
Short Name - OpenSSL PHP extension
- JSON PHP extension
安装
$ composer require "goletter/hyperf-sls-log"
配置
.env 中配置
ALIYUN_SLS_ENDPOINT=cn-shenzhen.log.aliyuncs.com ALIYUN_SLS_PROJECT= ALIYUN_SLS_LOGSTORE= ALIYUN_SLS_ACCESS_KEY_ID= ALIYUN_SLS_ACCESS_KEY_SECRET=
默认情况下,配置文件为 config/autoload/aliyun_sls.php , 如文件不存在,则在项目根目录下执行 php bin/hyperf.php vendor:publish nahuomall/aliyun-sls-log
<?php return [ 'endpoint' => env('ALIYUN_SLS_ENDPOINT', 'cn-beijing.log.aliyuncs.com'), 'access_key_id' => env('ALIYUN_SLS_ACCESS_KEY_ID', ''), 'access_key_secret' => env('ALIYUN_SLS_ACCESS_KEY_SECRET', ''), 'project' => env('ALIYUN_SLS_PROJECT', ''), 'log_store' => env('ALIYUN_SLS_LOGSTORE', ''), ];
使用,替换你的logger配置文件default驱动
<?php 'default' => [ 'driver' => LoggerFactory::class, 'name' => 'sls', 'handlers' => [ // 添加阿里云 SLS Handler [ 'class' => SLSMonologHandler::class, 'constructor' => [ 'endpoint' => env('ALIYUN_SLS_ENDPOINT'), 'accessKeyId' => env('ALIYUN_SLS_ACCESS_KEY_ID'), 'accessKeySecret' => env('ALIYUN_SLS_ACCESS_KEY_SECRET'), 'project' => env('ALIYUN_SLS_PROJECT', 'supply'), 'logstore' => env('ALIYUN_SLS_LOGSTORE', 'supply'), 'topic' => env('ALIYUN_SLS_TOPIC', 'supply'), 'level' => Logger::INFO, ], ], ], ],
创建slsHandler(注意修改命名空间,并修改logger中的命名空间)
<?php declare(strict_types=1); /** * This file is part of MineAdmin. * * @link https://www.mineadmin.com * @document https://doc.mineadmin.com * @contact root@imoi.cn * @license https://github.com/mineadmin/MineAdmin/blob/master/LICENSE */ namespace App\Kernel\Log; use Hyperf\Di\Annotation\Inject; use Hyperf\Contract\ContainerInterface; use Monolog\Handler\AbstractProcessingHandler; use Monolog\LogRecord; use Goletter\AliYun\Sls\ClientInterface; use Goletter\AliYun\Sls\LogItem; use Goletter\AliYun\Sls\Request\PutLogsRequest; class SLSMonologHandler extends AbstractProcessingHandler { #[Inject] protected ClientInterface $client; protected ContainerInterface $container; public function __construct(ContainerInterface $container, $level = \Monolog\Logger::DEBUG, bool $bubble = true) { $this->container = $container; parent::__construct($level, $bubble); } protected function write(LogRecord $record): void { try { // 转换日志数据 $logData = $this->formatLogRecord($record); // 创建日志条目 $logItem = make(LogItem::class, [time(), $logData]); // 构建请求 $config = config('aliyun_sls'); $putLogsRequest = $this->container->get(PutLogsRequest::class); $putLogsRequest->setProject($config['project']); $putLogsRequest->setLogstore($config['log_store']); $putLogsRequest->setTopic($config['topic'] ?? 'supply'); $putLogsRequest->setShardKey((string)(microtime(true) * 10000)); $putLogsRequest->setLogItems([$logItem]); // 使用协程池控制并发 co(function () use ($putLogsRequest, $logData) { try { $this->client->putLogs($putLogsRequest); } catch (\Throwable $e) { $this->handleSlsError($e, $logData); } }); } catch (\Throwable $e) { $this->handleUnexpectedError($e); } } private function formatLogRecord(LogRecord $record): array { // 过滤 context 和 extra,避免敏感信息 $context = array_filter($record->context, function ($key) { return !in_array($key, ['password', 'token', 'secret']); }, ARRAY_FILTER_USE_KEY); // 确保 contents 是字符串键值对 foreach ($context as $key => &$value) { ! is_string($value) ? $value = (string)$value : null; } $data = [ 'message' => (string) $record->message, 'context' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'level' => (string) $record->level->getName(), 'channel' => (string) $record->channel, 'datetime' => $record->datetime->format('c'), ]; return $data; } private function handleSlsError(\Throwable $e, array $logData): void { // 获取 fallback 日志路径配置 $fallbackLogPath = BASE_PATH . '/runtime/logs/sls_fallback.log'; if (config('aliyun_sls.fallback_log_path')) { $fallbackLogPath = config('aliyun_sls.fallback_log_path'); } // 记录详细错误信息 $errorMessage = \sprintf( "[%s][ERROR] SLS写入失败: %s\n日志内容: %s\n%s\n", date('Y-m-d H:i:s'), $e->getMessage(), $this->safeJsonEncode($logData), $e->getTraceAsString() ); // 安全写入日志 if (@file_put_contents($fallbackLogPath, $errorMessage, \FILE_APPEND) === false) { // 如果 fallback 日志也失败,考虑触发通知或记录到其他位置 error_log('SLS fallback log write failed: ' . $e->getMessage()); } } private function handleUnexpectedError(\Throwable $e): void { error_log("Unexpected error in SLSMonologHandler: " . $e->getMessage()); } private function safeJsonEncode(array $data): string { $encoded = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($encoded === false) { // 处理 json_encode 失败的情况 return '[JSON ENCODE FAILED]'; } return $encoded; } }
测试
$logger = make(\Psr\Log\LoggerInterface::class); $logger->info('测试日志');