Skip to content

Commit

Permalink
Merge pull request #138 from instana/correlation_id_support
Browse files Browse the repository at this point in the history
Support EUM script correlation
  • Loading branch information
Andrew Slotin authored Jul 21, 2020
2 parents d41cd22 + 9ffbfe7 commit 874e706
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 41 deletions.
25 changes: 25 additions & 0 deletions instrumentation_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func TestTracingHandlerFunc_Write(t *testing.T) {
assert.Equal(t, 0, span.Ec)
assert.EqualValues(t, instana.EntrySpanKind, span.Kind)
assert.False(t, span.Synthetic)
assert.Empty(t, span.CorrelationType)
assert.Empty(t, span.CorrelationID)

assert.Nil(t, span.ForeignParent)

Expand Down Expand Up @@ -214,6 +216,29 @@ func TestTracingHandlerFunc_SyntheticCall(t *testing.T) {
assert.True(t, spans[0].Synthetic)
}

func TestTracingHandlerFunc_EUMCall(t *testing.T) {
recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder))

h := instana.TracingHandlerFunc(s, "test-handler", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, "Ok")
})

rec := httptest.NewRecorder()

req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set(instana.FieldL, "1,correlationType=web;correlationId=eum correlation id")

h.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

spans := recorder.GetQueuedSpans()
require.Len(t, spans, 1)
assert.Equal(t, "web", spans[0].CorrelationType)
assert.Equal(t, "eum correlation id", spans[0].CorrelationID)
}

func TestTracingHandlerFunc_PanicHandling(t *testing.T) {
recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder))
Expand Down
50 changes: 27 additions & 23 deletions json_span.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,34 +111,38 @@ func newW3CForeignParent(trCtx w3ctrace.Context) *ForeignParent {

// Span represents the OpenTracing span document to be sent to the agent
type Span struct {
TraceID int64 `json:"t"`
ParentID int64 `json:"p,omitempty"`
SpanID int64 `json:"s"`
Timestamp uint64 `json:"ts"`
Duration uint64 `json:"d"`
Name string `json:"n"`
From *fromS `json:"f"`
Batch *batchInfo `json:"b,omitempty"`
Kind int `json:"k"`
Ec int `json:"ec,omitempty"`
Data typedSpanData `json:"data"`
Synthetic bool `json:"sy,omitempty"`
ForeignParent *ForeignParent `json:"fp,omitempty"`
TraceID int64 `json:"t"`
ParentID int64 `json:"p,omitempty"`
SpanID int64 `json:"s"`
Timestamp uint64 `json:"ts"`
Duration uint64 `json:"d"`
Name string `json:"n"`
From *fromS `json:"f"`
Batch *batchInfo `json:"b,omitempty"`
Kind int `json:"k"`
Ec int `json:"ec,omitempty"`
Data typedSpanData `json:"data"`
Synthetic bool `json:"sy,omitempty"`
ForeignParent *ForeignParent `json:"fp,omitempty"`
CorrelationType string `json:"crtp,omitempty"`
CorrelationID string `json:"crid,omitempty"`
}

func newSpan(span *spanS) Span {
data := RegisteredSpanType(span.Operation).ExtractData(span)
sp := Span{
TraceID: span.context.TraceID,
ParentID: span.context.ParentID,
SpanID: span.context.SpanID,
Timestamp: uint64(span.Start.UnixNano()) / uint64(time.Millisecond),
Duration: uint64(span.Duration) / uint64(time.Millisecond),
Name: string(data.Type()),
Ec: span.ErrorCount,
ForeignParent: newForeignParent(span.context.ForeignParent),
Kind: int(data.Kind()),
Data: data,
TraceID: span.context.TraceID,
ParentID: span.context.ParentID,
SpanID: span.context.SpanID,
Timestamp: uint64(span.Start.UnixNano()) / uint64(time.Millisecond),
Duration: uint64(span.Duration) / uint64(time.Millisecond),
Name: string(data.Type()),
Ec: span.ErrorCount,
ForeignParent: newForeignParent(span.context.ForeignParent),
CorrelationType: span.Correlation.Type,
CorrelationID: span.Correlation.ID,
Kind: int(data.Kind()),
Data: data,
}

if bs, ok := span.Tags[batchSizeTag].(int); ok {
Expand Down
101 changes: 98 additions & 3 deletions propagation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package instana

import (
"errors"
"net/http"
"strings"

Expand Down Expand Up @@ -113,7 +114,16 @@ func extractTraceContext(opaqueCarrier interface{}) (SpanContext, error) {
case FieldS:
spanID = v
case FieldL:
spanContext.Suppressed = parseLevel(v)
suppressed, corrData, err := parseLevel(v)
if err != nil {
sensor.logger.Info("failed to parse %s: %s %q", k, err, v)
// use defaults
suppressed, corrData = false, EUMCorrelationData{}
}
spanContext.Suppressed = suppressed
if !spanContext.Suppressed {
spanContext.Correlation = corrData
}
default:
if strings.HasPrefix(strings.ToLower(k), FieldB) {
// preserve original case of the baggage key
Expand All @@ -127,6 +137,11 @@ func extractTraceContext(opaqueCarrier interface{}) (SpanContext, error) {
return spanContext, err
}

// For compatibility reasons X-Instana-{T,S} values if EUM has provided correlation ID
if spanContext.Correlation.ID != "" {
return spanContext, nil
}

if traceID == "" && spanID == "" {
if spanContext.W3CContext.IsZero() {
return spanContext, ot.ErrSpanContextNotFound
Expand Down Expand Up @@ -197,8 +212,88 @@ func addEUMHeaders(h http.Header, sc SpanContext) {
h.Set("Server-Timing", strings.Join(st, ", "))
}

func parseLevel(s string) bool {
return s == "0"
var errMalformedHeader = errors.New("malformed header value")

func parseLevel(s string) (bool, EUMCorrelationData, error) {
const (
levelState uint8 = iota
partSeparatorState
correlationPartState
correlationTypeState
correlationIDState
finalState
)

if s == "" {
return false, EUMCorrelationData{}, nil
}

var (
typeInd int
state uint8
level, corrType, corrID string
)
PARSE:
for ptr := 0; state != finalState && ptr < len(s); ptr++ {
switch state {
case levelState: // looking for 0 or 1
level = s[ptr : ptr+1]

if level != "0" && level != "1" {
break PARSE
}

if ptr == len(s)-1 { // no correlation ID provided
state = finalState
} else {
state = partSeparatorState
}
case partSeparatorState: // skip OWS while looking for ','
switch s[ptr] {
case ' ', '\t': // advance
case ',':
state = correlationPartState
default:
break PARSE
}
case correlationPartState: // skip OWS while searching for 'correlationType=' prefix
switch {
case s[ptr] == ' ' || s[ptr] == '\t': // advance
case strings.HasPrefix(s[ptr:], "correlationType="):
ptr += 15 // advance to the end of prefix
typeInd = ptr + 1
state = correlationTypeState
default:
break PARSE
}
case correlationTypeState: // skip OWS while looking for ';'
switch s[ptr] {
case ' ', '\t': // possibly trailing OWS, advance
case ';':
state = correlationIDState
default:
corrType = s[typeInd : ptr+1]
}
case correlationIDState: // skip OWS while searching for 'correlationId=' prefix
switch {
case s[ptr] == ' ' || s[ptr] == '\t': // leading OWS, advance
case strings.HasPrefix(s[ptr:], "correlationId="):
ptr += 14
corrID = s[ptr:]
state = finalState
default:
break PARSE
}
default:
break PARSE
}
}

if state != finalState {
return false, EUMCorrelationData{}, errMalformedHeader
}

return level == "0", EUMCorrelationData{Type: corrType, ID: corrID}, nil
}

func formatLevel(sc SpanContext) string {
Expand Down
49 changes: 49 additions & 0 deletions propagation_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package instana

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseLevel(t *testing.T) {
examples := map[string]struct {
Value string
ExpectedSuppressed bool
ExpectedCorrelation EUMCorrelationData
}{
"empty header": {},
"level=0, no correlation id": {
Value: "0",
ExpectedSuppressed: true,
},
"level=1, no correlation id": {
Value: "1",
},
"level=0, with correlation id": {
Value: "0 , correlationType=web ;\t correlationId=Test Value",
ExpectedSuppressed: true,
ExpectedCorrelation: EUMCorrelationData{
Type: "web",
ID: "Test Value",
},
},
"level=1, with correlation id": {
Value: "1,correlationType=mobile;correlationId=Test Value",
ExpectedCorrelation: EUMCorrelationData{
Type: "mobile",
ID: "Test Value",
},
},
}

for name, example := range examples {
t.Run(name, func(t *testing.T) {
suppressed, corrData, err := parseLevel(example.Value)
require.NoError(t, err)
assert.Equal(t, example.ExpectedSuppressed, suppressed)
assert.Equal(t, example.ExpectedCorrelation, corrData)
})
}
}
56 changes: 56 additions & 0 deletions propagation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,20 @@ func TestTracer_Extract_HTTPHeaders(t *testing.T) {
Baggage: map[string]string{},
},
},
"tracing disabled, with correlation data": {
Headers: map[string]string{
"Authorization": "Basic 123",
"x-instana-t": "1314",
"X-INSTANA-S": "2435",
"X-Instana-L": "0,correlationType=web;correlationId=1234",
},
Expected: instana.SpanContext{
TraceID: 0x1314,
SpanID: 0x2435,
Suppressed: true,
Baggage: map[string]string{},
},
},
"w3c trace context, last vendor is instana": {
Headers: map[string]string{
"x-instana-t": "1314",
Expand Down Expand Up @@ -370,6 +384,48 @@ func TestTracer_Extract_HTTPHeaders(t *testing.T) {
}
}

func TestTracer_Extract_HTTPHeaders_WithEUMCorrelation(t *testing.T) {
examples := map[string]struct {
Headers map[string]string
Expected instana.SpanContext
}{
"tracing enabled, no instana headers": {
Headers: map[string]string{
"X-Instana-L": "1,correlationType=web;correlationId=1234",
},
},
"tracing enabled, with instana headers": {
Headers: map[string]string{
"X-Instana-T": "0000000000002435",
"X-Instana-S": "0000000000003546",
"X-Instana-L": "1,correlationType=web;correlationId=1234",
},
},
}

for name, example := range examples {
t.Run(name, func(t *testing.T) {
recorder := instana.NewTestRecorder()
tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder)

headers := http.Header{}
for k, v := range example.Headers {
headers.Set(k, v)
}

sc, err := tracer.Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(headers))
require.NoError(t, err)

spanContext := sc.(instana.SpanContext)

assert.EqualValues(t, 0, spanContext.TraceID)
assert.EqualValues(t, 0, spanContext.SpanID)
assert.Empty(t, spanContext.ParentID)
assert.Equal(t, instana.EUMCorrelationData{ID: "1234", Type: "web"}, spanContext.Correlation)
})
}
}

func TestTracer_Extract_HTTPHeaders_NoContext(t *testing.T) {
examples := map[string]struct {
Headers map[string]string
Expand Down
15 changes: 8 additions & 7 deletions span.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (
)

type spanS struct {
Service string
Operation string
Start time.Time
Duration time.Duration
Tags ot.Tags
Logs []ot.LogRecord
ErrorCount int
Service string
Operation string
Start time.Time
Duration time.Duration
Correlation EUMCorrelationData
Tags ot.Tags
Logs []ot.LogRecord
ErrorCount int

tracer *tracerS
mu sync.Mutex
Expand Down
9 changes: 9 additions & 0 deletions span_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import (
"github.com/instana/go-sensor/w3ctrace"
)

// EUMCorrelationData represents the data sent by the Instana End-User Monitoring script
// integrated into frontend
type EUMCorrelationData struct {
Type string
ID string
}

// SpanContext holds the basic Span metadata.
type SpanContext struct {
// A probabilistically unique identifier for a [multi-span] trace.
Expand All @@ -22,6 +29,8 @@ type SpanContext struct {
Baggage map[string]string // initialized on first use
// The W3C trace context
W3CContext w3ctrace.Context
// Correlation is the correlation data sent by the frontend EUM script
Correlation EUMCorrelationData
// The 3rd party parent if the context is derived from non-Instana trace
ForeignParent interface{}
}
Expand Down
Loading

0 comments on commit 874e706

Please sign in to comment.