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

stats/opentelemetry: introduce tracing propagator and carrier #7677

Merged
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d0a0b51
stats: opentelemetry GrpcTraceBinPropagator
purnesh42H Sep 1, 2024
edf1604
Address style, clarification review comments
purnesh42H Oct 6, 2024
5709a43
Make *CustomCarrier implement carrier interface
purnesh42H Oct 9, 2024
c97cbb5
Address 2nd round of style, docstring comments
purnesh42H Oct 9, 2024
4144464
use cmp.equal with cmpopts.SortSlices for equating keys slices
purnesh42H Oct 13, 2024
4f49b4f
separate fast path and slow path tests
purnesh42H Oct 16, 2024
96abe67
Addressing documentation comments
purnesh42H Oct 18, 2024
4839c45
rewrite FromBinary as C core and add unit tests
purnesh42H Oct 20, 2024
ce6cbd4
handle grpc-trace-bin keys for regular get and set
purnesh42H Oct 20, 2024
59b87e7
Address nits
purnesh42H Oct 22, 2024
26ee1b0
don't allow any other binary header except grpc-trace-bin
purnesh42H Oct 24, 2024
c5afe54
Suffix -bin instead of bin
purnesh42H Oct 25, 2024
309dbf6
address testing comments of merging in t-test and updating top level …
purnesh42H Oct 26, 2024
760330c
error and naming suggestions for tests
purnesh42H Nov 5, 2024
38cde19
handle success bool and err separately in tests
purnesh42H Nov 5, 2024
309386a
first pass from doug
purnesh42H Nov 6, 2024
23c9a58
Move grpcTraceBinPropagator under opentelemtry package
purnesh42H Nov 6, 2024
3c8389b
update mod.go for otel/trace
purnesh42H Nov 6, 2024
062e769
Changed context propagation to deal binary directly using metadata an…
purnesh42H Nov 12, 2024
ee2869c
remove stats.Trace usage
purnesh42H Nov 12, 2024
6756247
Switch back to context in carrier
purnesh42H Nov 15, 2024
de4ba6c
unexport grpc-trace-bin header const
purnesh42H Nov 15, 2024
04ee6cb
address nits
purnesh42H Nov 15, 2024
86d29bb
separate incoming/outgoing for Keys()
purnesh42H Nov 15, 2024
21a13be
rename to Carrier with NewIncomingCarrier and NewOutgoingCarrier cons…
purnesh42H Nov 16, 2024
c180834
fix incoming/outgoing carrier usage in propagator test
purnesh42H Nov 19, 2024
2540965
use incoming context for incoming carrier and outgoing context for ou…
purnesh42H Nov 20, 2024
4f77441
revert back recreating incoming and outgoing contexts
purnesh42H Nov 20, 2024
d708a8b
Separate incoming and outgoing carrier
purnesh42H Nov 20, 2024
a3c8693
address grpc-trace-bin propagator test comments
purnesh42H Nov 21, 2024
268b0a3
remove keys test from grpc trace bin propagator
purnesh42H Nov 22, 2024
5c7b69a
split invalid and valid injection tests
purnesh42H Nov 23, 2024
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
go.opentelemetry.io/otel/metric v1.31.0
go.opentelemetry.io/otel/sdk v1.31.0
go.opentelemetry.io/otel/sdk/metric v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0
Expand All @@ -32,7 +33,6 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
)
119 changes: 119 additions & 0 deletions stats/opentelemetry/grpc_trace_bin_propagator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
*
* Copyright 2024 gRPC 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 opentelemetry

import (
"context"

otelpropagation "go.opentelemetry.io/otel/propagation"
oteltrace "go.opentelemetry.io/otel/trace"
)

// gRPCTraceBinHeaderKey is the gRPC metadata header key `grpc-trace-bin` used
// to propagate trace context in binary format.
const grpcTraceBinHeaderKey = "grpc-trace-bin"

// GRPCTraceBinPropagator is an OpenTelemetry TextMapPropagator which is used
// to extract and inject trace context data from and into headers exchanged by
// gRPC applications. It propagates trace data in binary format using the
// `grpc-trace-bin` header.
type GRPCTraceBinPropagator struct{}

// Inject sets OpenTelemetry span context from the Context into the carrier as
// a `grpc-trace-bin` header if span context is valid.
//
// If span context is not valid, it returns without setting `grpc-trace-bin`
// header.
func (GRPCTraceBinPropagator) Inject(ctx context.Context, carrier otelpropagation.TextMapCarrier) {
sc := oteltrace.SpanFromContext(ctx)
if !sc.SpanContext().IsValid() {
return
}

bd := toBinary(sc.SpanContext())
carrier.Set(grpcTraceBinHeaderKey, string(bd))
purnesh42H marked this conversation as resolved.
Show resolved Hide resolved
}

// Extract reads OpenTelemetry span context from the `grpc-trace-bin` header of
// carrier into the provided context, if present.
//
// If a valid span context is retrieved from `grpc-trace-bin`, it returns a new
// context containing the extracted OpenTelemetry span context marked as
// remote.
//
// If `grpc-trace-bin` header is not present, it returns the context as is.
func (GRPCTraceBinPropagator) Extract(ctx context.Context, carrier otelpropagation.TextMapCarrier) context.Context {
h := carrier.Get(grpcTraceBinHeaderKey)
if h == "" {
return ctx
}

sc, ok := fromBinary([]byte(h))
if !ok {
return ctx
}

Check warning on line 70 in stats/opentelemetry/grpc_trace_bin_propagator.go

View check run for this annotation

Codecov / codecov/patch

stats/opentelemetry/grpc_trace_bin_propagator.go#L69-L70

Added lines #L69 - L70 were not covered by tests
return oteltrace.ContextWithRemoteSpanContext(ctx, sc)
}

// Fields returns the keys whose values are set with Inject.
//
// GRPCTraceBinPropagator always returns a slice containing only
// `grpc-trace-bin` key because it only sets the `grpc-trace-bin` header for
// propagating trace context.
func (GRPCTraceBinPropagator) Fields() []string {
return []string{grpcTraceBinHeaderKey}

Check warning on line 80 in stats/opentelemetry/grpc_trace_bin_propagator.go

View check run for this annotation

Codecov / codecov/patch

stats/opentelemetry/grpc_trace_bin_propagator.go#L79-L80

Added lines #L79 - L80 were not covered by tests
}

// toBinary returns the binary format representation of a SpanContext.
//
// If sc is the zero value, returns nil.
func toBinary(sc oteltrace.SpanContext) []byte {
if sc.Equal(oteltrace.SpanContext{}) {
return nil
}
var b [29]byte
traceID := oteltrace.TraceID(sc.TraceID())
copy(b[2:18], traceID[:])
b[18] = 1
spanID := oteltrace.SpanID(sc.SpanID())
copy(b[19:27], spanID[:])
b[27] = 2
b[28] = byte(oteltrace.TraceFlags(sc.TraceFlags()))
return b[:]
}

// fromBinary returns the SpanContext represented by b with Remote set to true.
//
// It returns with zero value SpanContext and false, if any of the
// below condition is not satisfied:
// - Valid header: len(b) = 29
// - Valid version: b[0] = 0
// - Valid traceID prefixed with 0: b[1] = 0
// - Valid spanID prefixed with 1: b[18] = 1
// - Valid traceFlags prefixed with 2: b[27] = 2
func fromBinary(b []byte) (oteltrace.SpanContext, bool) {
if len(b) != 29 || b[0] != 0 || b[1] != 0 || b[18] != 1 || b[27] != 2 {
return oteltrace.SpanContext{}, false
}

return oteltrace.SpanContext{}.WithTraceID(
oteltrace.TraceID(b[2:18])).WithSpanID(
oteltrace.SpanID(b[19:27])).WithTraceFlags(
oteltrace.TraceFlags(b[28])).WithRemote(true), true
}
253 changes: 253 additions & 0 deletions stats/opentelemetry/grpc_trace_bin_propagator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/*
*
* Copyright 2024 gRPC 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 opentelemetry

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
oteltrace "go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/metadata"
itracing "google.golang.org/grpc/stats/opentelemetry/internal/tracing"
)

var validSpanContext = oteltrace.SpanContext{}.WithTraceID(
oteltrace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}).WithSpanID(
oteltrace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}).WithTraceFlags(
oteltrace.TraceFlags(1))

// TestInject verifies that the GRPCTraceBinPropagator correctly injects
// existing binary trace data or OpenTelemetry span context as `grpc-trace-bin`
// header in the provided carrier's context metadata.
//
// It verifies that if a valid span context is injected, same span context can
// can be retreived from the carrier's context metadata.
//
// If an invalid span context is injected, it verifies that `grpc-trace-bin`
// header is not set in the carrier's context metadata.
func (s) TestInject(t *testing.T) {
tests := []struct {
name string
injectSC oteltrace.SpanContext
incomingMd metadata.MD
outgoingMd metadata.MD
wantKeys []string
}{
{
name: "valid OpenTelemetry span context",
injectSC: validSpanContext,
incomingMd: metadata.MD{"incoming-key": []string{"incoming-value"}},
outgoingMd: metadata.MD{"outgoing-key": []string{"outgoing-value"}},
dfawley marked this conversation as resolved.
Show resolved Hide resolved
wantKeys: []string{grpcTraceBinHeaderKey, "outgoing-key"},
},
{
name: "invalid OpenTelemetry span context",
injectSC: oteltrace.SpanContext{},
incomingMd: metadata.MD{"incoming-key": []string{"incoming-value"}},
outgoingMd: metadata.MD{"outgoing-key": []string{"outgoing-value"}},
wantKeys: []string{"outgoing-key"},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := GRPCTraceBinPropagator{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = oteltrace.ContextWithSpanContext(ctx, test.injectSC)
ctx = metadata.NewIncomingContext(ctx, test.incomingMd)
ctx = metadata.NewOutgoingContext(ctx, test.outgoingMd)

c := itracing.NewOutgoingCarrier(ctx)
p.Inject(ctx, c)

if gotKeys := c.Keys(); !cmp.Equal(test.wantKeys, gotKeys, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
t.Errorf("c.Keys() = keys %v, want %v", gotKeys, test.wantKeys)
}
md, _ := metadata.FromOutgoingContext(c.Context())
gotH := md.Get(grpcTraceBinHeaderKey)
if !test.injectSC.IsValid() {
if len(gotH) > 0 {
t.Fatalf("got %v value from Carrier's context metadata grpc-trace-bin header, want empty", gotH)
}
return
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When your test cases diverge in behavior so much, it really just feels like two different tests instead of one table driven test. Optionally rewrite that way to make it easier to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote separate tests for valid and invalid span context injection

if gotH[len(gotH)-1] == "" {
t.Fatalf("got empty value from Carrier's context metadata grpc-trace-bin header, want valid span context: %v", test.injectSC)
}
gotSC, ok := fromBinary([]byte(gotH[len(gotH)-1]))
if !ok {
t.Fatalf("got invalid span context %v from Carrier's context metadata grpc-trace-bin header, want valid span context: %v", gotSC, test.injectSC)
}
if test.injectSC.TraceID() != gotSC.TraceID() && test.injectSC.SpanID() != gotSC.SpanID() && test.injectSC.TraceFlags() != gotSC.TraceFlags() {
t.Fatalf("got span context = %v, want span contexts %v", gotSC, test.injectSC)
}
})
}
}

// TestExtract verifies that the GRPCTraceBinPropagator correctly extracts
// OpenTelemetry span context data from the provided context using carrier.
//
// If a valid span context was injected, it verifies same trace span context
// is extracted from carrier's metadata for `grpc-trace-bin` header key.
//
// If invalid span context was injected, it verifies that valid trace span
// context is not extracted.
func (s) TestExtract(t *testing.T) {
tests := []struct {
name string
wantSC oteltrace.SpanContext // expected span context from carrier
incomingMd metadata.MD
outgoingMd metadata.MD
wantKeys []string
}{
{
name: "valid OpenTelemetry span context",
wantSC: validSpanContext.WithRemote(true),
incomingMd: metadata.MD{grpcTraceBinHeaderKey: []string{string(toBinary(validSpanContext.WithRemote(true)))}, "incoming-key": []string{"incoming-value"}},
outgoingMd: metadata.MD{"outgoing-key": []string{"outgoing-value"}},
dfawley marked this conversation as resolved.
Show resolved Hide resolved
wantKeys: []string{grpcTraceBinHeaderKey, "incoming-key"},
},
{
name: "invalid OpenTelemetry span context",
wantSC: oteltrace.SpanContext{},
incomingMd: metadata.MD{grpcTraceBinHeaderKey: []string{string(toBinary(oteltrace.SpanContext{}))}, "incoming-key": []string{"incoming-value"}},
outgoingMd: metadata.MD{"outgoing-key": []string{"outgoing-value"}},
wantKeys: []string{grpcTraceBinHeaderKey, "incoming-key"},
dfawley marked this conversation as resolved.
Show resolved Hide resolved
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := GRPCTraceBinPropagator{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = metadata.NewIncomingContext(ctx, test.incomingMd)
ctx = metadata.NewOutgoingContext(ctx, test.outgoingMd)

c := itracing.NewIncomingCarrier(ctx)

if gotKeys := c.Keys(); !cmp.Equal(test.wantKeys, gotKeys, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
t.Errorf("c.Keys() = keys %v, want %v", gotKeys, test.wantKeys)
}
tCtx := p.Extract(ctx, c)
got := oteltrace.SpanContextFromContext(tCtx)
if !got.Equal(test.wantSC) {
t.Fatalf("got span context: %v, want span context: %v", got, test.wantSC)
}
})
}
}

// TestBinary verifies that the toBinary() function correctly serializes a valid
// OpenTelemetry span context into its binary format representation. If span
// context is invalid, it verifies that serialization is nil.
func (s) TestToBinary(t *testing.T) {
tests := []struct {
name string
sc oteltrace.SpanContext
want []byte
}{
{
name: "valid context",
sc: validSpanContext,
want: toBinary(validSpanContext),
},
{
name: "zero value context",
sc: oteltrace.SpanContext{},
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := toBinary(test.sc); !cmp.Equal(got, test.want) {
t.Fatalf("binary() = %v, want %v", got, test.want)
}
})
}
}

// TestFromBinary verifies that the fromBinary() function correctly
// deserializes a binary format representation of a valid OpenTelemetry span
// context into its corresponding span context format. If span context's binary
// representation is invalid, it verifies that deserialization is zero value
// span context.
func (s) TestFromBinary(t *testing.T) {
tests := []struct {
name string
b []byte
want oteltrace.SpanContext
ok bool
}{
{
name: "valid",
b: []byte{0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 17, 18, 19, 20, 21, 22, 23, 24, 2, 1},
want: validSpanContext.WithRemote(true),
ok: true,
},
{
name: "invalid length",
b: []byte{0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 17, 18, 19, 20, 21, 22, 23, 24, 2},
want: oteltrace.SpanContext{},
ok: false,
},
{
name: "invalid version",
b: []byte{1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 17, 18, 19, 20, 21, 22, 23, 24, 2, 1},
want: oteltrace.SpanContext{},
ok: false,
},
{
name: "invalid traceID field ID",
b: []byte{0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 17, 18, 19, 20, 21, 22, 23, 24, 2, 1},
want: oteltrace.SpanContext{},
ok: false,
},
{
name: "invalid spanID field ID",
b: []byte{0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0, 17, 18, 19, 20, 21, 22, 23, 24, 2, 1},
want: oteltrace.SpanContext{},
ok: false,
},
{
name: "invalid traceFlags field ID",
b: []byte{0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 17, 18, 19, 20, 21, 22, 23, 24, 1, 1},
want: oteltrace.SpanContext{},
ok: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, ok := fromBinary(test.b)
if ok != test.ok {
t.Fatalf("fromBinary() ok = %v, want %v", ok, test.ok)
return
}
if !got.Equal(test.want) {
t.Fatalf("fromBinary() got = %v, want %v", got, test.want)
}
})
}
}
Loading
Loading