Skip to content

Commit

Permalink
update X-Canary Header specification, reference to https://www.w3.org…
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed May 29, 2020
1 parent e0d8282 commit 33673c1
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 41 deletions.
75 changes: 58 additions & 17 deletions pkg/middlewares/canary/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
}

Expand All @@ -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
Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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, ",")
}
23 changes: 18 additions & 5 deletions pkg/middlewares/canary/canary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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",
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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{}
Expand Down
8 changes: 8 additions & 0 deletions pkg/middlewares/canary/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
52 changes: 33 additions & 19 deletions pkg/server/service/loadbalancer/lrr/lrr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import (
"strings"
)

const labelKey = "X-Canary"

var isPortReg = regexp.MustCompile(`^\d+$`)

type namedHandler struct {
http.Handler
name string
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
49 changes: 49 additions & 0 deletions pkg/server/service/loadbalancer/lrr/lrr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

0 comments on commit 33673c1

Please sign in to comment.