概念概述
追踪上下文传播(Trace Context Propagation)指的是:追踪上下文传播跨进程与协议边界传递追踪与跨度标识,使一条分布式追踪贯穿每个服务的调用。
从更严格的定义看,Trace context propagation is the transfer of trace identifiers and sampling decisions across service boundaries, typically through message headers, so spans created in different processes join one trace. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It is required for any distributed trace that crosses HTTP, gRPC, or message queues. 它与 Exemplars Linking Metrics to Traces、Operational Dashboard Design、Selecting Service Level Indicators 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- tracing works automatically once each service emits spans
- trace context can be inferred from timestamps and host names
需要避免的反模式
- creating a new trace per service instead of continuing the parent
- failing to propagate sampling decisions to downstream services
实践要点
- A standard header such as traceparent encodes the trace identifier, parent span, and flags, letting the receiver continue the same trace rather than start a disjoint one.
- Asynchronous hops need the context attached to the message, because no in-process call carries the header across a queue or scheduled job.
- Context must be sanitised at trust boundaries; untrusted clients that can set trace identifiers can otherwise pollute or join traces they should not.
参考来源
https://www.w3.org/TR/trace-context/
代码示例
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const sdk = new NodeSDK({
serviceName: 'orders-service',
traceExporter: new OTLPTraceExporter({
url: process.env.OTLP_ENDPOINT ?? 'http://collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
await sdk.start();
const tracer = trace.getTracer('orders', '1.0.0');
async function processOrder(orderId: string): Promise<void> {
return tracer.startActiveSpan('order.process', async (span) => {
span.setAttribute('order.id', orderId);
try {
await tracer.startActiveSpan('order.validate', async (child) => {
await new Promise((r) => setTimeout(r, 10));
child.setAttribute('validation.rules', 12);
child.end();
});
await tracer.startActiveSpan('order.charge', async (child) => {
await new Promise((r) => setTimeout(r, 25));
child.recordException(new Error('card declined simulated'));
child.setStatus({ code: SpanStatusCode.ERROR });
child.end();
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
} finally {
span.end();
}
});
}
await processOrder('ord-7');
await sdk.shutdown();
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)
}