TUN-6576: Consume cf-trace-id from incoming TCP requests to create root span

(cherry picked from commit f48a7cd3dd)
This commit is contained in:
Devin Carr
2022-07-26 14:00:53 -07:00
parent 7f1c890a82
commit b9cba7f2ae
13 changed files with 166 additions and 62 deletions

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"runtime"
"strings"
"github.com/rs/zerolog"
otelContrib "go.opentelemetry.io/contrib/propagators/jaeger"
@@ -33,6 +34,9 @@ const (
MaxErrorDescriptionLen = 100
traceHttpStatusCodeKey = "upstreamStatusCode"
traceID128bitsWidth = 128 / 4
separator = ":"
)
var (
@@ -66,22 +70,50 @@ func Init(version string) {
cloudflaredVersionAttribute = semconv.ProcessRuntimeVersionKey.String(version)
}
type TracedRequest struct {
type TracedHTTPRequest struct {
*http.Request
trace.TracerProvider
exporter InMemoryClient
*cfdTracer
}
// NewTracedRequest creates a new tracer for the current request context.
func NewTracedRequest(req *http.Request) *TracedRequest {
// NewTracedHTTPRequest creates a new tracer for the current HTTP request context.
func NewTracedHTTPRequest(req *http.Request, log *zerolog.Logger) *TracedHTTPRequest {
ctx, exists := extractTrace(req)
if !exists {
return &TracedRequest{req, trace.NewNoopTracerProvider(), &NoopOtlpClient{}}
return &TracedHTTPRequest{req, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}}
}
return &TracedHTTPRequest{req.WithContext(ctx), newCfdTracer(ctx, log)}
}
func (tr *TracedHTTPRequest) ToTracedContext() *TracedContext {
return &TracedContext{tr.Context(), tr.cfdTracer}
}
type TracedContext struct {
context.Context
*cfdTracer
}
// NewTracedHTTPRequest creates a new tracer for the current HTTP request context.
func NewTracedContext(ctx context.Context, traceContext string, log *zerolog.Logger) *TracedContext {
ctx, exists := extractTraceFromString(ctx, traceContext)
if !exists {
return &TracedContext{ctx, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}}
}
return &TracedContext{ctx, newCfdTracer(ctx, log)}
}
type cfdTracer struct {
trace.TracerProvider
exporter InMemoryClient
log *zerolog.Logger
}
// NewCfdTracer creates a new tracer for the current request context.
func newCfdTracer(ctx context.Context, log *zerolog.Logger) *cfdTracer {
mc := new(InMemoryOtlpClient)
exp, err := otlptrace.New(req.Context(), mc)
exp, err := otlptrace.New(ctx, mc)
if err != nil {
return &TracedRequest{req, trace.NewNoopTracerProvider(), &NoopOtlpClient{}}
return &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}
}
tp := tracesdk.NewTracerProvider(
// We want to dump to in-memory exporter immediately
@@ -98,36 +130,41 @@ func NewTracedRequest(req *http.Request) *TracedRequest {
)),
)
return &TracedRequest{req.WithContext(ctx), tp, mc}
return &cfdTracer{tp, mc, log}
}
func (cft *TracedRequest) Tracer() trace.Tracer {
func (cft *cfdTracer) Tracer() trace.Tracer {
return cft.TracerProvider.Tracer(tracerInstrumentName)
}
// Spans returns the spans as base64 encoded protobuf otlp traces.
func (cft *TracedRequest) AddSpans(headers http.Header, log *zerolog.Logger) {
if headers == nil {
log.Error().Msgf("provided headers map is nil")
return
}
// GetSpans returns the spans as base64 encoded string of protobuf otlp traces.
func (cft *cfdTracer) GetSpans() (enc string) {
enc, err := cft.exporter.Spans()
switch err {
case nil:
break
case errNoTraces:
log.Error().Err(err).Msgf("expected traces to be available")
cft.log.Trace().Err(err).Msgf("expected traces to be available")
return
case errNoopTracer:
return // noop tracer has no traces
default:
log.Error().Err(err)
cft.log.Debug().Err(err)
return
}
return
}
// AddSpans assigns spans as base64 encoded protobuf otlp traces to provided
// HTTP headers.
func (cft *cfdTracer) AddSpans(headers http.Header) {
if headers == nil {
return
}
enc := cft.GetSpans()
// No need to add header if no traces
if enc == "" {
log.Error().Msgf("no traces provided and no error from exporter")
return
}
@@ -166,6 +203,33 @@ func endSpan(span trace.Span, upstreamStatusCode int, spanStatusCode codes.Code,
span.End()
}
// extractTraceFromString will extract the trace information from the provided
// propagated trace string context.
func extractTraceFromString(ctx context.Context, trace string) (context.Context, bool) {
if trace == "" {
return ctx, false
}
// Jaeger specific separator
parts := strings.Split(trace, separator)
if len(parts) != 4 {
return ctx, false
}
if parts[0] == "" {
return ctx, false
}
// Correctly left pad the trace to a length of 32
if len(parts[0]) < traceID128bitsWidth {
left := traceID128bitsWidth - len(parts[0])
parts[0] = strings.Repeat("0", left) + parts[0]
trace = strings.Join(parts, separator)
}
// Override the 'cf-trace-id' as 'uber-trace-id' so the jaeger propagator can extract it.
traceHeader := map[string]string{TracerContextNameOverride: trace}
remoteCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(traceHeader))
return remoteCtx, true
}
// extractTrace attempts to check for a cf-trace-id from a request and return the
// trace context with the provided http.Request.
func extractTrace(req *http.Request) (context.Context, bool) {

View File

@@ -14,38 +14,42 @@ import (
)
func TestNewCfTracer(t *testing.T) {
log := zerolog.Nop()
req := httptest.NewRequest("GET", "http://localhost", nil)
req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1")
tr := NewTracedRequest(req)
tr := NewTracedHTTPRequest(req, &log)
assert.NotNil(t, tr)
assert.IsType(t, tracesdk.NewTracerProvider(), tr.TracerProvider)
assert.IsType(t, &InMemoryOtlpClient{}, tr.exporter)
}
func TestNewCfTracerMultiple(t *testing.T) {
log := zerolog.Nop()
req := httptest.NewRequest("GET", "http://localhost", nil)
req.Header.Add(TracerContextName, "1241ce3ecdefc68854e8514e69ba42ca:b38f1bf5eae406f3:0:1")
req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1")
tr := NewTracedRequest(req)
tr := NewTracedHTTPRequest(req, &log)
assert.NotNil(t, tr)
assert.IsType(t, tracesdk.NewTracerProvider(), tr.TracerProvider)
assert.IsType(t, &InMemoryOtlpClient{}, tr.exporter)
}
func TestNewCfTracerNilHeader(t *testing.T) {
log := zerolog.Nop()
req := httptest.NewRequest("GET", "http://localhost", nil)
req.Header[http.CanonicalHeaderKey(TracerContextName)] = nil
tr := NewTracedRequest(req)
tr := NewTracedHTTPRequest(req, &log)
assert.NotNil(t, tr)
assert.IsType(t, trace.NewNoopTracerProvider(), tr.TracerProvider)
assert.IsType(t, &NoopOtlpClient{}, tr.exporter)
}
func TestNewCfTracerInvalidHeaders(t *testing.T) {
log := zerolog.Nop()
req := httptest.NewRequest("GET", "http://localhost", nil)
for _, test := range [][]string{nil, {""}} {
req.Header[http.CanonicalHeaderKey(TracerContextName)] = test
tr := NewTracedRequest(req)
tr := NewTracedHTTPRequest(req, &log)
assert.NotNil(t, tr)
assert.IsType(t, trace.NewNoopTracerProvider(), tr.TracerProvider)
assert.IsType(t, &NoopOtlpClient{}, tr.exporter)
@@ -53,9 +57,10 @@ func TestNewCfTracerInvalidHeaders(t *testing.T) {
}
func TestAddingSpansWithNilMap(t *testing.T) {
log := zerolog.Nop()
req := httptest.NewRequest("GET", "http://localhost", nil)
req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1")
tr := NewTracedRequest(req)
tr := NewTracedHTTPRequest(req, &log)
exporter := tr.exporter.(*InMemoryOtlpClient)
@@ -65,5 +70,5 @@ func TestAddingSpansWithNilMap(t *testing.T) {
assert.NoError(t, err)
// a panic shouldn't occur
tr.AddSpans(nil, &zerolog.Logger{})
tr.AddSpans(nil)
}