diff --git a/pkg/middlewares/canary/canary.go b/pkg/middlewares/canary/canary.go index 60051f753f..94197bafb4 100644 --- a/pkg/middlewares/canary/canary.go +++ b/pkg/middlewares/canary/canary.go @@ -50,7 +50,7 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)) if cfg.Product == "" { - return nil, fmt.Errorf("product name required for Canary middleware") + return nil, fmt.Errorf("product name required for canary middleware") } expiration := time.Duration(cfg.CacheExpiration) @@ -97,7 +97,13 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) { requestID := req.Header.Get(headerXRequestID) if c.addRequestID { if requestID == "" { - requestID = generatorUUID() + // extract trace-id as x-request-id + // https://www.w3.org/TR/trace-context/#traceparent-header + if traceparent := req.Header.Get("traceparent"); len(traceparent) >= 55 { + requestID = traceparent[3:35] + } else { + requestID = generatorUUID() + } req.Header.Set(headerXRequestID, requestID) } rw.Header().Set(headerXRequestID, requestID) @@ -112,6 +118,9 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) { logData.Core["XRequestID"] = requestID logData.Core["UserAgent"] = req.Header.Get(headerUA) logData.Core["Referer"] = req.Header.Get("Referer") + if traceparent := req.Header.Get("traceparent"); traceparent != "" { + logData.Core["Traceparent"] = traceparent + } } } @@ -136,10 +145,10 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) { if info.label == "" && info.uid != "" { labels := c.ls.MustLoadLabels(req.Context(), info.uid, req.Header.Get(headerXRequestID)) for _, l := range labels { - if info.client != "" && !l.MatchClient(info.client) { + if !l.MatchClient(info.client) { continue } - if info.channel != "" && !l.MatchChannel(info.channel) { + if !l.MatchChannel(info.channel) { continue } info.label = l.Label @@ -154,7 +163,7 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) { if logData := accesslog.GetLogData(req); logData != nil { logData.Core["UID"] = info.uid - logData.Core["XCanary"] = req.Header.Values(headerXCanary) + logData.Core["XCanary"] = info.String() } } @@ -239,6 +248,12 @@ func extractUserIDFromBase64(s string) string { return "" } +// Canary Header specification, reference to https://www.w3.org/TR/trace-context/#tracestate-header +// X-Canary: label=beta,nofallback +// X-Canary: client=iOS,channel=stable,app=teambition,version=v10.0 +// full example +// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,client=iOS,channel=stable,app=teambition,version=v10.0,nofallback,testing +// support fields: label, product, uid, client, channel, app, version, nofallback, testing type canaryHeader struct { label string product string @@ -252,8 +267,32 @@ type canaryHeader struct { } // uid and product will not be extracted +// standard +// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ... +// and compatible with +// X-Canary: beta +// or +// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ... +// or +// X-Canary: label=beta +// X-Canary: product=urbs +// X-Canary: uid=5c4057f0be825b390667abee +// X-Canary: nofallback func (ch *canaryHeader) fromHeader(header http.Header, trust bool) { - ch.feed(header.Values(headerXCanary), trust) + vals := header.Values(headerXCanary) + if len(vals) == 1 { + if strings.IndexByte(vals[0], ',') > 0 { + vals = strings.Split(vals[0], ",") + } else if strings.IndexByte(vals[0], ';') > 0 { + vals = strings.Split(vals[0], ";") + } + } + ch.feed(vals, trust) +} + +// label should not be empty +func (ch *canaryHeader) intoHeader(header http.Header) { + header.Set(headerXCanary, ch.String()) } func (ch *canaryHeader) feed(vals []string, trust bool) { @@ -287,33 +326,35 @@ func (ch *canaryHeader) feed(vals []string, trust bool) { } // label should not be empty -func (ch *canaryHeader) intoHeader(header http.Header) { +func (ch *canaryHeader) String() string { if ch.label == "" { - return + return "" } - header.Set(headerXCanary, fmt.Sprintf("label=%s", ch.label)) + vals := make([]string, 0, 4) + vals = append(vals, fmt.Sprintf("label=%s", ch.label)) if ch.product != "" { - header.Add(headerXCanary, fmt.Sprintf("product=%s", ch.product)) + vals = append(vals, fmt.Sprintf("product=%s", ch.product)) } if ch.uid != "" { - header.Add(headerXCanary, fmt.Sprintf("uid=%s", ch.uid)) + vals = append(vals, fmt.Sprintf("uid=%s", ch.uid)) } if ch.client != "" { - header.Add(headerXCanary, fmt.Sprintf("client=%s", ch.client)) + vals = append(vals, fmt.Sprintf("client=%s", ch.client)) } if ch.channel != "" { - header.Add(headerXCanary, fmt.Sprintf("channel=%s", ch.channel)) + vals = append(vals, fmt.Sprintf("channel=%s", ch.channel)) } if ch.app != "" { - header.Add(headerXCanary, fmt.Sprintf("app=%s", ch.app)) + vals = append(vals, fmt.Sprintf("app=%s", ch.app)) } if ch.version != "" { - header.Add(headerXCanary, fmt.Sprintf("version=%s", ch.version)) + vals = append(vals, fmt.Sprintf("version=%s", ch.version)) } if ch.nofallback { - header.Add(headerXCanary, "nofallback") + vals = append(vals, "nofallback") } if ch.testing { - header.Add(headerXCanary, "testing") + vals = append(vals, "testing") } + return strings.Join(vals, ",") } diff --git a/pkg/middlewares/canary/canary_test.go b/pkg/middlewares/canary/canary_test.go index d2bbf69b19..6be3ccd9d6 100644 --- a/pkg/middlewares/canary/canary_test.go +++ b/pkg/middlewares/canary/canary_test.go @@ -66,6 +66,20 @@ func TestCanaryHeader(t *testing.T) { a.Equal("version", ch.version) a.True(ch.nofallback) a.True(ch.testing) + + ch = &canaryHeader{} + h = http.Header{} + h.Set(headerXCanary, "label=label,version=version,app=app, channel=channel,client=client, uid=uid,product=product,ip=ip,nofallback,testing") + ch.fromHeader(h, false) + a.Equal("label", ch.label) + a.Equal("", ch.product) + a.Equal("", ch.uid) + a.Equal("client", ch.client) + a.Equal("channel", ch.channel) + a.Equal("app", ch.app) + a.Equal("version", ch.version) + a.True(ch.nofallback) + a.True(ch.testing) }) t.Run("intoHeader should work", func(t *testing.T) { @@ -74,7 +88,7 @@ func TestCanaryHeader(t *testing.T) { ch := &canaryHeader{} h := http.Header{} ch.intoHeader(h) - a.Equal(0, len(h.Values(headerXCanary))) + a.Equal("", h.Get(headerXCanary)) ch = &canaryHeader{ label: "label", @@ -84,7 +98,7 @@ func TestCanaryHeader(t *testing.T) { } h = http.Header{} ch.intoHeader(h) - a.Equal(4, len(h.Values(headerXCanary))) + a.Equal("label=label,product=product,uid=uid,channel=channel", h.Get(headerXCanary)) chn := &canaryHeader{} chn.fromHeader(h, true) @@ -103,7 +117,7 @@ func TestCanaryHeader(t *testing.T) { } h = http.Header{} ch.intoHeader(h) - a.Equal(9, len(h.Values(headerXCanary))) + a.Equal("label=label,product=product,uid=uid,client=client,channel=channel,app=app,version=version,nofallback,testing", h.Get(headerXCanary)) chn = &canaryHeader{} chn.fromHeader(h, true) @@ -256,8 +270,7 @@ func TestCanary(t *testing.T) { req = httptest.NewRequest("GET", "http://example.com/foo", nil) rw = httptest.NewRecorder() - req.Header.Set(headerXCanary, "label=beta") - req.Header.Add(headerXCanary, "client=iOS") + req.Header.Set(headerXCanary, "label=beta,client=iOS") req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "stable"}) c.processCanary(rw, req) ch = &canaryHeader{} diff --git a/pkg/middlewares/canary/request.go b/pkg/middlewares/canary/request.go index ef4a72e1f2..85be0f116b 100644 --- a/pkg/middlewares/canary/request.go +++ b/pkg/middlewares/canary/request.go @@ -15,6 +15,7 @@ import ( "github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/version" + "github.com/opentracing/opentracing-go" ) func init() { @@ -93,6 +94,13 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err req.Header.Set(headerUA, userAgent) req.Header.Set(headerXRequestID, xRequestID) + if span := opentracing.SpanFromContext(ctx); span != nil { + opentracing.GlobalTracer().Inject( + span.Context(), + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(req.Header)) + } + resp, err := client.Do(req) if err != nil { if err.(*url.Error).Unwrap() == context.Canceled { diff --git a/pkg/server/service/loadbalancer/lrr/lrr.go b/pkg/server/service/loadbalancer/lrr/lrr.go index b86a1a01d7..2d9dba343e 100644 --- a/pkg/server/service/loadbalancer/lrr/lrr.go +++ b/pkg/server/service/loadbalancer/lrr/lrr.go @@ -8,10 +8,6 @@ import ( "strings" ) -const labelKey = "X-Canary" - -var isPortReg = regexp.MustCompile(`^\d+$`) - type namedHandler struct { http.Handler name string @@ -51,21 +47,7 @@ type Balancer struct { } func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // X-Canary: beta - // X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ... - label := "" - fallback := true - for i, v := range req.Header.Values(labelKey) { - switch { - case strings.HasPrefix(v, "label="): - label = v[6:] - case v == "nofallback": - fallback = false - case i == 0: - label = v - } - } - + label, fallback := extractLabel(req.Header) name := b.serviceName if label != "" { name = fmt.Sprintf("%s-%s", name, label) @@ -90,6 +72,8 @@ func (b *Balancer) AddService(fullServiceName string, handler http.Handler) { b.handlers = b.handlers.AppendAndSort(h) } +var isPortReg = regexp.MustCompile(`^\d+$`) + // full service name format (build by fullServiceName function): namespace-serviceName-port func removeNsPort(fullServiceName, ServiceName string) string { i := strings.Index(fullServiceName, ServiceName) @@ -98,3 +82,33 @@ func removeNsPort(fullServiceName, ServiceName string) string { } return strings.TrimRight(fullServiceName, "0123456789-") // remove port } + +func extractLabel(header http.Header) (string, bool) { + // standard specification, reference to https://www.w3.org/TR/trace-context/#tracestate-header + // X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ... + // and compatible with + // X-Canary: beta + // X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ... + label := "" + fallback := true + vals := header.Values("X-Canary") + if len(vals) == 1 { + if strings.IndexByte(vals[0], ',') > 0 { + vals = strings.Split(vals[0], ",") + } else if strings.IndexByte(vals[0], ';') > 0 { + vals = strings.Split(vals[0], ";") + } + } + for i, v := range vals { + v = strings.TrimSpace(v) + switch { + case strings.HasPrefix(v, "label="): + label = v[6:] + case v == "nofallback": + fallback = false + case i == 0: + label = v + } + } + return label, fallback +} diff --git a/pkg/server/service/loadbalancer/lrr/lrr_test.go b/pkg/server/service/loadbalancer/lrr/lrr_test.go index 25980d3b3a..90dd2670a6 100644 --- a/pkg/server/service/loadbalancer/lrr/lrr_test.go +++ b/pkg/server/service/loadbalancer/lrr/lrr_test.go @@ -95,4 +95,53 @@ func TestLRRBalancer(t *testing.T) { h = s.Match("lr", true) a.Nil(h) }) + + t.Run("extractLabel should work", func(t *testing.T) { + a := assert.New(t) + header := http.Header{} + label, fallback := extractLabel(header) + a.Equal("", label) + a.True(fallback) + + // X-Canary: dev + header.Set("X-Canary", "dev") + label, fallback = extractLabel(header) + a.Equal("dev", label) + a.True(fallback) + + // X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ... + header = http.Header{} + header.Set("X-Canary", "label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback") + label, fallback = extractLabel(header) + a.Equal("beta", label) + a.False(fallback) + + header = http.Header{} + header.Set("X-Canary", "label=dev,product=urbs,uid=5c4057f0be825b390667abee") + label, fallback = extractLabel(header) + a.Equal("dev", label) + a.True(fallback) + + header = http.Header{} + header.Set("X-Canary", "product=urbs,uid=5c4057f0be825b390667abee,nofallback,label=dev") + label, fallback = extractLabel(header) + a.Equal("dev", label) + a.False(fallback) + + // X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ... + header = http.Header{} + header.Set("X-Canary", "label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback") + label, fallback = extractLabel(header) + a.Equal("beta", label) + a.False(fallback) + + header = http.Header{} + header.Add("X-Canary", "label=beta") + header.Add("X-Canary", "product=urbs") + header.Add("X-Canary", "uid=5c4057f0be825b390667abee") + header.Add("X-Canary", "nofallback") + label, fallback = extractLabel(header) + a.Equal("beta", label) + a.False(fallback) + }) }