2. 埋点数据采集需求:请求URL、方法、耗时、状态码、异常信息、时间戳

埋点数据的质量直接决定后续分析的准确性。本章系统拆解六个核心埋点字段的采集需求,明确每个字段的数据来源、采集时机和边界情况处理,并给出基于 OkHttp Interceptor 的标准实现框架。

2.1 埋点字段定义与采集规则

字段 数据类型 来源对象 采集时机 边界说明
请求 URL String Request.url() 请求发出前 保留完整路径和 Query,不包含 Fragment
请求方法 String Request.method() 请求发出前 标准 HTTP 方法:GET/POST/PUT/DELETE/PATCH
耗时 long (ms) 时间差计算 响应返回后 包含 DNS 解析、连接、TLS 握手、请求发送、响应接收全部时间
状态码 int Response.code() 正常响应后 含 200–5xx;注意 304 等缓存码也需记录
异常信息 String/null Throwable 异常抛出时 仅当网络/协议异常时有值;业务错误码归入状态码
时间戳 long (epoch ms) System.currentTimeMillis() 请求开始时 使用客户端本地时间,避免依赖服务器返回时间
设计原则:所有埋点字段必须在 Interceptor 的 intercept() 方法单次调用内完成采集,保证数据原子性。避免跨线程或异步回传时间戳导致的偏差。

2.2 采集流程与拦截器实现骨架

拦截器埋点数据采集流程 开始 intercept() 记录时间戳、URL、方法 startNs = System.nanoTime() 执行 chain.proceed(request) try { response = chain.proceed(request) } 获取状态码 response.code() 捕获异常,记录异常信息 catch (Exception e) { exception = e } 计算耗时、组装埋点数据 elapsed = (System.nanoTime() - startNs) / 1_000_000 异常分支无状态码,置为0
流程关键点:
  • 耗时计算必须使用 System.nanoTime() 而非 currentTimeMillis(),避免系统时间跳变引起的负值或偏差。
  • 异常处理应在 try 块外声明 Request request 和 long startNs,确保 catch 块也能访问到请求信息。

2.3 代码示例:基础拦截器埋点实现

public class TracingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        long startNs = System.nanoTime();
        long timestamp = System.currentTimeMillis();
        try {
            Response response = chain.proceed(request);
            long elapsedMs = (System.nanoTime() - startNs) / 1_000_000;
            // 组装埋点数据
            TraceData data = new TraceData.Builder()
                    .url(request.url().toString())
                    .method(request.method())
                    .costMs(elapsedMs)
                    .statusCode(response.code())
                    .exception(null)
                    .timestamp(timestamp)
                    .build();
            // 输出或发送 data(可在此处存储或上报)
            return response;
        } catch (IOException e) {
            long elapsedMs = (System.nanoTime() - startNs) / 1_000_000;
            TraceData data = new TraceData.Builder()
                    .url(request.url().toString())
                    .method(request.method())
                    .costMs(elapsedMs)
                    .statusCode(0)            // 网络异常无状态码
                    .exception(e.getClass().getName() + ": " + e.getMessage())
                    .timestamp(timestamp)
                    .build();
            throw e;  // 必须重新抛出,保持 OkHttp 的异常传递链
        }
    }
}
注意:异常分支中 statusCode 不能设为 null,约定使用 0 表示无状态码;exception 字段在正常返回时为 null,异常时为非空字符串。这样下游分析逻辑可以统一按 statusCode == 0 判断网络异常。

2.4 数据模型定义(Java POJO)

public class TraceData {
    private final String url;
    private final String method;
    private final long costMs;
    private final int statusCode;
    private final String exception;
    private final long timestamp;

    private TraceData(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.costMs = builder.costMs;
        this.statusCode = builder.statusCode;
        this.exception = builder.exception;
        this.timestamp = builder.timestamp;
    }

    // Builder 模式可避免构造方法参数过多,详见附录
    public static class Builder { /* 省略具体实现 */ }
}
最佳实践:将 TraceData 设计为不可变对象,确保后续多线程上报场景下的线程安全。Builder 模式同时支持可选字段扩展(如后续新增 payloadSize 字段)。