Senior Software Engineer · Technical Coding

Apple 请求日志解析:合法记录、4xx/5xx 与失败率

先确定日志语法和失败率分母,再单遍解析、校验与计数;附 11,111 组独立序列核验。

题目重点:先确定日志语法和失败率分母,再单遍解析、校验与计数;附 11,111 组独立序列核验。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练格式为四字段以 TAB 分隔:IP、带时区的 ISO-8601 时间、以 `/` 开头且无空白的 path、三位十进制状态码 100–599。IPv4 与 IPv6 都允许;生产系统应签真实日志协议。
  • `total` 代表输入行数;valid/invalid 分开统计;失败率的分母是有效请求数,分子为有效请求中 4xx+5xx。无有效请求时训练版返回 0.0,报表也保留分母 0 供 UI 显示 N/A。

解题路线

  1. 先签 schema 与非法行处理,否则分隔符、timestamp 时区和百分比分母不同会导致完全不同答案。
  2. 逐行拆成恰好四字段;用 IP 解析器而非仅靠点号,时间须能解析且带时区,path 与状态码检查格式和范围。
  3. 只对合法请求累加 valid 与 4xx/5xx;最后用计数算 failure rate,避免每行浮点累加。非法行数由总行数减 valid 得出。
  4. 对 0–4 行的 11,111 组组合序列,用与实现独立的正则/规则参考实现核验,覆盖非法 IP、时间、path、status、零分母与混合状态。

手动走一遍

三行有效记录的状态分别 200、404、503,再加一行坏 IP:total=4、valid=3、invalid=1、4xx=1、5xx=1、failure_rate=2/3。若业务要求非法行也计入请求总数,应改变指标命名与分母,而不是悄悄让错误率下降。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from ipaddress import ip_address


@dataclass(frozen=True)
class LogReport:
    total: int
    valid: int
    invalid: int
    client_errors: int
    server_errors: int
    failure_rate: float


def summarize_logs(lines: list[str]) -> LogReport:
    """Training format: IP TAB ISO-8601-UTC TAB /path TAB status.

    Failure rate denominator is valid requests only; 4xx and 5xx fail.
    """
    valid = client_errors = server_errors = 0
    for line in lines:
        fields = line.split("\t")
        if len(fields) != 4:
            continue
        ip_text, stamp, path, status_text = fields
        try:
            ip_address(ip_text)
            parsed = datetime.fromisoformat(stamp.replace("Z", "+00:00"))
            if parsed.tzinfo is None or parsed.utcoffset() is None:
                continue
            if not path.startswith("/") or any(char.isspace() for char in path):
                continue
            if len(status_text) != 3 or not status_text.isascii() or not status_text.isdigit():
                continue
            status = int(status_text)
            if not 100 <= status <= 599:
                continue
        except ValueError:
            continue
        valid += 1
        client_errors += 400 <= status <= 499
        server_errors += 500 <= status <= 599
    failures = client_errors + server_errors
    return LogReport(len(lines), valid, len(lines) - valid, client_errors,
                     server_errors, failures / valid if valid else 0.0)

复杂度

设总字符数为 L,单遍解析时间 O(L),计数空间 O(1)(不保存所有行);输入列表本身不算新增空间。生产日志可逐行流式读取,不能对无界输入先 `readlines()`。

易错点与追问

容易失分的边界

  • 未经确认把空格 `split()` 当原日志语法,带空格 URL/时间戳会歧义。
  • 非法行也进失败率分母却只对合法行统计失败,造成误导。
  • 把所有非 2xx 当失败,却仍只报 4xx/5xx 两类。
  • 状态码接受 99 或 600,或字符串 `四零四` 被粗糙判断误收。
  • 把时间戳去掉时区后无法统一排序与聚合。

继续深挖

  • 若日志里 path 可带查询字符串和转义空白,该如何解析?
  • 如何按 5 分钟窗口输出失败率并处理乱序事件?
  • 大量非法行时如何限速采样并保护敏感 path?
  • 若要按 IP 或 API 分组,内存上限与基数爆炸怎么处理?
✓已复制微信号!