github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/w3ctrace/state.go (about)

     1  // (c) Copyright IBM Corp. 2021
     2  // (c) Copyright Instana Inc. 2020
     3  
     4  package w3ctrace
     5  
     6  import (
     7  	"bytes"
     8  	"regexp"
     9  	"strings"
    10  )
    11  
    12  // VendorInstana is the Instana vendor key in the `tracestate` list
    13  const VendorInstana = "in"
    14  
    15  // Length of entries that should be filtered first in case, if tracestate has more than `MaxStateEntries` items
    16  const thresholdLen = 128
    17  
    18  var instanaListMemberRegex = regexp.MustCompile("^\\s*" + VendorInstana + "\\s*=\\s*([^,]*)\\s*$")
    19  
    20  // State is list of key=value pairs representing vendor-specific data in the trace context
    21  type State struct {
    22  	// The key-value pairs of other vendors. The "in" list member, if present, will be stored separately
    23  	// (Note: list member is the term the W3C trace context specfication uses for the key-value pairs.)
    24  	listMembers []string
    25  
    26  	// The value for the Instana-specific list member ("in=...").
    27  	instanaTraceStateValue string
    28  }
    29  
    30  // NewState creates a new State with the given values
    31  func NewState(listMembers []string, instanaTraceStateValue string) State {
    32  	return State{
    33  		listMembers:            listMembers,
    34  		instanaTraceStateValue: instanaTraceStateValue,
    35  	}
    36  }
    37  
    38  // FormStateWithInstanaTraceStateValue returns a new state prepended with the provided Instana value. If the original state had an Instana
    39  // list member pair, it is discarded/overwritten.
    40  func FormStateWithInstanaTraceStateValue(st State, instanaTraceStateValue string) State {
    41  	var listMembers []string
    42  	if st.instanaTraceStateValue == "" && instanaTraceStateValue != "" && len(st.listMembers) == MaxStateEntries {
    43  		// The incoming tracestate had the maximum number of list members but no Instana list member, now we are adding an
    44  		// Instana list member, so we would exceed the maximum by one. Hence, we need to discard one of the other list
    45  		// members to stay within the limits mandated by the specification.
    46  		listMembers = st.listMembers[:MaxStateEntries-1]
    47  	} else {
    48  		listMembers = st.listMembers
    49  	}
    50  
    51  	return State{listMembers: listMembers, instanaTraceStateValue: instanaTraceStateValue}
    52  }
    53  
    54  // ParseState parses the value of `tracestate` header. Empty list items are omitted.
    55  func ParseState(traceStateValue string) State {
    56  	listMembers := filterEmptyItems(strings.Split(traceStateValue, ","))
    57  
    58  	// Look for the Instana list member first, before discarding any list members due to length restrictions.
    59  	instanaTraceStateValue, instanaTraceStateIdx := searchInstanaHeader(listMembers)
    60  
    61  	if instanaTraceStateIdx >= 0 {
    62  		// remove the entry for instana from the array of list members
    63  		listMembers = append(listMembers[:instanaTraceStateIdx], listMembers[instanaTraceStateIdx+1:]...)
    64  	}
    65  
    66  	// Depending on whether we found an Instana list member, we can either allow 31 or 32 list members from other vendors.
    67  	maxListMembers := MaxStateEntries
    68  	if instanaTraceStateValue != "" {
    69  		maxListMembers--
    70  	}
    71  
    72  	if len(listMembers) < maxListMembers {
    73  		return State{listMembers: listMembers, instanaTraceStateValue: instanaTraceStateValue}
    74  	}
    75  
    76  	itemsToFilter := len(listMembers) - maxListMembers
    77  	filteredListMembers := listMembers[:0]
    78  	i := 0
    79  	for ; itemsToFilter > 0 && i < len(listMembers); i++ {
    80  		if len(listMembers[i]) > thresholdLen {
    81  			itemsToFilter--
    82  			continue
    83  		}
    84  		filteredListMembers = append(filteredListMembers, listMembers[i])
    85  	}
    86  	filteredListMembers = append(filteredListMembers, listMembers[i:]...)
    87  
    88  	if len(filteredListMembers) > maxListMembers {
    89  		return State{listMembers: filteredListMembers[:maxListMembers], instanaTraceStateValue: instanaTraceStateValue}
    90  	}
    91  
    92  	return State{listMembers: filteredListMembers, instanaTraceStateValue: instanaTraceStateValue}
    93  }
    94  
    95  // FetchInstanaTraceStateValue retrieves the value of the Instana tracestate list member, if any.
    96  func (st State) FetchInstanaTraceStateValue() (string, bool) {
    97  	return st.instanaTraceStateValue, st.instanaTraceStateValue != ""
    98  }
    99  
   100  // String returns string representation of a trace state. The returned value is compatible with the
   101  // `tracestate` header format. If the state has an Instana-specific list member, that one is always rendered first. This
   102  // is optimized for the use case of injecting the string representation of the tracestate header into downstream
   103  // requests.
   104  func (st State) String() string {
   105  	if len(st.listMembers) == 0 && st.instanaTraceStateValue == "" {
   106  		return ""
   107  	}
   108  	if len(st.listMembers) == 0 {
   109  		return VendorInstana + "=" + st.instanaTraceStateValue
   110  	}
   111  
   112  	buf := bytes.NewBuffer(nil)
   113  	if st.instanaTraceStateValue != "" {
   114  		buf.WriteString(VendorInstana)
   115  		buf.WriteString("=")
   116  		buf.WriteString(st.instanaTraceStateValue)
   117  		buf.WriteString(",")
   118  	}
   119  	for _, vd := range st.listMembers {
   120  		buf.WriteString(vd)
   121  		buf.WriteByte(',')
   122  	}
   123  	buf.Truncate(buf.Len() - 1) // remove trailing comma
   124  
   125  	return buf.String()
   126  }
   127  
   128  func filterEmptyItems(entries []string) []string {
   129  	result := entries[:0]
   130  	for _, v := range entries {
   131  		if v != "" {
   132  			result = append(result, v)
   133  		}
   134  	}
   135  
   136  	return result
   137  }
   138  
   139  func searchInstanaHeader(listMembers []string) (string, int) {
   140  	var instanaTraceStateValue string
   141  	instanaTraceStateIdx := -1
   142  	for i, vd := range listMembers {
   143  		matchResult := instanaListMemberRegex.FindStringSubmatch(vd)
   144  		if len(matchResult) == 2 {
   145  			instanaTraceStateValue = strings.TrimSpace(matchResult[1])
   146  			instanaTraceStateIdx = i
   147  			break
   148  		}
   149  	}
   150  	return instanaTraceStateValue, instanaTraceStateIdx
   151  }