Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Commit 7999321

Browse files
authored
Tracestate propagate (#901)
* Propagate tracestate * Fixed review comments. * Fixed leading/trailing OWS removal. * Refactor to create separate package to inject/extract tracestate for http. * Fix review comments. * removed unnecessary check and import comment. * Revert "removed unnecessary check and import comment." This reverts commit 5349341. * Revert "Fix review comments." This reverts commit 40c2858. * Revert "Refactor to create separate package to inject/extract tracestate for http." This reverts commit 5574ce8. * shorten variable name.
1 parent 209434a commit 7999321

File tree

2 files changed

+219
-6
lines changed

2 files changed

+219
-6
lines changed

plugin/ochttp/propagation/tracecontext/propagation.go

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,29 @@ import (
2424

2525
"go.opencensus.io/trace"
2626
"go.opencensus.io/trace/propagation"
27+
"go.opencensus.io/trace/tracestate"
28+
"regexp"
2729
)
2830

2931
const (
30-
supportedVersion = 0
31-
maxVersion = 254
32-
header = "traceparent"
32+
supportedVersion = 0
33+
maxVersion = 254
34+
maxTracestateLen = 512
35+
traceparentHeader = "traceparent"
36+
tracestateHeader = "tracestate"
37+
trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$`
3338
)
3439

40+
var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt)
41+
3542
var _ propagation.HTTPFormat = (*HTTPFormat)(nil)
3643

3744
// HTTPFormat implements the TraceContext trace propagation format.
3845
type HTTPFormat struct{}
3946

4047
// SpanContextFromRequest extracts a span context from incoming requests.
4148
func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
42-
h := req.Header.Get(header)
49+
h := req.Header.Get(traceparentHeader)
4350
if h == "" {
4451
return trace.SpanContext{}, false
4552
}
@@ -87,15 +94,69 @@ func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanCon
8794
return trace.SpanContext{}, false
8895
}
8996

97+
sc.Tracestate = tracestateFromRequest(req)
9098
return sc, true
9199
}
92100

93-
// SpanContextToRequest modifies the given request to include a header.
101+
// TODO(rghetia): return an empty Tracestate when parsing tracestate header encounters an error.
102+
// Revisit to return additional boolean value to indicate parsing error when following issues
103+
// are resolved.
104+
// https://github.com/w3c/distributed-tracing/issues/172
105+
// https://github.com/w3c/distributed-tracing/issues/175
106+
func tracestateFromRequest(req *http.Request) *tracestate.Tracestate {
107+
h := req.Header.Get(tracestateHeader)
108+
if h == "" {
109+
return nil
110+
}
111+
112+
var entries []tracestate.Entry
113+
pairs := strings.Split(h, ",")
114+
hdrLenWithoutOWS := len(pairs) - 1 // Number of commas
115+
for _, pair := range pairs {
116+
matches := trimOWSRegExp.FindStringSubmatch(pair)
117+
if matches == nil {
118+
return nil
119+
}
120+
pair = matches[1]
121+
hdrLenWithoutOWS += len(pair)
122+
if hdrLenWithoutOWS > maxTracestateLen {
123+
return nil
124+
}
125+
kv := strings.Split(pair, "=")
126+
if len(kv) != 2 {
127+
return nil
128+
}
129+
entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]})
130+
}
131+
ts, err := tracestate.New(nil, entries...)
132+
if err != nil {
133+
return nil
134+
}
135+
136+
return ts
137+
}
138+
139+
func tracestateToRequest(sc trace.SpanContext, req *http.Request) {
140+
var pairs = make([]string, 0, len(sc.Tracestate.Entries()))
141+
if sc.Tracestate != nil {
142+
for _, entry := range sc.Tracestate.Entries() {
143+
pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "="))
144+
}
145+
h := strings.Join(pairs, ",")
146+
147+
if h != "" && len(h) <= maxTracestateLen {
148+
req.Header.Set(tracestateHeader, h)
149+
}
150+
}
151+
}
152+
153+
// SpanContextToRequest modifies the given request to include traceparent and tracestate headers.
94154
func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
95155
h := fmt.Sprintf("%x-%x-%x-%x",
96156
[]byte{supportedVersion},
97157
sc.TraceID[:],
98158
sc.SpanID[:],
99159
[]byte{byte(sc.TraceOptions)})
100-
req.Header.Set(header, h)
160+
req.Header.Set(traceparentHeader, h)
161+
tracestateToRequest(sc, req)
101162
}

plugin/ochttp/propagation/tracecontext/propagation_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,29 @@
1515
package tracecontext
1616

1717
import (
18+
"fmt"
1819
"net/http"
1920
"reflect"
2021
"testing"
2122

2223
"go.opencensus.io/trace"
24+
"go.opencensus.io/trace/tracestate"
25+
"strings"
26+
)
27+
28+
var (
29+
tpHeader = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
30+
traceID = trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54}
31+
spanID = trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183}
32+
traceOpt = trace.TraceOptions(1)
33+
oversizeValue = strings.Repeat("a", maxTracestateLen/2)
34+
oversizeEntry1 = tracestate.Entry{Key: "foo", Value: oversizeValue}
35+
oversizeEntry2 = tracestate.Entry{Key: "hello", Value: oversizeValue}
36+
entry1 = tracestate.Entry{Key: "foo", Value: "bar"}
37+
entry2 = tracestate.Entry{Key: "hello", Value: "world example"}
38+
oversizeTs, _ = tracestate.New(nil, oversizeEntry1, oversizeEntry2)
39+
defaultTs, _ = tracestate.New(nil, nil...)
40+
nonDefaultTs, _ = tracestate.New(nil, entry1, entry2)
2341
)
2442

2543
func TestHTTPFormat_FromRequest(t *testing.T) {
@@ -113,3 +131,137 @@ func TestHTTPFormat_ToRequest(t *testing.T) {
113131
})
114132
}
115133
}
134+
135+
func TestHTTPFormatTracestate_FromRequest(t *testing.T) {
136+
scWithNonDefaultTracestate := trace.SpanContext{
137+
TraceID: traceID,
138+
SpanID: spanID,
139+
TraceOptions: traceOpt,
140+
Tracestate: nonDefaultTs,
141+
}
142+
143+
scWithDefaultTracestate := trace.SpanContext{
144+
TraceID: traceID,
145+
SpanID: spanID,
146+
TraceOptions: traceOpt,
147+
Tracestate: defaultTs,
148+
}
149+
150+
tests := []struct {
151+
name string
152+
tpHeader string
153+
tsHeader string
154+
wantSc trace.SpanContext
155+
wantOk bool
156+
}{
157+
{
158+
name: "tracestate invalid entries delimiter",
159+
tpHeader: tpHeader,
160+
tsHeader: "foo=bar;hello=world",
161+
wantSc: scWithDefaultTracestate,
162+
wantOk: true,
163+
},
164+
{
165+
name: "tracestate invalid key-value delimiter",
166+
tpHeader: tpHeader,
167+
tsHeader: "foo=bar,hello-world",
168+
wantSc: scWithDefaultTracestate,
169+
wantOk: true,
170+
},
171+
{
172+
name: "tracestate invalid value character",
173+
tpHeader: tpHeader,
174+
tsHeader: "foo=bar,hello=world example \u00a0 ",
175+
wantSc: scWithDefaultTracestate,
176+
wantOk: true,
177+
},
178+
{
179+
name: "tracestate blank key-value",
180+
tpHeader: tpHeader,
181+
tsHeader: "foo=bar, ",
182+
wantSc: scWithDefaultTracestate,
183+
wantOk: true,
184+
},
185+
{
186+
name: "tracestate oversize header",
187+
tpHeader: tpHeader,
188+
tsHeader: fmt.Sprintf("foo=%s,hello=%s", oversizeValue, oversizeValue),
189+
wantSc: scWithDefaultTracestate,
190+
wantOk: true,
191+
},
192+
{
193+
name: "tracestate valid",
194+
tpHeader: tpHeader,
195+
tsHeader: "foo=bar , hello=world example",
196+
wantSc: scWithNonDefaultTracestate,
197+
wantOk: true,
198+
},
199+
}
200+
201+
f := &HTTPFormat{}
202+
for _, tt := range tests {
203+
t.Run(tt.name, func(t *testing.T) {
204+
req, _ := http.NewRequest("GET", "http://example.com", nil)
205+
req.Header.Set("traceparent", tt.tpHeader)
206+
req.Header.Set("tracestate", tt.tsHeader)
207+
208+
gotSc, gotOk := f.SpanContextFromRequest(req)
209+
if !reflect.DeepEqual(gotSc, tt.wantSc) {
210+
t.Errorf("HTTPFormat.FromRequest() gotTs = %v, want %v", gotSc.Tracestate, tt.wantSc.Tracestate)
211+
}
212+
if gotOk != tt.wantOk {
213+
t.Errorf("HTTPFormat.FromRequest() gotOk = %v, want %v", gotOk, tt.wantOk)
214+
}
215+
})
216+
}
217+
}
218+
219+
func TestHTTPFormatTracestate_ToRequest(t *testing.T) {
220+
tests := []struct {
221+
name string
222+
sc trace.SpanContext
223+
wantHeader string
224+
}{
225+
{
226+
name: "valid span context with default tracestate",
227+
sc: trace.SpanContext{
228+
TraceID: traceID,
229+
SpanID: spanID,
230+
TraceOptions: traceOpt,
231+
},
232+
wantHeader: "",
233+
},
234+
{
235+
name: "valid span context with non default tracestate",
236+
sc: trace.SpanContext{
237+
TraceID: traceID,
238+
SpanID: spanID,
239+
TraceOptions: traceOpt,
240+
Tracestate: nonDefaultTs,
241+
},
242+
wantHeader: "foo=bar,hello=world example",
243+
},
244+
{
245+
name: "valid span context with oversize tracestate",
246+
sc: trace.SpanContext{
247+
TraceID: traceID,
248+
SpanID: spanID,
249+
TraceOptions: traceOpt,
250+
Tracestate: oversizeTs,
251+
},
252+
wantHeader: "",
253+
},
254+
}
255+
for _, tt := range tests {
256+
t.Run(tt.name, func(t *testing.T) {
257+
f := &HTTPFormat{}
258+
req, _ := http.NewRequest("GET", "http://example.com", nil)
259+
f.SpanContextToRequest(tt.sc, req)
260+
261+
h := req.Header.Get("tracestate")
262+
if got, want := h, tt.wantHeader; got != want {
263+
t.Errorf("HTTPFormat.ToRequest() tracestate header = %v, want %v", got, want)
264+
}
265+
})
266+
}
267+
}

0 commit comments

Comments
 (0)