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

v1.0.0 2026-01-07 09:28 UTC

This package is auto-updated.

Last update: 2026-01-07 09:33:29 UTC


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('测试日志');