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:
augushong
2026-07-28 06:57:13 +08:00
parent 0f9f12174c
commit f1a2e8fd5d
10 changed files with 49 additions and 49 deletions

View File

@@ -0,0 +1,239 @@
<?php
namespace base\common\service;
use DateTime;
/**
* Nginx access log 解析器Base 层).
*
* 匹配 analytics log_format11 字段):
* 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();
}
}