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