Skip to content

Commit aea2cbb

Browse files
author
Bastian Krol
committed
do not discard the Instana tracestate list member
Previously, when more than 32 list members were in the incoming tracestate header value, and the Instana-specific list member (in=...) was at position 33 or later, we would drop it together with the other list members that need to be droppped. With this change, we give the Instana list member higher priority than list members from other vendors. We also keep it separate in the TraceState struct. Furthermore, this simplifies/reduces the API of the TraceState module. We do not have a use case for adding/removing list members for arbitrary vendors - we only need to propagate the list members we received and make sure that the Instana trace state value is added in the left-most position.
1 parent 5d5bdee commit aea2cbb

File tree

5 files changed

+189
-166
lines changed

5 files changed

+189
-166
lines changed

propagation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func addW3CTraceContext(h http.Header, sc SpanContext) {
234234
// participate in w3c trace context if tracing is enabled
235235
if !sc.Suppressed {
236236
// propagate truncated trace ID downstream
237-
trCtx.RawState = trCtx.State().Add(w3ctrace.VendorInstana, FormatID(sc.TraceID)+";"+spanID).String()
237+
trCtx.RawState = trCtx.State().SetInstanaTraceStateValue(FormatID(sc.TraceID)+";"+spanID).String()
238238
}
239239

240240
w3ctrace.Inject(trCtx, h)

span_context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func NewSpanContext(parent SpanContext) SpanContext {
103103
// check if there is Instana state stored in the W3C tracestate header
104104
if foreignTrace {
105105
w3cState := c.W3CContext.State()
106-
if ancestor, ok := w3cState.Fetch(w3ctrace.VendorInstana); ok {
106+
if ancestor, ok := w3cState.FetchInstanaTraceStateValue(); ok {
107107
if ref, ok := parseW3CInstanaState(ancestor); ok {
108108
c.Links = append(c.Links, ref)
109109
}
@@ -160,7 +160,7 @@ func restoreFromW3CTraceState(trCtx w3ctrace.Context) SpanContext {
160160
W3CContext: trCtx,
161161
}
162162

163-
state, ok := trCtx.State().Fetch(w3ctrace.VendorInstana)
163+
state, ok := trCtx.State().FetchInstanaTraceStateValue()
164164
if !ok {
165165
return c
166166
}

w3ctrace/context_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestContext_State(t *testing.T) {
9595
RawState: exampleTraceState,
9696
}
9797

98-
assert.Equal(t, w3ctrace.State{"vendorname1=opaqueValue1 ", " vendorname2=opaqueValue2"}, trCtx.State())
98+
assert.Equal(t, w3ctrace.NewState([]string{"vendorname1=opaqueValue1 ", " vendorname2=opaqueValue2"}, ""), trCtx.State())
9999
}
100100

101101
func TestContext_Parent(t *testing.T) {

w3ctrace/state.go

Lines changed: 82 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package w3ctrace
55

66
import (
77
"bytes"
8+
"regexp"
89
"strings"
910
)
1011

@@ -17,109 +18,117 @@ const maxKVPairs = 32
1718
// Length of entries that should be filtered first in case, if tracestate has more than `maxKVPairs` items
1819
const thresholdLen = 128
1920

20-
// State is list of key=value pairs representing vendor-specific data in the trace context
21-
type State []string
21+
var instanaListMemberRegex = regexp.MustCompile("^\\s*" + VendorInstana + "\\s*=\\s*([^,]*)\\s*$")
2222

23-
// ParseState parses the value of `tracestate` header. Empty list items are omitted.
24-
func ParseState(traceStateValue string) (State, error) {
25-
states := filterEmptyItems(strings.Split(traceStateValue, ","))
26-
if len(states) < maxKVPairs {
27-
return states, nil
28-
}
23+
// State is list of key=value pairs representing vendor-specific data in the trace context
24+
type State struct {
25+
// The key-value pairs of other vendors. The "in" list member, if present, will be stored separately
26+
// (Note: list member is the term the W3C trace context specfication uses for the key-value pairs.)
27+
listMembers []string
2928

30-
itemsToFilter := len(states) - maxKVPairs
31-
filteredStates := states[:0]
29+
// The value for the Instana-specific list member ("in=...").
30+
instanaTraceStateValue string
31+
}
3232

33-
i := 0
34-
for ; itemsToFilter > 0 && i < len(states); i++ {
33+
// makeState creates a new State with the given values
34+
func NewState(listMembers []string, instanaTraceStateValue string) State {
35+
return State{
36+
listMembers: listMembers,
37+
instanaTraceStateValue: instanaTraceStateValue,
38+
}
39+
}
3540

36-
if len(states[i]) > thresholdLen {
37-
itemsToFilter--
38-
continue
41+
// ParseState parses the value of `tracestate` header. Empty list items are omitted.
42+
func ParseState(traceStateValue string) (State, error) {
43+
listMembers := filterEmptyItems(strings.Split(traceStateValue, ","))
44+
45+
// Look for the Instana list member first, before discarding any list members due to length restrictions.
46+
var instanaTraceStateValue string
47+
instanaTraceStateIdx := -1
48+
for i, vd := range listMembers {
49+
matchResult := instanaListMemberRegex.FindStringSubmatch(vd)
50+
if len(matchResult) == 2 {
51+
instanaTraceStateValue = strings.TrimSpace(matchResult[1])
52+
instanaTraceStateIdx = i
53+
break
3954
}
40-
41-
filteredStates = append(filteredStates, states[i])
4255
}
43-
filteredStates = append(filteredStates, states[i:]...)
4456

45-
if len(filteredStates) > maxKVPairs {
46-
return filteredStates[:maxKVPairs], nil
57+
if instanaTraceStateIdx >= 0 {
58+
// remove the entry for instana from the array of list members
59+
listMembers = append(listMembers[:instanaTraceStateIdx], listMembers[instanaTraceStateIdx+1:]...)
4760
}
4861

49-
return filteredStates, nil
50-
}
51-
52-
// Add returns a new state prepended with provided vendor-specific data. It removes any existing
53-
// entries for this vendor and returns the same state if vendor is empty. If the number of entries
54-
// in a state reaches the MaxStateEntries, rest of the items will be truncated
55-
func (st State) Add(vendor, data string) State {
56-
if vendor == "" {
57-
return st
62+
// Depending on whether we found an Instana list member, we can either allow 31 or 32 list members from other vendors.
63+
maxListMembers := maxKVPairs
64+
if instanaTraceStateValue != "" {
65+
maxListMembers--
5866
}
5967

60-
newSt := make(State, 1, len(st)+1)
61-
newSt[0] = vendor + "=" + data
62-
newSt = append(newSt, st.Remove(vendor)...)
63-
64-
// truncate the state if it reached the max number of entries
65-
if len(newSt) > MaxStateEntries {
66-
newSt = newSt[:MaxStateEntries]
68+
if len(listMembers) < maxListMembers {
69+
return State{listMembers: listMembers, instanaTraceStateValue: instanaTraceStateValue}, nil
6770
}
6871

69-
return newSt
70-
}
71-
72-
// Fetch retrieves stored vendor-specific data for given vendor
73-
func (st State) Fetch(vendor string) (string, bool) {
74-
i := st.Index(vendor)
75-
if i < 0 {
76-
return "", false
72+
itemsToFilter := len(listMembers) - maxListMembers
73+
filteredListMembers := listMembers[:0]
74+
i := 0
75+
for ; itemsToFilter > 0 && i < len(listMembers); i++ {
76+
if len(listMembers[i]) > thresholdLen {
77+
itemsToFilter--
78+
continue
79+
}
80+
filteredListMembers = append(filteredListMembers, listMembers[i])
7781
}
82+
filteredListMembers = append(filteredListMembers, listMembers[i:]...)
7883

79-
return strings.TrimPrefix(st[i], vendor+"="), true
80-
}
81-
82-
// Index returns the index of vendor-specific data for given vendor in the state.
83-
// It returns -1 if the state does not contain data for this vendor.
84-
func (st State) Index(vendor string) int {
85-
prefix := vendor + "="
86-
87-
for i, vd := range st {
88-
if strings.HasPrefix(vd, prefix) {
89-
return i
90-
}
84+
if len(filteredListMembers) > maxListMembers {
85+
return State{listMembers: filteredListMembers[:maxListMembers], instanaTraceStateValue: instanaTraceStateValue}, nil
9186
}
9287

93-
return -1
88+
return State{listMembers: filteredListMembers, instanaTraceStateValue: instanaTraceStateValue}, nil
9489
}
9590

96-
// Remove returns a new state without data for specified vendor. It returns the same state if vendor is empty
97-
func (st State) Remove(vendor string) State {
98-
if vendor == "" {
99-
return st
91+
// SetInstanaValue returns a new state prepended with the provided Instana value. If the original state had an Instana
92+
// list member pair, it is discarded/overwritten.
93+
func (st State) SetInstanaTraceStateValue(instanaTraceStateValue string) State {
94+
var listMembers []string
95+
if st.instanaTraceStateValue == "" && instanaTraceStateValue != "" && len(st.listMembers) == maxKVPairs {
96+
// The incoming tracestate had the maximum number of list members but no Instana list member, now we are adding an
97+
// Instana list member, so we would exceed the maximum by one. Hence, we need to discard one of the other list
98+
// members to stay within the limits mandated by the specification.
99+
listMembers = st.listMembers[:maxKVPairs-1]
100+
} else {
101+
listMembers = st.listMembers
100102
}
101103

102-
prefix := vendor + "="
103-
104-
var newSt State
105-
for _, vd := range st {
106-
if !strings.HasPrefix(vd, prefix) {
107-
newSt = append(newSt, vd)
108-
}
109-
}
104+
return State{listMembers: listMembers, instanaTraceStateValue: instanaTraceStateValue}
105+
}
110106

111-
return newSt
107+
// FetchInstanaTraceStateValue retrieves the value of the Instana tracestate list member, if any.
108+
func (st State) FetchInstanaTraceStateValue() (string, bool) {
109+
return st.instanaTraceStateValue, st.instanaTraceStateValue != ""
112110
}
113111

114112
// String returns string representation of a trace state. The returned value is compatible with the
115-
// `tracestate` header format
113+
// `tracestate` header format. If the state has an Instana-specific list member, that one is always rendered first. This
114+
// is optimized for the use case of injecting the string represenation of the tracestate header into downstream
115+
// requests.
116116
func (st State) String() string {
117-
if len(st) == 0 {
117+
if len(st.listMembers) == 0 && st.instanaTraceStateValue == "" {
118118
return ""
119119
}
120+
if len(st.listMembers) == 0 {
121+
return VendorInstana + "=" + st.instanaTraceStateValue
122+
}
120123

121124
buf := bytes.NewBuffer(nil)
122-
for _, vd := range st {
125+
if st.instanaTraceStateValue != "" {
126+
buf.WriteString(VendorInstana)
127+
buf.WriteString("=")
128+
buf.WriteString(st.instanaTraceStateValue)
129+
buf.WriteString(",")
130+
}
131+
for _, vd := range st.listMembers {
123132
buf.WriteString(vd)
124133
buf.WriteByte(',')
125134
}

0 commit comments

Comments
 (0)