Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support EUM script correlation #138

Merged
merged 6 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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