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  }