#feature: add AI log analysis & some bugfix

This commit is contained in:
2026-01-14 13:58:50 +08:00
parent e479ed02ea
commit ae6c169f5f
33 changed files with 3898 additions and 164 deletions

292
app/Clients/SlsClient.php Normal file
View File

@@ -0,0 +1,292 @@
<?php
namespace App\Clients;
use Aliyun_Log_Client;
use Aliyun_Log_Models_GetLogsRequest;
use Aliyun_Log_Models_GetHistogramsRequest;
use Aliyun_Log_Exception;
use RuntimeException;
class SlsClient
{
private ?Aliyun_Log_Client $client = null;
private string $project;
private array $logstores;
public function __construct()
{
$config = config('services.sls');
if (empty($config['endpoint']) || empty($config['access_key_id']) || empty($config['access_key_secret'])) {
return;
}
$this->project = $config['project'] ?? '';
// 解析 logstore 配置,支持逗号分隔的多个 logstore
$logstoreConfig = $config['logstore'] ?? '';
if (!empty($logstoreConfig)) {
// 如果包含逗号,说明是多个 logstore
if (str_contains($logstoreConfig, ',')) {
$this->logstores = array_map('trim', explode(',', $logstoreConfig));
} else {
$this->logstores = [$logstoreConfig];
}
} else {
$this->logstores = [];
}
$this->client = new Aliyun_Log_Client(
$config['endpoint'],
$config['access_key_id'],
$config['access_key_secret'],
$config['security_token'] ?: ''
);
}
public function isConfigured(): bool
{
// 只要有 client、project并且至少有一个 logstore 就算配置完成
return $this->client !== null
&& !empty($this->project)
&& !empty($this->logstores);
}
/**
* 获取配置的所有 logstore
*/
public function getLogstores(): array
{
return $this->logstores;
}
/**
* 获取默认的 logstore第一个
*/
private function getDefaultLogstore(): string
{
if (empty($this->logstores)) {
throw new RuntimeException('没有配置可用的 logstore');
}
return $this->logstores[0];
}
/**
* 查询日志
*
* @param int $from 开始时间戳
* @param int $to 结束时间戳
* @param string|null $query SLS 查询语句
* @param int $offset 偏移量
* @param int $limit 返回数量
* @param string|null $logstore 可选的 logstore不传则使用默认
* @return array{logs: array, count: int, complete: bool}
*/
public function getLogs(
int $from,
int $to,
?string $query = null,
int $offset = 0,
int $limit = 100,
?string $logstore = null
): array {
$this->ensureConfigured();
$request = new Aliyun_Log_Models_GetLogsRequest(
$this->project,
$logstore ?? $this->getDefaultLogstore(),
$from,
$to,
'',
$query ?? '*',
$limit,
$offset,
false
);
try {
$response = $this->client->getLogs($request);
$logs = [];
foreach ($response->getLogs() as $log) {
$logs[] = $log->getContents();
}
return [
'logs' => $logs,
'count' => $response->getCount(),
'complete' => $response->isCompleted(),
];
} catch (Aliyun_Log_Exception $e) {
throw new RuntimeException(
"SLS 查询失败: [{$e->getErrorCode()}] {$e->getErrorMessage()}",
0,
$e
);
}
}
/**
* 获取日志分布直方图
*
* @param int $from 开始时间戳
* @param int $to 结束时间戳
* @param string|null $query SLS 查询语句
* @param string|null $logstore 可选的 logstore
* @return array{histograms: array, count: int, complete: bool}
*/
public function getHistograms(
int $from,
int $to,
?string $query = null,
?string $logstore = null
): array {
$this->ensureConfigured();
$request = new Aliyun_Log_Models_GetHistogramsRequest(
$this->project,
$logstore ?? $this->getDefaultLogstore(),
$from,
$to,
'',
$query ?? '*'
);
try {
$response = $this->client->getHistograms($request);
$histograms = [];
foreach ($response->getHistograms() as $histogram) {
$histograms[] = [
'from' => $histogram->getFrom(),
'to' => $histogram->getTo(),
'count' => $histogram->getCount(),
'complete' => $histogram->isCompleted(),
];
}
return [
'histograms' => $histograms,
'count' => $response->getTotalCount(),
'complete' => $response->isCompleted(),
];
} catch (Aliyun_Log_Exception $e) {
throw new RuntimeException(
"SLS 直方图查询失败: [{$e->getErrorCode()}] {$e->getErrorMessage()}",
0,
$e
);
}
}
/**
* 分页获取所有日志
*
* @param int $from 开始时间戳
* @param int $to 结束时间戳
* @param string|null $query SLS 查询语句
* @param int $maxLogs 最大返回日志数
* @param string|null $logstore 可选的 logstore
* @return array
*/
public function getAllLogs(
int $from,
int $to,
?string $query = null,
int $maxLogs = 1000,
?string $logstore = null
): array {
$allLogs = [];
$offset = 0;
$batchSize = 100;
while (count($allLogs) < $maxLogs) {
$result = $this->getLogs($from, $to, $query, $offset, $batchSize, $logstore);
$allLogs = array_merge($allLogs, $result['logs']);
if ($result['complete'] || count($result['logs']) < $batchSize) {
break;
}
$offset += $batchSize;
}
return array_slice($allLogs, 0, $maxLogs);
}
/**
* 测试连接
*/
public function testConnection(): bool
{
if (!$this->isConfigured()) {
return false;
}
try {
$now = time();
$this->getLogs($now - 60, $now, '*', 0, 1);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* 从多个 logstore 获取日志
*
* @param int $from 开始时间戳
* @param int $to 结束时间戳
* @param string|null $query SLS 查询语句
* @param int $maxLogs 最大返回日志数
* @param array|null $logstores 要查询的 logstore 列表,不传则使用配置的所有 logstore
* @return array logstore 分组的日志数据 ['logstore_name' => ['logs' => [...], 'count' => 100]]
*/
public function getAllLogsFromMultipleStores(
int $from,
int $to,
?string $query = null,
int $maxLogs = 1000,
?array $logstores = null
): array {
$this->ensureConfigured();
$targetLogstores = $logstores ?? $this->getLogstores();
if (empty($targetLogstores)) {
throw new RuntimeException('没有配置可用的 logstore');
}
$results = [];
foreach ($targetLogstores as $logstore) {
try {
$logs = $this->getAllLogs($from, $to, $query, $maxLogs, $logstore);
$results[$logstore] = [
'logs' => $logs,
'count' => count($logs),
'success' => true,
];
} catch (\Exception $e) {
$results[$logstore] = [
'logs' => [],
'count' => 0,
'success' => false,
'error' => $e->getMessage(),
];
}
}
return $results;
}
private function ensureConfigured(): void
{
if (!$this->isConfigured()) {
throw new RuntimeException('SLS 客户端未配置,请检查 .env 中的 SLS_* 配置项');
}
}
}