← 中文资源

简体中文版本

Server-Timing 指标响应头

Server-Timing 向浏览器上报具名的后端耗时,让服务器侧的性能数据进入真实用户监控,而无需部署独立的客户端探针。

阅读英文原文

概念概述

Server-Timing 指标响应头(Server-Timing Metrics Header)指的是:Server-Timing 向浏览器上报具名的后端耗时,让服务器侧的性能数据进入真实用户监控,而无需部署独立的客户端探针。

从更严格的定义看,The Server-Timing response header carries one or more metric entries, each with a name and a duration in milliseconds, describing where time was spent while the server handled the request. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It is useful for correlation between front-end timing APIs and server-side work such as database or cache time. 它与 Hop-by-Hop Versus End-to-End Headers、HTTP Trailer Fields、CAA Records Constraining Certificates 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • Server-Timing measures network latency between client and server
  • these values are visible to the end user and therefore sensitive

需要避免的反模式

  • emitting per-request timings on a high-volume endpoint without sampling
  • putting user identifiers or query text into metric names

实践要点

  1. Entries use the syntax name;dur=milliseconds, optionally with a description, and repeated names are allowed so a handler can report several internal phases.
  2. Browsers expose the values through the PerformanceResourceTiming interface, which lets a real user monitoring beacon merge server-side durations with navigation and resource timings.
  3. Because headers add bytes to every response, gate detailed timings behind a sampling rate or a debug flag and never include anything that could leak internal topology or user data.

参考来源

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing

代码示例

const MAX_DELAY_MS = 24 * 60 * 60 * 1000;

export function parseRetryAfter(value, options = {}) {
  if (typeof value !== "string") return null;
  const { now = Date.now(), serverDate, maxMs = MAX_DELAY_MS } = options;
  if (!Number.isFinite(now)) throw new RangeError("now must be a finite timestamp");
  if (!Number.isFinite(maxMs) || maxMs < 0) {
    throw new RangeError("maxMs must be a non-negative finite number");
  }
  if (serverDate !== undefined && !Number.isFinite(serverDate)) {
    throw new RangeError("serverDate must be a finite timestamp");
  }

  const trimmed = value.trim();
  if (trimmed === "") return null;

  if (/^\d+$/.test(trimmed)) {
    const seconds = Number(trimmed);
    if (!Number.isSafeInteger(seconds)) return null;
    return Math.min(seconds * 1000, maxMs);
  }

  const target = Date.parse(trimmed);
  if (Number.isNaN(target)) return null;
  const reference = serverDate === undefined ? now : serverDate;
  return Math.min(Math.max(0, target - reference), maxMs);
}
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
)

type requestIDKey struct{}

func validRequestID(id string) bool {
	if len(id) == 0 || len(id) > 128 {
		return false
	}
	for _, char := range id {
		switch {
		case char >= 'a' && char <= 'z':
		case char >= 'A' && char <= 'Z':
		case char >= '0' && char <= '9':
		case char == '-' || char == '_':
		default:
			return false
		}
	}
	return true
}

func RequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if !validRequestID(id) {
			buffer := make([]byte, 8)
			if _, err := rand.Read(buffer); err != nil {
				http.Error(w, "cannot create request id", http.StatusInternalServerError)
				return
			}
			id = hex.EncodeToString(buffer)
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), requestIDKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func RequestIDFrom(ctx context.Context) string {
	id, _ := ctx.Value(requestIDKey{}).(string)
	return id
}

func main() {
	handler := RequestIDMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "request", RequestIDFrom(r.Context()))
	}))
	http.Handle("/", handler)
	_ = http.ListenAndServe(":8080", nil)
}