github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/propagation.go (about) 1 // (c) Copyright IBM Corp. 2021 2 // (c) Copyright Instana Inc. 2016 3 4 package instana 5 6 import ( 7 "errors" 8 "net/http" 9 "strings" 10 11 "github.com/instana/go-sensor/w3ctrace" 12 ot "github.com/opentracing/opentracing-go" 13 ) 14 15 // Instana header constants 16 const ( 17 // FieldT Trace ID header 18 FieldT = "x-instana-t" 19 // FieldS Span ID header 20 FieldS = "x-instana-s" 21 // FieldL Level header 22 FieldL = "x-instana-l" 23 // FieldB OT Baggage header 24 FieldB = "x-instana-b-" 25 // FieldSynthetic if set to 1, marks the call as synthetic, e.g. 26 // a healthcheck request 27 FieldSynthetic = "x-instana-synthetic" 28 ) 29 30 func injectTraceContext(sc SpanContext, opaqueCarrier interface{}) error { 31 roCarrier, ok := opaqueCarrier.(ot.TextMapReader) 32 if !ok { 33 return ot.ErrInvalidCarrier 34 } 35 36 // Handle pre-existing case-sensitive keys 37 exstfieldT := FieldT 38 exstfieldS := FieldS 39 exstfieldL := FieldL 40 exstfieldB := FieldB 41 42 roCarrier.ForeachKey(func(k, v string) error { 43 switch strings.ToLower(k) { 44 case FieldT: 45 exstfieldT = k 46 case FieldS: 47 exstfieldS = k 48 case FieldL: 49 exstfieldL = k 50 default: 51 if strings.HasPrefix(strings.ToLower(k), FieldB) { 52 exstfieldB = string([]rune(k)[:len(FieldB)]) 53 } 54 } 55 return nil 56 }) 57 58 carrier, ok := opaqueCarrier.(ot.TextMapWriter) 59 if !ok { 60 return ot.ErrInvalidCarrier 61 } 62 63 if c, ok := opaqueCarrier.(ot.HTTPHeadersCarrier); ok { 64 // Even though the godoc claims that the key passed to (*http.Header).Set() 65 // is case-insensitive, it actually normalizes it using textproto.CanonicalMIMEHeaderKey() 66 // before populating the value. As a result headers with non-canonical will not be 67 // overwritten with a new value. This is only the case if header names were set while 68 // initializing the http.Header instance, i.e. 69 // h := http.Headers{"X-InStAnA-T": {"abc123"}} 70 // and does not apply to a common case when requests are being created using http.NewRequest() 71 // or http.ReadRequest() that call (*http.Header).Set() to set header values. 72 h := http.Header(c) 73 delete(h, exstfieldT) 74 delete(h, exstfieldS) 75 delete(h, exstfieldL) 76 77 for key := range h { 78 if strings.HasPrefix(strings.ToLower(key), FieldB) { 79 delete(h, key) 80 } 81 } 82 83 addW3CTraceContext(h, sc) 84 addEUMHeaders(h, sc) 85 } 86 87 if !sc.Suppressed { 88 carrier.Set(exstfieldT, FormatID(sc.TraceID)) 89 carrier.Set(exstfieldS, FormatID(sc.SpanID)) 90 } else { 91 // remove trace context keys from the carrier 92 switch c := opaqueCarrier.(type) { 93 case ot.HTTPHeadersCarrier: 94 h := http.Header(c) 95 h.Del(exstfieldT) 96 h.Del(exstfieldS) 97 case ot.TextMapCarrier: 98 delete(c, exstfieldT) 99 delete(c, exstfieldS) 100 case interface{ RemoveAll() }: 101 // in case carrier has the RemoveAll() method that wipes all trace 102 // headers, for example the instasarama.ProducerMessagCarrier, we 103 // use it to remove the context of a suppressed trace 104 c.RemoveAll() 105 } 106 } 107 108 carrier.Set(exstfieldL, formatLevel(sc)) 109 110 for k, v := range sc.Baggage { 111 carrier.Set(exstfieldB+k, v) 112 } 113 114 return nil 115 } 116 117 // This method searches for Instana headers (FieldT, FieldS, FieldL and header with name prefixed with FieldB) 118 // and try to parse their values. It also tries to extract W3C context and assign it inside returned object. W3C context 119 // will be propagated further and can be used as a fallback. 120 func extractTraceContext(opaqueCarrier interface{}) (SpanContext, error) { 121 spanContext := SpanContext{ 122 Baggage: make(map[string]string), 123 } 124 125 carrier, ok := opaqueCarrier.(ot.TextMapReader) 126 if !ok { 127 return spanContext, ot.ErrInvalidCarrier 128 } 129 130 if c, ok := opaqueCarrier.(ot.HTTPHeadersCarrier); ok { 131 pickupW3CTraceContext(http.Header(c), &spanContext) 132 } 133 134 // Iterate over the headers, look for Instana headers and try to parse them. 135 // In case of error interrupt, iteration and return it. 136 err := carrier.ForeachKey(func(k, v string) error { 137 var err error 138 139 switch strings.ToLower(k) { 140 case FieldT: 141 spanContext.TraceIDHi, spanContext.TraceID, err = ParseLongID(v) 142 if err != nil { 143 return ot.ErrSpanContextCorrupted 144 } 145 case FieldS: 146 spanContext.SpanID, err = ParseID(v) 147 if err != nil { 148 return ot.ErrSpanContextCorrupted 149 } 150 case FieldL: 151 // When FieldL is present and equals to "0", then spanContext is suppressed. 152 // In addition to that non-empty correlation data may be extracted. 153 suppressed, corrData, err := parseLevel(v) 154 if err != nil { 155 sensor.logger.Info("failed to parse ", k, ": ", err, " (", v, ")") 156 // use defaults 157 suppressed, corrData = false, EUMCorrelationData{} 158 } 159 160 spanContext.Suppressed = suppressed 161 if !spanContext.Suppressed { 162 spanContext.Correlation = corrData 163 } 164 default: 165 if strings.HasPrefix(strings.ToLower(k), FieldB) { 166 // preserve original case of the baggage key 167 spanContext.Baggage[k[len(FieldB):]] = v 168 } 169 } 170 171 return nil 172 }) 173 if err != nil { 174 return spanContext, err 175 } 176 177 // reset the trace IDs if a correlation ID has been provided 178 if spanContext.Correlation.ID != "" { 179 spanContext.TraceIDHi, spanContext.TraceID, spanContext.SpanID = 0, 0, 0 180 181 return spanContext, nil 182 } 183 184 if spanContext.IsZero() { 185 return spanContext, ot.ErrSpanContextNotFound 186 } 187 188 // When the context is not suppressed and one of Instana ID headers set. 189 if !spanContext.Suppressed && 190 (spanContext.SpanID == 0 != (spanContext.TraceIDHi == 0 && spanContext.TraceID == 0)) { 191 sensor.logger.Debug("broken Instana trace context:", 192 " SpanID=", FormatID(spanContext.SpanID), 193 " TraceID=", FormatLongID(spanContext.TraceIDHi, spanContext.TraceID)) 194 195 // Check if w3 context was found 196 if !spanContext.W3CContext.IsZero() { 197 // Return SpanContext with w3 context, ignore other values 198 return SpanContext{ 199 W3CContext: spanContext.W3CContext, 200 }, nil 201 } 202 203 return spanContext, ot.ErrSpanContextCorrupted 204 } 205 206 return spanContext, nil 207 } 208 209 func addW3CTraceContext(h http.Header, sc SpanContext) { 210 traceID, spanID := FormatLongID(sc.TraceIDHi, sc.TraceID), FormatID(sc.SpanID) 211 trCtx := sc.W3CContext 212 213 // check for an existing w3c trace 214 if trCtx.IsZero() { 215 // initiate trace if none 216 trCtx = w3ctrace.New(w3ctrace.Parent{ 217 Version: w3ctrace.Version_Max, 218 TraceID: traceID, 219 ParentID: spanID, 220 Flags: w3ctrace.Flags{ 221 Sampled: !sc.Suppressed, 222 }, 223 }) 224 } 225 226 // update the traceparent parent ID 227 p := trCtx.Parent() 228 p.ParentID = spanID 229 // sync the traceparent `sampled` flags with the X-Instana-L value 230 p.Flags.Sampled = !sc.Suppressed 231 232 trCtx.RawParent = p.String() 233 234 // participate in w3c trace context if tracing is enabled 235 if !sc.Suppressed { 236 // propagate truncated trace ID downstream 237 trCtx.RawState = w3ctrace.FormStateWithInstanaTraceStateValue(trCtx.State(), FormatID(sc.TraceID)+";"+spanID).String() 238 } 239 240 w3ctrace.Inject(trCtx, h) 241 } 242 243 func pickupW3CTraceContext(h http.Header, sc *SpanContext) { 244 trCtx, err := w3ctrace.Extract(h) 245 if err != nil { 246 return 247 } 248 sc.W3CContext = trCtx 249 } 250 251 func addEUMHeaders(h http.Header, sc SpanContext) { 252 // Preserve original Server-Timing header values by combining them into a comma-separated list 253 st := append(h["Server-Timing"], "intid;desc="+FormatID(sc.TraceID)) 254 h.Set("Server-Timing", strings.Join(st, ", ")) 255 } 256 257 var errMalformedHeader = errors.New("malformed header value") 258 259 func parseLevel(s string) (bool, EUMCorrelationData, error) { 260 const ( 261 levelState uint8 = iota 262 partSeparatorState 263 correlationPartState 264 correlationTypeState 265 correlationIDState 266 finalState 267 ) 268 269 if s == "" { 270 return false, EUMCorrelationData{}, nil 271 } 272 273 var ( 274 typeInd int 275 state uint8 276 level, corrType, corrID string 277 ) 278 PARSE: 279 for ptr := 0; state != finalState && ptr < len(s); ptr++ { 280 switch state { 281 case levelState: // looking for 0 or 1 282 level = s[ptr : ptr+1] 283 284 if level != "0" && level != "1" { 285 break PARSE 286 } 287 288 if ptr == len(s)-1 { // no correlation ID provided 289 state = finalState 290 } else { 291 state = partSeparatorState 292 } 293 case partSeparatorState: // skip OWS while looking for ',' 294 switch s[ptr] { 295 case ' ', '\t': // advance 296 case ',': 297 state = correlationPartState 298 default: 299 break PARSE 300 } 301 case correlationPartState: // skip OWS while searching for 'correlationType=' prefix 302 switch { 303 case s[ptr] == ' ' || s[ptr] == '\t': // advance 304 case strings.HasPrefix(s[ptr:], "correlationType="): 305 ptr += 15 // advance to the end of prefix 306 typeInd = ptr + 1 307 state = correlationTypeState 308 default: 309 break PARSE 310 } 311 case correlationTypeState: // skip OWS while looking for ';' 312 switch s[ptr] { 313 case ' ', '\t': // possibly trailing OWS, advance 314 case ';': 315 state = correlationIDState 316 default: 317 corrType = s[typeInd : ptr+1] 318 } 319 case correlationIDState: // skip OWS while searching for 'correlationId=' prefix 320 switch { 321 case s[ptr] == ' ' || s[ptr] == '\t': // leading OWS, advance 322 case strings.HasPrefix(s[ptr:], "correlationId="): 323 ptr += 14 324 corrID = s[ptr:] 325 state = finalState 326 default: 327 break PARSE 328 } 329 default: 330 break PARSE 331 } 332 } 333 334 if state != finalState { 335 return false, EUMCorrelationData{}, errMalformedHeader 336 } 337 338 return level == "0", EUMCorrelationData{Type: corrType, ID: corrID}, nil 339 } 340 341 func formatLevel(sc SpanContext) string { 342 if sc.Suppressed { 343 return "0" 344 } 345 346 return "1" 347 }