diff --git a/pkg/middlewares/canary/canary.go b/pkg/middlewares/canary/canary.go index 87cb55a609..60051f753f 100644 --- a/pkg/middlewares/canary/canary.go +++ b/pkg/middlewares/canary/canary.go @@ -14,7 +14,7 @@ import ( "github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/middlewares" "github.com/containous/traefik/v2/pkg/middlewares/accesslog" - "github.com/containous/traefik/v2/pkg/tracing" + "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ) @@ -84,7 +84,7 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string // GetTracingInformation implements Tracable interface func (c *Canary) GetTracingInformation() (string, ext.SpanKindEnum) { - return c.name, tracing.SpanKindNoneEnum + return c.name, ext.SpanKindRPCClientEnum } func (c *Canary) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -103,9 +103,15 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) { rw.Header().Set(headerXRequestID, requestID) } + if span := opentracing.SpanFromContext(req.Context()); span != nil { + span.SetTag("component", "Canary") + span.SetTag("x-request-id", requestID) + } + if logData := accesslog.GetLogData(req); logData != nil { logData.Core["XRequestID"] = requestID logData.Core["UserAgent"] = req.Header.Get(headerUA) + logData.Core["Referer"] = req.Header.Get("Referer") } } @@ -119,8 +125,8 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) { // load user's labels and update to header when work as public gateway. info.fromHeader(req.Header, false) if info.label == "" { - if cookie, _ := req.Cookie(headerXCanary); cookie != nil && validLabelReg.MatchString(cookie.Value) { - info.label = cookie.Value + if cookie, _ := req.Cookie(headerXCanary); cookie != nil && cookie.Value != "" { + info.feed(strings.Split(cookie.Value, ","), false) } } @@ -155,8 +161,10 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) { type userInfo struct { UID0 string `json:"uid"` UID1 string `json:"_userId"` - UID2 string `json:"sub"` - UID3 string `json:"id"` + UID2 string `json:"userId"` + UID3 string `json:"user_id"` + UID4 string `json:"sub"` + UID5 string `json:"id"` } func extractUserID(req *http.Request, uidCookies []string) string { @@ -221,6 +229,10 @@ func extractUserIDFromBase64(s string) string { return user.UID2 case user.UID3 != "": return user.UID3 + case user.UID4 != "": + return user.UID4 + case user.UID5 != "": + return user.UID5 } } } @@ -228,19 +240,25 @@ func extractUserIDFromBase64(s string) string { } type canaryHeader struct { - label string - product string - uid string - client string - channel string - app string - version string + label string + product string + uid string + client string + channel string + app string + version string + nofallback bool + testing bool } // uid and product will not be extracted func (ch *canaryHeader) fromHeader(header http.Header, trust bool) { - vals := header.Values(headerXCanary) - for _, v := range vals { + ch.feed(header.Values(headerXCanary), trust) +} + +func (ch *canaryHeader) feed(vals []string, trust bool) { + for i, v := range vals { + v = strings.TrimSpace(v) switch { case strings.HasPrefix(v, "label="): ch.label = v[6:] @@ -256,8 +274,12 @@ func (ch *canaryHeader) fromHeader(header http.Header, trust bool) { ch.app = v[4:] case strings.HasPrefix(v, "version="): ch.version = v[8:] + case v == "nofallback": + ch.nofallback = true + case v == "testing": + ch.testing = true default: - if len(vals) == 1 && validLabelReg.MatchString(v) { + if i == 0 && validLabelReg.MatchString(v) { ch.label = v } } @@ -288,4 +310,10 @@ func (ch *canaryHeader) intoHeader(header http.Header) { if ch.version != "" { header.Add(headerXCanary, fmt.Sprintf("version=%s", ch.version)) } + if ch.nofallback { + header.Add(headerXCanary, "nofallback") + } + if ch.testing { + header.Add(headerXCanary, "testing") + } } diff --git a/pkg/middlewares/canary/canary_test.go b/pkg/middlewares/canary/canary_test.go index 989c2bfccd..d2bbf69b19 100644 --- a/pkg/middlewares/canary/canary_test.go +++ b/pkg/middlewares/canary/canary_test.go @@ -54,6 +54,8 @@ func TestCanaryHeader(t *testing.T) { h.Add(headerXCanary, fmt.Sprintf("uid=%s", "uid")) h.Add(headerXCanary, fmt.Sprintf("product=%s", "product")) h.Add(headerXCanary, fmt.Sprintf("ip=%s", "ip")) + h.Add(headerXCanary, "nofallback") + h.Add(headerXCanary, "testing") ch.fromHeader(h, false) a.Equal("label", ch.label) a.Equal("", ch.product) @@ -62,6 +64,8 @@ func TestCanaryHeader(t *testing.T) { 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) { @@ -87,17 +91,19 @@ func TestCanaryHeader(t *testing.T) { a.Equal(*ch, *chn) ch = &canaryHeader{ - label: "label", - product: "product", - uid: "uid", - client: "client", - channel: "channel", - app: "app", - version: "version", + label: "label", + product: "product", + uid: "uid", + client: "client", + channel: "channel", + app: "app", + version: "version", + nofallback: true, + testing: true, } h = http.Header{} ch.intoHeader(h) - a.Equal(7, len(h.Values(headerXCanary))) + a.Equal(9, len(h.Values(headerXCanary))) chn = &canaryHeader{} chn.fromHeader(h, true) @@ -209,6 +215,38 @@ func TestCanary(t *testing.T) { ch.fromHeader(req.Header, true) a.Equal("beta", ch.label) a.Equal("Urbs", ch.product) + a.False(ch.nofallback) + a.False(ch.testing) + ch = &canaryHeader{} + ch.fromHeader(rw.Header(), true) + a.Equal("", ch.label) + a.Equal("", ch.product) + + req = httptest.NewRequest("GET", "http://example.com/foo", nil) + rw = httptest.NewRecorder() + req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "beta, nofallback,testing "}) + c.processCanary(rw, req) + ch = &canaryHeader{} + ch.fromHeader(req.Header, true) + a.Equal("beta", ch.label) + a.Equal("Urbs", ch.product) + a.True(ch.nofallback) + a.True(ch.testing) + ch = &canaryHeader{} + ch.fromHeader(rw.Header(), true) + a.Equal("", ch.label) + a.Equal("", ch.product) + + req = httptest.NewRequest("GET", "http://example.com/foo", nil) + rw = httptest.NewRecorder() + req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "label=beta,nofallback,testing"}) + c.processCanary(rw, req) + ch = &canaryHeader{} + ch.fromHeader(req.Header, true) + a.Equal("beta", ch.label) + a.Equal("Urbs", ch.product) + a.True(ch.nofallback) + a.True(ch.testing) ch = &canaryHeader{} ch.fromHeader(rw.Header(), true) a.Equal("", ch.label) diff --git a/pkg/middlewares/canary/label.go b/pkg/middlewares/canary/label.go index 03ee8b7a77..3794428531 100644 --- a/pkg/middlewares/canary/label.go +++ b/pkg/middlewares/canary/label.go @@ -9,6 +9,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/log" + "github.com/opentracing/opentracing-go" ) var storesMu sync.Mutex @@ -119,10 +120,17 @@ func (ls *LabelStore) MustLoadLabels(ctx context.Context, uid, requestID string) e.mu.Lock() defer e.mu.Unlock() + fetchLabels := false + if e.value == nil || e.expireAt.Before(now) { labels, ts := ls.mustFetchLabels(ctx, uid, requestID) e.value = labels e.expireAt = time.Unix(ts, 0).Add(ls.expiration) + fetchLabels = true + } + + if span := opentracing.SpanFromContext(ctx); span != nil { + span.SetTag("fetched-labels", fetchLabels) } return e.value diff --git a/pkg/middlewares/canary/request.go b/pkg/middlewares/canary/request.go index 1e69efb7fd..ef4a72e1f2 100644 --- a/pkg/middlewares/canary/request.go +++ b/pkg/middlewares/canary/request.go @@ -14,10 +14,7 @@ import ( "time" "github.com/containous/traefik/v2/pkg/log" - "github.com/containous/traefik/v2/pkg/tracing" "github.com/containous/traefik/v2/pkg/version" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" ) func init() { @@ -94,19 +91,6 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err return nil, err } - var sp opentracing.Span - if tr, _ := tracing.FromContext(req.Context()); tr != nil { - opParts := []string{"label"} - span, re, finish := tr.StartSpanf(req, ext.SpanKindRPCClientEnum, "canary", opParts, "/") - sp = span - defer finish() - - span.SetTag(headerXRequestID, xRequestID) - ext.HTTPUrl.Set(span, re.URL.String()) - tracing.InjectRequestHeaders(re) - req = re - } - req.Header.Set(headerUA, userAgent) req.Header.Set(headerXRequestID, xRequestID) resp, err := client.Do(req) @@ -120,9 +104,6 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err } hc.Reset() - if sp != nil { - tracing.LogResponseCode(sp, resp.StatusCode) - } respBody, err := ioutil.ReadAll(resp.Body) resp.Body.Close() diff --git a/pkg/server/service/loadbalancer/lrr/lrr.go b/pkg/server/service/loadbalancer/lrr/lrr.go index 19a3202cb1..b86a1a01d7 100644 --- a/pkg/server/service/loadbalancer/lrr/lrr.go +++ b/pkg/server/service/loadbalancer/lrr/lrr.go @@ -24,9 +24,11 @@ func New(defaultServiceName string, handler http.Handler) *Balancer { type sliceHandler []*namedHandler -func (s sliceHandler) Match(name string) *namedHandler { +func (s sliceHandler) Match(name string, fallback bool) *namedHandler { for _, handler := range s { - if strings.HasPrefix(name, handler.name) { + if fallback && strings.HasPrefix(name, handler.name) { + return handler + } else if name == handler.name { return handler } } @@ -50,22 +52,30 @@ type Balancer struct { func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // X-Canary: beta - // X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; ... - label := req.Header.Get(labelKey) - if label != "" && strings.HasPrefix(label, "label=") { - label = label[6:] + // 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 + } } name := b.serviceName if label != "" { name = fmt.Sprintf("%s-%s", name, label) - if handler := b.handlers.Match(name); handler != nil { + if handler := b.handlers.Match(name, fallback); handler != nil { handler.ServeHTTP(w, req) return } } - if b.defaultHandler != nil { + if b.defaultHandler != nil && (fallback || label == "") { b.defaultHandler.ServeHTTP(w, req) return } diff --git a/pkg/server/service/loadbalancer/lrr/lrr_test.go b/pkg/server/service/loadbalancer/lrr/lrr_test.go index e5503775b0..25980d3b3a 100644 --- a/pkg/server/service/loadbalancer/lrr/lrr_test.go +++ b/pkg/server/service/loadbalancer/lrr/lrr_test.go @@ -63,27 +63,36 @@ func TestLRRBalancer(t *testing.T) { r string } list := []ir{ - ir{"lrr", "lrr"}, - ir{"lrr-api", "lrr-api"}, - ir{"lrr-api-stable", "lrr-api-stable"}, - ir{"lrr-api-canary", "lrr-api-canary"}, - ir{"lrr-api-canary-v2", "lrr-api-canary-v2"}, - ir{"lrr-api-canary-v1", "lrr-api-canary-v1"}, - ir{"lrr-api-canary-v3", "lrr-api-canary"}, - ir{"lrr-api-canary-v1-beta1", "lrr-api-canary-v1"}, - ir{"lrr-api-dev", "lrr-api"}, - ir{"lrr-web", "lrr"}, + {"lrr", "lrr"}, + {"lrr-api", "lrr-api"}, + {"lrr-api-stable", "lrr-api-stable"}, + {"lrr-api-canary", "lrr-api-canary"}, + {"lrr-api-canary-v2", "lrr-api-canary-v2"}, + {"lrr-api-canary-v1", "lrr-api-canary-v1"}, + {"lrr-api-canary-v3", "lrr-api-canary"}, + {"lrr-api-canary-v1-beta1", "lrr-api-canary-v1"}, + {"lrr-api-dev", "lrr-api"}, + {"lrr-web", "lrr"}, } for _, ele := range list { - h := s.Match(ele.i) + h := s.Match(ele.i, true) a.Equal(ele.r, h.name) } - h := s.Match("api") + h := s.Match("lrr", false) + a.NotNil(h) + + h = s.Match("lrr-api", false) + a.NotNil(h) + + h = s.Match("lrr-api-canary-v1-beta1", false) + a.Nil(h) + + h = s.Match("api", true) a.Nil(h) - h = s.Match("lr") + h = s.Match("lr", true) a.Nil(h) }) }