forked from open-telemetry/opentelemetry-go-contrib
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add AWS X-Ray propagator and tests * made naming convention changes to adhere to linter * added comment to interface implementation for clarity * update naming convention and added description for method
- Loading branch information
Showing
3 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package aws | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"strings" | ||
|
||
"go.opentelemetry.io/otel" | ||
"go.opentelemetry.io/otel/api/trace" | ||
) | ||
|
||
const ( | ||
traceHeaderKey = "X-Amzn-Trace-Id" | ||
traceHeaderDelimiter = ";" | ||
kvDelimiter = "=" | ||
traceIDKey = "Root" | ||
sampleFlagKey = "Sampled" | ||
parentIDKey = "Parent" | ||
traceIDVersion = "1" | ||
traceIDDelimiter = "-" | ||
isSampled = "1" | ||
notSampled = "0" | ||
|
||
traceFlagNone = 0x0 | ||
traceFlagSampled = 0x1 << 0 | ||
traceIDLength = 35 | ||
traceIDDelimitterIndex1 = 1 | ||
traceIDDelimitterIndex2 = 10 | ||
traceIDFirstPartLength = 8 | ||
sampledFlagLength = 1 | ||
) | ||
|
||
var ( | ||
empty = trace.EmptySpanContext() | ||
errInvalidTraceHeader = errors.New("invalid X-Amzn-Trace-Id header value, should contain 3 different part separated by ;") | ||
errMalformedTraceID = errors.New("cannot decode trace id from header, should be a string of hex, lowercase trace id can't be all zero") | ||
errInvalidSpanIDLength = errors.New("invalid span id length, must be 16") | ||
) | ||
|
||
// AWS X-Ray propagator serializes Span Context to/from AWS X-Ray headers | ||
// | ||
// AWS X-Ray format | ||
// | ||
// X-Amzn-Trace-Id: Root={traceId};Parent={parentId};Sampled={samplingFlag} | ||
type Xray struct{} | ||
|
||
// Asserts that the propagator implements the otel.textMapPropagator interface | ||
var _ otel.TextMapPropagator = &Xray{} | ||
|
||
// Inject injects a context to the carrier following AWS X-Ray format. | ||
func (awsxray Xray) Inject(ctx context.Context, carrier otel.TextMapCarrier) { | ||
sc := trace.SpanFromContext(ctx).SpanContext() | ||
headers := []string{} | ||
if !sc.TraceID.IsValid() || !sc.SpanID.IsValid() { | ||
return | ||
} | ||
otTraceID := sc.TraceID.String() | ||
xrayTraceID := traceIDVersion + traceIDDelimiter + otTraceID[0:traceIDFirstPartLength] + | ||
traceIDDelimiter + otTraceID[traceIDFirstPartLength:] | ||
parentID := sc.SpanID | ||
samplingFlag := notSampled | ||
if sc.TraceFlags == traceFlagSampled { | ||
samplingFlag = isSampled | ||
} | ||
|
||
headers = append(headers, traceIDKey, kvDelimiter, xrayTraceID, traceHeaderDelimiter, parentIDKey, | ||
kvDelimiter, parentID.String(), traceHeaderDelimiter, sampleFlagKey, kvDelimiter, samplingFlag) | ||
|
||
carrier.Set(traceHeaderKey, strings.Join(headers, "")) | ||
} | ||
|
||
// Extract gets a context from the carrier if it contains AWS X-Ray headers. | ||
func (awsxray Xray) Extract(ctx context.Context, carrier otel.TextMapCarrier) context.Context { | ||
// extract tracing information | ||
if header := carrier.Get(traceHeaderKey); header != "" { | ||
sc, err := extract(header) | ||
if err == nil && sc.IsValid() { | ||
return trace.ContextWithRemoteSpanContext(ctx, sc) | ||
} | ||
} | ||
return ctx | ||
} | ||
|
||
//extracts Span Context from context | ||
func extract(headerVal string) (trace.SpanContext, error) { | ||
var ( | ||
sc = trace.SpanContext{} | ||
err error | ||
delimiterIndex int | ||
part string | ||
) | ||
pos := 0 | ||
for pos < len(headerVal) { | ||
delimiterIndex = indexOf(headerVal, traceHeaderDelimiter, pos) | ||
if delimiterIndex >= 0 { | ||
part = headerVal[pos:delimiterIndex] | ||
pos = delimiterIndex + 1 | ||
} else { | ||
//last part | ||
part = strings.TrimSpace(headerVal[pos:]) | ||
pos = len(headerVal) | ||
} | ||
equalsIndex := strings.Index(part, kvDelimiter) | ||
if equalsIndex < 0 { | ||
return empty, errInvalidTraceHeader | ||
} | ||
value := part[equalsIndex+1:] | ||
if strings.HasPrefix(part, traceIDKey) { | ||
sc.TraceID, err = parseTraceID(value) | ||
if err != nil { | ||
return empty, errMalformedTraceID | ||
} | ||
} else if strings.HasPrefix(part, parentIDKey) { | ||
//extract parentId | ||
sc.SpanID, err = trace.SpanIDFromHex(value) | ||
if err != nil { | ||
return empty, errInvalidSpanIDLength | ||
} | ||
} else if strings.HasPrefix(part, sampleFlagKey) { | ||
//extract traceflag | ||
sc.TraceFlags = parseTraceFlag(value) | ||
} | ||
} | ||
return sc, nil | ||
} | ||
|
||
//returns position of the first occurrence of a substring starting at pos index | ||
func indexOf(str string, substr string, pos int) int { | ||
index := strings.Index(str[pos:], substr) | ||
if index > -1 { | ||
index += pos | ||
} | ||
return index | ||
} | ||
|
||
//returns trace Id if valid else return invalid trace Id | ||
func parseTraceID(xrayTraceID string) (trace.ID, error) { | ||
if len(xrayTraceID) != traceIDLength { | ||
return empty.TraceID, errMalformedTraceID | ||
} | ||
if !strings.HasPrefix(xrayTraceID, traceIDVersion) { | ||
return empty.TraceID, errMalformedTraceID | ||
} | ||
|
||
if xrayTraceID[traceIDDelimitterIndex1:traceIDDelimitterIndex1+1] != traceIDDelimiter || | ||
xrayTraceID[traceIDDelimitterIndex2:traceIDDelimitterIndex2+1] != traceIDDelimiter { | ||
return empty.TraceID, errMalformedTraceID | ||
} | ||
|
||
epochPart := xrayTraceID[traceIDDelimitterIndex1+1 : traceIDDelimitterIndex2] | ||
uniquePart := xrayTraceID[traceIDDelimitterIndex2+1 : traceIDLength] | ||
|
||
result := epochPart + uniquePart | ||
return trace.IDFromHex(result) | ||
} | ||
|
||
//returns traceFlag | ||
func parseTraceFlag(xraySampledFlag string) byte { | ||
if len(xraySampledFlag) == sampledFlagLength && xraySampledFlag != isSampled { | ||
return traceFlagNone | ||
} | ||
return trace.FlagsSampled | ||
} | ||
|
||
func (awsxray Xray) Fields() []string { | ||
return []string{traceHeaderKey} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package aws | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"go.opentelemetry.io/otel/api/trace" | ||
) | ||
|
||
var ( | ||
traceID = trace.ID{0x8a, 0x3c, 0x60, 0xf7, 0xd1, 0x88, 0xf8, 0xfa, 0x79, 0xd4, 0x8a, 0x39, 0x1a, 0x77, 0x8f, 0xa6} | ||
xrayTraceID = "1-8a3c60f7-d188f8fa79d48a391a778fa6" | ||
parentID64Str = "53995c3f42cd8ad8" | ||
parentSpanID = trace.SpanID{0x53, 0x99, 0x5c, 0x3f, 0x42, 0xcd, 0x8a, 0xd8} | ||
zeroSpanIDStr = "0000000000000000" | ||
zeroTraceIDStr = "1-00000000-000000000000000000000000" | ||
invalidTraceHeaderID = "1b00000000b000000000000000000000000" | ||
wrongVersionTraceHeaderID = "5b00000000b000000000000000000000000" | ||
) | ||
|
||
func TestAwsXrayExtract(t *testing.T) { | ||
testData := []struct { | ||
traceID string | ||
parentSpanID string | ||
samplingFlag string | ||
expected trace.SpanContext | ||
err error | ||
}{ | ||
{ | ||
xrayTraceID, parentID64Str, notSampled, | ||
trace.SpanContext{ | ||
TraceID: traceID, | ||
SpanID: parentSpanID, | ||
TraceFlags: traceFlagNone, | ||
}, | ||
nil, | ||
}, | ||
{ | ||
xrayTraceID, parentID64Str, isSampled, | ||
trace.SpanContext{ | ||
TraceID: traceID, | ||
SpanID: parentSpanID, | ||
TraceFlags: traceFlagSampled, | ||
}, | ||
nil, | ||
}, | ||
{ | ||
zeroTraceIDStr, parentID64Str, isSampled, | ||
trace.SpanContext{}, | ||
errMalformedTraceID, | ||
}, | ||
{ | ||
xrayTraceID, zeroSpanIDStr, isSampled, | ||
trace.SpanContext{}, | ||
errInvalidSpanIDLength, | ||
}, | ||
{ | ||
invalidTraceHeaderID, parentID64Str, isSampled, | ||
trace.SpanContext{}, | ||
errMalformedTraceID, | ||
}, | ||
{ | ||
wrongVersionTraceHeaderID, parentID64Str, isSampled, | ||
trace.SpanContext{}, | ||
errMalformedTraceID, | ||
}, | ||
} | ||
|
||
for _, test := range testData { | ||
headerVal := strings.Join([]string{traceIDKey, kvDelimiter, test.traceID, traceHeaderDelimiter, parentIDKey, kvDelimiter, | ||
test.parentSpanID, traceHeaderDelimiter, sampleFlagKey, kvDelimiter, test.samplingFlag}, "") | ||
|
||
sc, err := extract(headerVal) | ||
|
||
info := []interface{}{ | ||
"trace ID: %q, parent span ID: %q, sampling flag: %q", | ||
test.traceID, | ||
test.parentSpanID, | ||
test.samplingFlag, | ||
} | ||
|
||
if !assert.Equal(t, test.err, err, info...) { | ||
continue | ||
} | ||
|
||
assert.Equal(t, test.expected, sc, info...) | ||
} | ||
} |