mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 20:55:32 +08:00
fix(nginx-log): service 文件加 Service 后缀(命名规范合规)
F2 Code quality review 发现:3 个新增 service 未遵守 .agents/rules/ulthon-naming-convention.md 第 19 行规定 (service 模块文件名需带 Service 后缀)。 变更: - 重命名 6 个文件(3 Base + 3 App): NginxLogParser(Base)→NginxLogParserService(Base) NginxLogReader(Base)→NginxLogReaderService(Base) NginxLogAggregator(Base)→NginxLogAggregatorService(Base) - 同步类名、use、类型 hint、new 引用 - NginxLogReaderServiceBase 异常消息前缀同步加 Service - controller Base 引用更新(NginxLogImportBase / NginxLogStatAggregateBase) 验证: - php -l 全部 8 个文件语法通过 - grep 残留旧名 = 0 结果(24 处引用全部带 Service 后缀)
This commit is contained in:
239
extend/base/common/service/NginxLogParserServiceBase.php
Normal file
239
extend/base/common/service/NginxLogParserServiceBase.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Nginx access log 解析器(Base 层).
|
||||
*
|
||||
* 匹配 analytics log_format(11 字段):
|
||||
* remote_addr - remote_user [time_local] "request" status body_bytes_sent "referer" "ua" request_time upstream_response_time bytes_sent
|
||||
*
|
||||
* 示例行:
|
||||
* 192.168.32.1 - - [27/Jul/2026:16:20:15 +0000] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (...)" 5.085 5.086 0
|
||||
*
|
||||
* 业务侧通过 app/common/service/NginxLogParserService 覆盖本类方法.
|
||||
*/
|
||||
class NginxLogParserServiceBase
|
||||
{
|
||||
/** 爬虫 UA 关键词(大小写不敏感匹配) */
|
||||
public const SPIDER_KEYWORDS = [
|
||||
'Googlebot', 'Baiduspider', 'bingbot', 'Sogou', '360Spider',
|
||||
'YisouSpider', 'PetalBot', 'AhrefsBot', 'semrushbot', 'DotBot',
|
||||
];
|
||||
|
||||
/** 静态资源扩展名 */
|
||||
public const STATIC_EXTS = [
|
||||
'css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'ico',
|
||||
'woff', 'woff2', 'ttf', 'svg', 'map',
|
||||
];
|
||||
|
||||
/** URI 最大存储长度 */
|
||||
protected const URI_MAX_LENGTH = 255;
|
||||
|
||||
/** UA 最大存储长度 */
|
||||
protected const UA_MAX_LENGTH = 500;
|
||||
|
||||
/** analytics log_format 正则(11 捕获组) */
|
||||
protected const LINE_PATTERN = '/^(\S+) - (\S+) \[(.+?)\] "([^"]*)" (\d+) (\d+) "([^"]*)" "([^"]*)" (\S+) (\S+) (\d+)$/';
|
||||
|
||||
/** $request 拆分正则:METHOD URI HTTP/version */
|
||||
protected const REQUEST_PATTERN = '/^(\S+)\s+(\S+)\s+(HTTP\/\S+)$/';
|
||||
|
||||
/**
|
||||
* 解析单行 nginx access log.
|
||||
*
|
||||
* @param string $line 原始日志行
|
||||
*
|
||||
* @return array|null 解析成功返回字段数组,正则不匹配或 request 畸形返回 null
|
||||
*/
|
||||
public function parse(string $line): ?array
|
||||
{
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preg_match(self::LINE_PATTERN, $line, $m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// $m[1..11] = 11 个捕获组
|
||||
$remoteAddr = $m[1];
|
||||
$remoteUser = $m[2];
|
||||
$timeLocalStr = $m[3];
|
||||
$request = $m[4];
|
||||
$status = (int) $m[5];
|
||||
$bodyBytes = (int) $m[6];
|
||||
$referer = $m[7];
|
||||
$userAgent = $m[8];
|
||||
$requestTime = $m[9];
|
||||
$upstreamTime = $m[10];
|
||||
$bytesSent = (int) $m[11];
|
||||
|
||||
// time_local 字符串 → Unix 时间戳(DateTime 自动转业务时区)
|
||||
$timeLocal = $this->parseTimeLocal($timeLocalStr);
|
||||
if ($timeLocal === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 拆分 request → method / uri / http_version
|
||||
[$method, $uri, $httpVersion] = $this->splitRequest($request);
|
||||
if ($method === null) {
|
||||
// 畸形 request(含空格等),无法提取核心字段
|
||||
return null;
|
||||
}
|
||||
|
||||
// URI 截断 + 拆 query_string
|
||||
$uri = substr($uri, 0, self::URI_MAX_LENGTH);
|
||||
$queryString = '';
|
||||
$qsPos = strpos($uri, '?');
|
||||
if ($qsPos !== false) {
|
||||
$queryString = substr($uri, $qsPos + 1);
|
||||
$uri = substr($uri, 0, $qsPos);
|
||||
}
|
||||
|
||||
// UA 截断
|
||||
$userAgent = substr($userAgent, 0, self::UA_MAX_LENGTH);
|
||||
|
||||
// referer 域名提取
|
||||
$refererDomain = $this->extractRefererDomain($referer);
|
||||
|
||||
// UA 分类
|
||||
[$uaType, $uaName] = $this->classifyUa($userAgent);
|
||||
|
||||
return [
|
||||
'remote_addr' => $remoteAddr,
|
||||
'remote_user' => $remoteUser,
|
||||
'time_local' => $timeLocal,
|
||||
'method' => $method,
|
||||
'uri' => $uri,
|
||||
'query_string' => $queryString,
|
||||
'http_version' => $httpVersion,
|
||||
'status' => $status,
|
||||
'body_bytes_sent' => $bodyBytes,
|
||||
'http_referer' => $referer,
|
||||
'referer_domain' => $refererDomain,
|
||||
'http_user_agent' => $userAgent,
|
||||
'ua_type' => $uaType,
|
||||
'ua_name' => $uaName,
|
||||
'request_time' => $requestTime !== '-' ? (float) $requestTime : 0.0,
|
||||
'upstream_response_time' => $upstreamTime !== '-' ? (float) $upstreamTime : null,
|
||||
'bytes_sent' => $bytesSent,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 首行格式自检:判断给定行是否匹配 analytics log_format.
|
||||
*/
|
||||
public function isFormatMatch(string $sampleLine): bool
|
||||
{
|
||||
return preg_match(self::LINE_PATTERN, trim($sampleLine)) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* UA 分类.
|
||||
*
|
||||
* @param string $ua User-Agent 字符串
|
||||
*
|
||||
* @return array{0:string,1:string} [ua_type, ua_name];spider 时 ua_name = 匹配的关键词
|
||||
*/
|
||||
public function classifyUa(string $ua): array
|
||||
{
|
||||
if ($ua === '' || $ua === '-') {
|
||||
return ['unknown', ''];
|
||||
}
|
||||
|
||||
foreach (self::SPIDER_KEYWORDS as $keyword) {
|
||||
if (stripos($ua, $keyword) !== false) {
|
||||
return ['spider', $keyword];
|
||||
}
|
||||
}
|
||||
|
||||
return ['browser', ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 referer 域名.
|
||||
*
|
||||
* @param string $referer 原始 referer 值
|
||||
*
|
||||
* @return string 域名;空或 "-" 返回 'direct';解析失败返回 'unknown'
|
||||
*/
|
||||
public function extractRefererDomain(string $referer): string
|
||||
{
|
||||
if ($referer === '' || $referer === '-') {
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
$host = parse_url($referer, PHP_URL_HOST);
|
||||
if ($host === false || $host === null || $host === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分 request → [method, uri, http_version].
|
||||
*
|
||||
* @param string $request 原始 request 字段("GET /path HTTP/1.1")
|
||||
*
|
||||
* @return array{0:?string,1:?string,2:?string} 匹配失败三项均为 null
|
||||
*/
|
||||
public function splitRequest(string $request): array
|
||||
{
|
||||
if (preg_match(self::REQUEST_PATTERN, $request, $m)) {
|
||||
return [$m[1], $m[2], $m[3]];
|
||||
}
|
||||
|
||||
return [null, null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 URI 是否为静态资源.
|
||||
*
|
||||
* 取 URI 路径部分的扩展名,命中 STATIC_EXTS 返回 true.
|
||||
*/
|
||||
public function isStaticResource(string $uri): bool
|
||||
{
|
||||
// 去掉 query string
|
||||
$path = $uri;
|
||||
$qsPos = strpos($path, '?');
|
||||
if ($qsPos !== false) {
|
||||
$path = substr($path, 0, $qsPos);
|
||||
}
|
||||
|
||||
$dotPos = strrpos($path, '.');
|
||||
if ($dotPos === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ext = strtolower(substr($path, $dotPos + 1));
|
||||
|
||||
return in_array($ext, self::STATIC_EXTS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* time_local 字符串 → Unix 时间戳.
|
||||
*
|
||||
* 格式示例:27/Jul/2026:16:20:15 +0000
|
||||
* DateTime::createFromFormat 自动处理时区偏移,
|
||||
* getTimestamp() 返回 UTC Unix 时间戳,
|
||||
* 后续 date() 按业务时区(Asia/Shanghai)格式化.
|
||||
*
|
||||
* @param string $timeLocalStr nginx time_local 字段值
|
||||
*
|
||||
* @return int|null Unix 时间戳;解析失败返回 null
|
||||
*/
|
||||
protected function parseTimeLocal(string $timeLocalStr): ?int
|
||||
{
|
||||
$dt = DateTime::createFromFormat('d/M/Y:H:i:s O', $timeLocalStr);
|
||||
if ($dt === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $dt->getTimestamp();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user