Skip to content

Commit 33673c1

Browse files
committed
update X-Canary Header specification, reference to https://www.w3.org/TR/trace-context/#tracestate-header
1 parent e0d8282 commit 33673c1

File tree

5 files changed

+166
-41
lines changed

5 files changed

+166
-41
lines changed

pkg/middlewares/canary/canary.go

+58-17
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string
5050
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName))
5151

5252
if cfg.Product == "" {
53-
return nil, fmt.Errorf("product name required for Canary middleware")
53+
return nil, fmt.Errorf("product name required for canary middleware")
5454
}
5555

5656
expiration := time.Duration(cfg.CacheExpiration)
@@ -97,7 +97,13 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) {
9797
requestID := req.Header.Get(headerXRequestID)
9898
if c.addRequestID {
9999
if requestID == "" {
100-
requestID = generatorUUID()
100+
// extract trace-id as x-request-id
101+
// https://www.w3.org/TR/trace-context/#traceparent-header
102+
if traceparent := req.Header.Get("traceparent"); len(traceparent) >= 55 {
103+
requestID = traceparent[3:35]
104+
} else {
105+
requestID = generatorUUID()
106+
}
101107
req.Header.Set(headerXRequestID, requestID)
102108
}
103109
rw.Header().Set(headerXRequestID, requestID)
@@ -112,6 +118,9 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) {
112118
logData.Core["XRequestID"] = requestID
113119
logData.Core["UserAgent"] = req.Header.Get(headerUA)
114120
logData.Core["Referer"] = req.Header.Get("Referer")
121+
if traceparent := req.Header.Get("traceparent"); traceparent != "" {
122+
logData.Core["Traceparent"] = traceparent
123+
}
115124
}
116125
}
117126

@@ -136,10 +145,10 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
136145
if info.label == "" && info.uid != "" {
137146
labels := c.ls.MustLoadLabels(req.Context(), info.uid, req.Header.Get(headerXRequestID))
138147
for _, l := range labels {
139-
if info.client != "" && !l.MatchClient(info.client) {
148+
if !l.MatchClient(info.client) {
140149
continue
141150
}
142-
if info.channel != "" && !l.MatchChannel(info.channel) {
151+
if !l.MatchChannel(info.channel) {
143152
continue
144153
}
145154
info.label = l.Label
@@ -154,7 +163,7 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
154163

155164
if logData := accesslog.GetLogData(req); logData != nil {
156165
logData.Core["UID"] = info.uid
157-
logData.Core["XCanary"] = req.Header.Values(headerXCanary)
166+
logData.Core["XCanary"] = info.String()
158167
}
159168
}
160169

@@ -239,6 +248,12 @@ func extractUserIDFromBase64(s string) string {
239248
return ""
240249
}
241250

251+
// Canary Header specification, reference to https://www.w3.org/TR/trace-context/#tracestate-header
252+
// X-Canary: label=beta,nofallback
253+
// X-Canary: client=iOS,channel=stable,app=teambition,version=v10.0
254+
// full example
255+
// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,client=iOS,channel=stable,app=teambition,version=v10.0,nofallback,testing
256+
// support fields: label, product, uid, client, channel, app, version, nofallback, testing
242257
type canaryHeader struct {
243258
label string
244259
product string
@@ -252,8 +267,32 @@ type canaryHeader struct {
252267
}
253268

254269
// uid and product will not be extracted
270+
// standard
271+
// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ...
272+
// and compatible with
273+
// X-Canary: beta
274+
// or
275+
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ...
276+
// or
277+
// X-Canary: label=beta
278+
// X-Canary: product=urbs
279+
// X-Canary: uid=5c4057f0be825b390667abee
280+
// X-Canary: nofallback
255281
func (ch *canaryHeader) fromHeader(header http.Header, trust bool) {
256-
ch.feed(header.Values(headerXCanary), trust)
282+
vals := header.Values(headerXCanary)
283+
if len(vals) == 1 {
284+
if strings.IndexByte(vals[0], ',') > 0 {
285+
vals = strings.Split(vals[0], ",")
286+
} else if strings.IndexByte(vals[0], ';') > 0 {
287+
vals = strings.Split(vals[0], ";")
288+
}
289+
}
290+
ch.feed(vals, trust)
291+
}
292+
293+
// label should not be empty
294+
func (ch *canaryHeader) intoHeader(header http.Header) {
295+
header.Set(headerXCanary, ch.String())
257296
}
258297

259298
func (ch *canaryHeader) feed(vals []string, trust bool) {
@@ -287,33 +326,35 @@ func (ch *canaryHeader) feed(vals []string, trust bool) {
287326
}
288327

289328
// label should not be empty
290-
func (ch *canaryHeader) intoHeader(header http.Header) {
329+
func (ch *canaryHeader) String() string {
291330
if ch.label == "" {
292-
return
331+
return ""
293332
}
294-
header.Set(headerXCanary, fmt.Sprintf("label=%s", ch.label))
333+
vals := make([]string, 0, 4)
334+
vals = append(vals, fmt.Sprintf("label=%s", ch.label))
295335
if ch.product != "" {
296-
header.Add(headerXCanary, fmt.Sprintf("product=%s", ch.product))
336+
vals = append(vals, fmt.Sprintf("product=%s", ch.product))
297337
}
298338
if ch.uid != "" {
299-
header.Add(headerXCanary, fmt.Sprintf("uid=%s", ch.uid))
339+
vals = append(vals, fmt.Sprintf("uid=%s", ch.uid))
300340
}
301341
if ch.client != "" {
302-
header.Add(headerXCanary, fmt.Sprintf("client=%s", ch.client))
342+
vals = append(vals, fmt.Sprintf("client=%s", ch.client))
303343
}
304344
if ch.channel != "" {
305-
header.Add(headerXCanary, fmt.Sprintf("channel=%s", ch.channel))
345+
vals = append(vals, fmt.Sprintf("channel=%s", ch.channel))
306346
}
307347
if ch.app != "" {
308-
header.Add(headerXCanary, fmt.Sprintf("app=%s", ch.app))
348+
vals = append(vals, fmt.Sprintf("app=%s", ch.app))
309349
}
310350
if ch.version != "" {
311-
header.Add(headerXCanary, fmt.Sprintf("version=%s", ch.version))
351+
vals = append(vals, fmt.Sprintf("version=%s", ch.version))
312352
}
313353
if ch.nofallback {
314-
header.Add(headerXCanary, "nofallback")
354+
vals = append(vals, "nofallback")
315355
}
316356
if ch.testing {
317-
header.Add(headerXCanary, "testing")
357+
vals = append(vals, "testing")
318358
}
359+
return strings.Join(vals, ",")
319360
}

pkg/middlewares/canary/canary_test.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ func TestCanaryHeader(t *testing.T) {
6666
a.Equal("version", ch.version)
6767
a.True(ch.nofallback)
6868
a.True(ch.testing)
69+
70+
ch = &canaryHeader{}
71+
h = http.Header{}
72+
h.Set(headerXCanary, "label=label,version=version,app=app, channel=channel,client=client, uid=uid,product=product,ip=ip,nofallback,testing")
73+
ch.fromHeader(h, false)
74+
a.Equal("label", ch.label)
75+
a.Equal("", ch.product)
76+
a.Equal("", ch.uid)
77+
a.Equal("client", ch.client)
78+
a.Equal("channel", ch.channel)
79+
a.Equal("app", ch.app)
80+
a.Equal("version", ch.version)
81+
a.True(ch.nofallback)
82+
a.True(ch.testing)
6983
})
7084

7185
t.Run("intoHeader should work", func(t *testing.T) {
@@ -74,7 +88,7 @@ func TestCanaryHeader(t *testing.T) {
7488
ch := &canaryHeader{}
7589
h := http.Header{}
7690
ch.intoHeader(h)
77-
a.Equal(0, len(h.Values(headerXCanary)))
91+
a.Equal("", h.Get(headerXCanary))
7892

7993
ch = &canaryHeader{
8094
label: "label",
@@ -84,7 +98,7 @@ func TestCanaryHeader(t *testing.T) {
8498
}
8599
h = http.Header{}
86100
ch.intoHeader(h)
87-
a.Equal(4, len(h.Values(headerXCanary)))
101+
a.Equal("label=label,product=product,uid=uid,channel=channel", h.Get(headerXCanary))
88102

89103
chn := &canaryHeader{}
90104
chn.fromHeader(h, true)
@@ -103,7 +117,7 @@ func TestCanaryHeader(t *testing.T) {
103117
}
104118
h = http.Header{}
105119
ch.intoHeader(h)
106-
a.Equal(9, len(h.Values(headerXCanary)))
120+
a.Equal("label=label,product=product,uid=uid,client=client,channel=channel,app=app,version=version,nofallback,testing", h.Get(headerXCanary))
107121

108122
chn = &canaryHeader{}
109123
chn.fromHeader(h, true)
@@ -256,8 +270,7 @@ func TestCanary(t *testing.T) {
256270

257271
req = httptest.NewRequest("GET", "http://example.com/foo", nil)
258272
rw = httptest.NewRecorder()
259-
req.Header.Set(headerXCanary, "label=beta")
260-
req.Header.Add(headerXCanary, "client=iOS")
273+
req.Header.Set(headerXCanary, "label=beta,client=iOS")
261274
req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "stable"})
262275
c.processCanary(rw, req)
263276
ch = &canaryHeader{}

pkg/middlewares/canary/request.go

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/containous/traefik/v2/pkg/log"
1717
"github.com/containous/traefik/v2/pkg/version"
18+
"github.com/opentracing/opentracing-go"
1819
)
1920

2021
func init() {
@@ -93,6 +94,13 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err
9394

9495
req.Header.Set(headerUA, userAgent)
9596
req.Header.Set(headerXRequestID, xRequestID)
97+
if span := opentracing.SpanFromContext(ctx); span != nil {
98+
opentracing.GlobalTracer().Inject(
99+
span.Context(),
100+
opentracing.HTTPHeaders,
101+
opentracing.HTTPHeadersCarrier(req.Header))
102+
}
103+
96104
resp, err := client.Do(req)
97105
if err != nil {
98106
if err.(*url.Error).Unwrap() == context.Canceled {

pkg/server/service/loadbalancer/lrr/lrr.go

+33-19
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import (
88
"strings"
99
)
1010

11-
const labelKey = "X-Canary"
12-
13-
var isPortReg = regexp.MustCompile(`^\d+$`)
14-
1511
type namedHandler struct {
1612
http.Handler
1713
name string
@@ -51,21 +47,7 @@ type Balancer struct {
5147
}
5248

5349
func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
54-
// X-Canary: beta
55-
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ...
56-
label := ""
57-
fallback := true
58-
for i, v := range req.Header.Values(labelKey) {
59-
switch {
60-
case strings.HasPrefix(v, "label="):
61-
label = v[6:]
62-
case v == "nofallback":
63-
fallback = false
64-
case i == 0:
65-
label = v
66-
}
67-
}
68-
50+
label, fallback := extractLabel(req.Header)
6951
name := b.serviceName
7052
if label != "" {
7153
name = fmt.Sprintf("%s-%s", name, label)
@@ -90,6 +72,8 @@ func (b *Balancer) AddService(fullServiceName string, handler http.Handler) {
9072
b.handlers = b.handlers.AppendAndSort(h)
9173
}
9274

75+
var isPortReg = regexp.MustCompile(`^\d+$`)
76+
9377
// full service name format (build by fullServiceName function): namespace-serviceName-port
9478
func removeNsPort(fullServiceName, ServiceName string) string {
9579
i := strings.Index(fullServiceName, ServiceName)
@@ -98,3 +82,33 @@ func removeNsPort(fullServiceName, ServiceName string) string {
9882
}
9983
return strings.TrimRight(fullServiceName, "0123456789-") // remove port
10084
}
85+
86+
func extractLabel(header http.Header) (string, bool) {
87+
// standard specification, reference to https://www.w3.org/TR/trace-context/#tracestate-header
88+
// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ...
89+
// and compatible with
90+
// X-Canary: beta
91+
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ...
92+
label := ""
93+
fallback := true
94+
vals := header.Values("X-Canary")
95+
if len(vals) == 1 {
96+
if strings.IndexByte(vals[0], ',') > 0 {
97+
vals = strings.Split(vals[0], ",")
98+
} else if strings.IndexByte(vals[0], ';') > 0 {
99+
vals = strings.Split(vals[0], ";")
100+
}
101+
}
102+
for i, v := range vals {
103+
v = strings.TrimSpace(v)
104+
switch {
105+
case strings.HasPrefix(v, "label="):
106+
label = v[6:]
107+
case v == "nofallback":
108+
fallback = false
109+
case i == 0:
110+
label = v
111+
}
112+
}
113+
return label, fallback
114+
}

pkg/server/service/loadbalancer/lrr/lrr_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,53 @@ func TestLRRBalancer(t *testing.T) {
9595
h = s.Match("lr", true)
9696
a.Nil(h)
9797
})
98+
99+
t.Run("extractLabel should work", func(t *testing.T) {
100+
a := assert.New(t)
101+
header := http.Header{}
102+
label, fallback := extractLabel(header)
103+
a.Equal("", label)
104+
a.True(fallback)
105+
106+
// X-Canary: dev
107+
header.Set("X-Canary", "dev")
108+
label, fallback = extractLabel(header)
109+
a.Equal("dev", label)
110+
a.True(fallback)
111+
112+
// X-Canary: label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback ...
113+
header = http.Header{}
114+
header.Set("X-Canary", "label=beta,product=urbs,uid=5c4057f0be825b390667abee,nofallback")
115+
label, fallback = extractLabel(header)
116+
a.Equal("beta", label)
117+
a.False(fallback)
118+
119+
header = http.Header{}
120+
header.Set("X-Canary", "label=dev,product=urbs,uid=5c4057f0be825b390667abee")
121+
label, fallback = extractLabel(header)
122+
a.Equal("dev", label)
123+
a.True(fallback)
124+
125+
header = http.Header{}
126+
header.Set("X-Canary", "product=urbs,uid=5c4057f0be825b390667abee,nofallback,label=dev")
127+
label, fallback = extractLabel(header)
128+
a.Equal("dev", label)
129+
a.False(fallback)
130+
131+
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ...
132+
header = http.Header{}
133+
header.Set("X-Canary", "label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback")
134+
label, fallback = extractLabel(header)
135+
a.Equal("beta", label)
136+
a.False(fallback)
137+
138+
header = http.Header{}
139+
header.Add("X-Canary", "label=beta")
140+
header.Add("X-Canary", "product=urbs")
141+
header.Add("X-Canary", "uid=5c4057f0be825b390667abee")
142+
header.Add("X-Canary", "nofallback")
143+
label, fallback = extractLabel(header)
144+
a.Equal("beta", label)
145+
a.False(fallback)
146+
})
98147
}

0 commit comments

Comments
 (0)