Files
ulthon_admin/extend/base/common/service/NginxLogParserServiceBase.php
augushong f1a2e8fd5d 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 后缀)
2026-07-28 06:57:13 +08:00

240 lines
7.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
}
}