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

feat(propagator): create a new one-way propagator #343

Merged
merged 3 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
51 changes: 51 additions & 0 deletions propagator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# OpenTelemetry Google Cloud Trace Propagators

[![Docs](https://godoc.org/github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator?status.svg)](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator)

This package contains Trace Context Propagators for use with [Google Cloud
Trace](https://cloud.google.com/trace) that make it compatible with
[OpenTelemetry](http://opentelemetry.io).

There are two available propagators in this package:

### `CloudTraceOneWayPropagator`

The `CloudTraceOneWayPropagator` reads the `X-Cloud-Trace-Context` header for
trace and span IDs, and writes this data into the `traceparent` header.

dashpole marked this conversation as resolved.
Show resolved Hide resolved
### `CloudTraceFormatPropagator`

The `CloudTraceFormatPropagator` reads and writes the `X-Cloud-Trace-Context` header only.

## Differences between Google Cloud Trace and W3C Trace Context

Google Cloud Trace encodes trace information in the `X-Cloud-Trace-Context` HTTP
header, using the format described in the [Trace documentation](https://cloud.google.com/trace/docs/setup#force-trace).

OpenTelemetry uses the newer, W3C standard
[`traceparent` header](https://www.w3.org/TR/trace-context/#traceparent-header)

There is an important semantic difference between Cloud Trace's
`TRACE_TRUE` flag, and W3C's `sampled` flag.

As outlined in the [Trace
documentation](https://cloud.google.com/trace/docs/setup#force-trace), setting
the `TRACE_TRUE` flag will cause trace information to be collected.

This differs from the W3C behavior, where the [`sampled`
flag](https://www.w3.org/TR/trace-context/#sampled-flag) indicates that the
caller *may* have recorded trace information, but does not necessarily impact
the sampling done by other services.

To preserve the Cloud-Trace behavior when using `traceparent`, you can use the
[`ParentBased`
sampler](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/trace#ParentBased) like
so:

```go
import sdktrace go.opentelemetry.io/otel/sdk/trace
sampler := sdktrace.ParentBased(
sdktrace.NeverSample(),
sdktrace.WithRemoteParentSampled(sdktrace.AlwaysSample()))
)
```
44 changes: 37 additions & 7 deletions propagator/propagator.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ const traceContextHeaderFormat = "^(?P<trace_id>[0-9a-f]{32})/(?P<span_id>[0-9]{
// traceContextHeaderRe is a regular expression object of TraceContextHeaderFormat.
var traceContextHeaderRe = regexp.MustCompile(traceContextHeaderFormat)

// traceContextHeaders is the list of headers that are propagated. Cloud Trace only requires
// cloudTraceContextHeaders is the list of headers that are propagated. Cloud Trace only requires
// one element in the list.
var traceContextHeaders = []string{TraceContextHeaderName}
var cloudTraceContextHeaders = []string{TraceContextHeaderName}

// TraceparentHeaderName is the HTTP header field for w3c standard trace information
// https://www.w3.org/TR/trace-context/
const TraceparentHeaderName = "traceparent"
muncus marked this conversation as resolved.
Show resolved Hide resolved

type errInvalidHeader struct {
header string
Expand All @@ -50,6 +54,32 @@ func (e errInvalidHeader) Error() string {
return fmt.Sprintf("invalid header %s", e.header)
}

// CloudTraceOneWayPropagator will extract Trace IDs from the
// x-cloud-trace-context header, and inject the w3c-standard 'traceparent'
// header.
type CloudTraceOneWayPropagator struct {
propagation.TraceContext
muncus marked this conversation as resolved.
Show resolved Hide resolved
}

func (p CloudTraceOneWayPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
header := carrier.Get(TraceContextHeaderName)
if header == "" {
return ctx
}
sc, err := spanContextFromXCTCHeader(header)
if err != nil {
log.Printf("CloudTraceFormatPropagator: %v", err)
muncus marked this conversation as resolved.
Show resolved Hide resolved
return ctx
}
return trace.ContextWithRemoteSpanContext(ctx, sc)
}

func (p CloudTraceOneWayPropagator) Fields() []string {
return []string{TraceContextHeaderName, TraceparentHeaderName}
muncus marked this conversation as resolved.
Show resolved Hide resolved
}

var _ propagation.TextMapPropagator = CloudTraceOneWayPropagator{}

// CloudTraceFormatPropagator is a TextMapPropagator that injects/extracts a context to/from the carrier
// following Google Cloud Trace format.
type CloudTraceFormatPropagator struct{}
Expand Down Expand Up @@ -87,8 +117,8 @@ func (p CloudTraceFormatPropagator) Inject(ctx context.Context, carrier propagat
carrier.Set(TraceContextHeaderName, header)
}

// spanContextFromHeader creates trace.SpanContext from XCTC header value.
func (p CloudTraceFormatPropagator) spanContextFromHeader(header string) (trace.SpanContext, error) {
// spanContextFromXCTCHeader creates trace.SpanContext from XCTC header value.
func spanContextFromXCTCHeader(header string) (trace.SpanContext, error) {
match := traceContextHeaderRe.FindStringSubmatch(header)
if match == nil {
return trace.SpanContext{}, errInvalidHeader{header}
Expand Down Expand Up @@ -148,7 +178,7 @@ func (p CloudTraceFormatPropagator) spanContextFromHeader(header string) (trace.
// In this method, SpanID is expected to be stored in big endian.
func (p CloudTraceFormatPropagator) SpanContextFromRequest(req *http.Request) (trace.SpanContext, error) {
h := req.Header.Get(TraceContextHeaderName)
return p.spanContextFromHeader(h)
return spanContextFromXCTCHeader(h)
}

// Extract extacts a context from the carrier if the header contains Google Cloud Trace header format.
Expand All @@ -158,7 +188,7 @@ func (p CloudTraceFormatPropagator) Extract(ctx context.Context, carrier propaga
if header == "" {
return ctx
}
sc, err := p.spanContextFromHeader(header)
sc, err := spanContextFromXCTCHeader(header)
if err != nil {
log.Printf("CloudTraceFormatPropagator: %v", err)
return ctx
Expand All @@ -168,7 +198,7 @@ func (p CloudTraceFormatPropagator) Extract(ctx context.Context, carrier propaga

// Fields just returns the header name.
func (p CloudTraceFormatPropagator) Fields() []string {
return traceContextHeaders
return cloudTraceContextHeaders
}

// Confirming if CloudTraceFormatPropagator satisifies the TextMapPropagator interface.
Expand Down
108 changes: 108 additions & 0 deletions propagator/propagator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,114 @@ func TestValidTraceContextHeaderFormats(t *testing.T) {
}
}

func TestOneWayPropagatorExtract(t *testing.T) {
testCases := []struct {
key string
traceID string
spanID string
flagPart string
}{
{
"X-Cloud-Trace-Context",
validTraceIDStr,
validSpanIDStr,
"1",
},
{
"X-Cloud-Trace-Context",
validTraceIDStr,
validSpanIDStr,
"0",
},
{
"x-cloud-trace-context",
validTraceIDStr,
validSpanIDStr,
"0",
},
}

for _, c := range testCases {
req := httptest.NewRequest("GET", "http://example.com", nil)
value := fmt.Sprintf("%s/%s;o=%s", c.traceID, c.spanID, c.flagPart)
req.Header.Set(c.key, value)

ctx := context.Background()
propagator := CloudTraceOneWayPropagator{}
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header))

sc := trace.SpanContextFromContext(ctx)

sid, err := strconv.ParseUint(sc.SpanID().String(), 16, 64)
if err != nil {
t.Errorf("SpanID can't be convert to uint64: %v", err)
}
sidStr := fmt.Sprintf("%d", sid)

flag := fmt.Sprintf("%d", sc.TraceFlags())

if sc.TraceID().String() != c.traceID {
t.Errorf("TraceID unmatch: expected %v, but got %v", c.traceID, sc.TraceID())
}
if sidStr != c.spanID {
t.Errorf("SpanID unmatch: expected %v, but got %v", c.spanID, sidStr)
}
if flag != c.flagPart {
t.Errorf("FlagPart unmatch: expected %v, but got %v", c.flagPart, flag)
}
}
}

func TestOneWayPropagatorInject(t *testing.T) {
propagator := CloudTraceOneWayPropagator{}

testCases := []struct {
name string
scc trace.SpanContextConfig
wantHeaders map[string]string
}{
{
"valid TraceID and SpanID with sampled flag",
trace.SpanContextConfig{
TraceID: validTraceID,
SpanID: validSpanID,
TraceFlags: trace.FlagsSampled,
},
map[string]string{
TraceparentHeaderName: fmt.Sprintf("00-%s-%s-01", validTraceID.String(), validSpanID.String()),
muncus marked this conversation as resolved.
Show resolved Hide resolved
},
},
{
"valid TraceID and SpanID without sampled flag",
trace.SpanContextConfig{
TraceID: validTraceID,
SpanID: validSpanID,
},
map[string]string{
TraceparentHeaderName: fmt.Sprintf("00-%s-%s-00", validTraceID.String(), validSpanID.String()),
muncus marked this conversation as resolved.
Show resolved Hide resolved
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
header := http.Header{}
ctx := trace.ContextWithSpanContext(
context.Background(),
trace.NewSpanContext(tc.scc),
)
propagator.Inject(ctx, propagation.HeaderCarrier(header))

for h, v := range tc.wantHeaders {
result, want := header.Get(h), v
if diff := cmp.Diff(want, result); diff != "" {
t.Errorf("%v, header: %s diff: %s", tc.name, h, diff)
}
}
})
}
}

func TestCloudTraceContextHeaderExtract(t *testing.T) {
testCases := []struct {
key string
Expand Down