二、接口数据采集:从日志到可分析的数据
说实话,接口行为基线这事儿,门槛不在算法,也不在模型。
门槛在哪?在数据。我见过太多团队,算法选得漂亮,模型调得飞起,结果一上生产,全是垃圾进垃圾出。为什么?数据采集那一步就埋了雷。
所以这一章,咱们把数据采集这件事聊透。你想想看,你的检测系统再聪明,如果喂进去的日志是残缺的、错位的、甚至干脆丢了,那后面的基线建立和异常检测,全白搭。
2.1 HTTP请求/响应日志:最原始的金矿
每个接口调用,都会产生一对请求和响应。这是最原始的数据源。我个人习惯,先搞清楚日志里到底有什么。
2.1.1 请求端该抓什么
- 请求方法:GET、POST、PUT、DELETE,别小看这个字段,它决定了行为的语义。
- 请求路径:/api/v1/orders,这就定义了操作对象。
- 请求头:Content-Type、Authorization、User-Agent,这里藏着调用方的身份和意图。
- 请求参数:Query String + Request Body,这是真正的载荷。
- 时间戳:请求发起时间,精度要至少到毫秒。
2.1.2 响应端该抓什么
- HTTP状态码:200、400、500,这是行为健康的直接信号。
- 响应体大小:单位字节,太大或者太小,都可能是异常信号。
- 响应时间:从请求到达服务端到响应发出所耗时长。这就是我们常说的延迟。
- 响应头:比如 Content-Encoding、Set-Cookie,有时能发现配置问题。
你可能会问:响应体内容要不要抓?我建议:不要全量存储。为什么?一是存储成本太可怕,二是涉及用户隐私和敏感数据。我在项目中遇到过,有人把整个支付接口的响应体全部落盘,结果等保审计直接亮了红灯。
2.2 API网关日志:统一的流量入口
如果你的系统接了API网关——无论是Kong、Nginx Plus、还是云上的API Gateway——恭喜你,捡到宝了。
API网关是所有请求的必经之路。在这里采集日志,天然具有全局视角。而且很多网关会帮你做一件事:请求重组的标准化。比如,网关会把原始请求IP、设备指纹、用户ID这些字段,统一注入到日志里。这对于后续建立用户级别的行为基线,太重要了。
2.2.1 网关日志 vs 应用日志,有什么区别?
| 维度 | API网关日志 | 应用服务日志 |
|---|---|---|
| 覆盖范围 | 全量流量,包括被限流、鉴权失败的请求 | 只有到达服务的请求 |
| 延迟影响 | 几乎无侵入,网关转发就好 | 可能影响应用性能,尤其高并发时 |
| 字段丰富度 | 头部和元数据丰富,但缺业务细节 | 业务字段完整,团队可控 |
| 标准化程度 | 高,统一格式 | 低,不同服务可能格式各异 |
2.3 数据清洗与预处理:从混乱到有序
日志采回来了,但你能直接用吗?不能。原始日志长什么样?嗯,格式乱、有空值、有脏数据、有重复。不做清洗,你后面的分析就是自欺欺人。
2.3.1 常见的“脏数据”有哪些
- 缺失值:比如响应时间字段忽然是空,可能因为超时没记录。
- 异常值:响应时间忽然显示 -1 或者 999999,明显是错误占位符。
- 重复记录:因为重试机制,同一条请求被记录了多次。
- 格式不统一:有的路径带尾部斜杠 /api/v1/orders/,有的不带,直接拼到一起分析就出问题。
- 语义不合法:比如状态码填了 999,或者请求体里的 JSON 格式是坏的。
2.3.2 清洗的标准流程(我自己的套路)
- 字段提取与重命名:把不同来源的字段统一命名,比如“resp_time”、“response_time”、“rt”都统一成“response_time_ms”。
- 格式校验:把时间戳转成统一格式 ISO 8601,把 JSON 体做一次解析校验。
- 异常值过滤:响应时间 <= 0 的直接标记为无效。
- 去重:基于 (时间戳, 请求ID, 用户ID) 的联合键做去重。
- 缺失值处理:关键字段比如状态码、响应时间如果为空,建议直接丢弃这一条。不关键的字段,比如某些自定义头,可以填空字符串。
下面是一段我常用的Python清洗示例,你可以参考一下:
import pandas as pd
import json
# 假设原始日志是DataFrame
df = pd.read_csv('raw_api_logs.csv')
# 1. 统一字段命名
df.rename(columns={
'resp_time': 'response_time_ms',
'method': 'http_method'
}, inplace=True)
# 2. 格式校验:时间戳转datetime
df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
# 3. 异常值过滤
df = df[df['response_time_ms'] > 0]
# 4. 去重
df.drop_duplicates(subset=['timestamp', 'request_id', 'user_id'], inplace=True)
# 5. 缺失值处理:状态码为空则丢弃
df.dropna(subset=['status_code'], inplace=True)
print(f"清洗后剩余记录数: {len(df)}")
2.4 一些关于字段设计的思考
数据清洗完,你会发现一个现实:不同来源的数据,字段结构往往不一样。你做异常检测时,总不能一个模型吃两种格式吧?所以,在清洗阶段就定义一套统一的数据模型,非常重要。
我个人推荐的最小字段集,供你参考:
| 字段名 | 类型 | 含义 | 来源 |
|---|---|---|---|
| timestamp | datetime | 请求时间 | 统一来自网关或服务 |
| request_id | string | 唯一请求标识 | 网关生成 |
| uri | string | 归一化的路径(不含参数) | 清洗时处理 |
| http_method | string | GET/POST等 | 原始日志 |
| status_code | int | HTTP状态码 | 原始日志 |
| response_time_ms | float | 响应耗时(毫秒) | 原始日志或计算 |
| user_id | string | 调用方用户标识 | 从解析 |
| client_ip | string | 客户端IP | 网关日志 |
| request_body_size | int | 请求体字节数 | 原始日志 |
好了,数据采集和预处理这块,我该讲的都讲透了。数据到手了,也洗干净了,接下来呢?嗯,后面的章节,我们会把这些数据喂给模型,开始建立真正的行为基线。但那是后话了,先把眼前这一步做扎实。