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(logging): OpenTelemetry trace/span ID integration for Go logging library #10030

Merged
merged 8 commits into from
May 16, 2024
2 changes: 1 addition & 1 deletion logging/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/googleapis/gax-go/v2 v2.12.3
go.opencensus.io v0.24.0
go.opentelemetry.io/otel/trace v1.24.0
golang.org/x/oauth2 v0.19.0
google.golang.org/api v0.175.0
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda
Expand Down Expand Up @@ -38,7 +39,6 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import (
"time"
"unicode/utf8"

"go.opentelemetry.io/otel/trace"

vkit "cloud.google.com/go/logging/apiv2"
logpb "cloud.google.com/go/logging/apiv2/loggingpb"
"cloud.google.com/go/logging/internal"
Expand Down Expand Up @@ -813,6 +815,13 @@ func populateTraceInfo(e *Entry, req *http.Request) bool {
return false
}
}
otelSpanContext := trace.SpanContextFromContext(req.Context())
if otelSpanContext.IsValid() {
e.Trace = otelSpanContext.TraceID().String()
e.SpanID = otelSpanContext.SpanID().String()
e.TraceSampled = otelSpanContext.IsSampled()
return true
}
header := req.Header.Get("Traceparent")
if header != "" {
// do not use traceSampled flag defined by traceparent because
Expand Down
166 changes: 166 additions & 0 deletions logging/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
gax "github.com/googleapis/gax-go/v2"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"golang.org/x/oauth2"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
Expand Down Expand Up @@ -609,6 +611,170 @@ func TestToLogEntry(t *testing.T) {
}
}

func TestToLogEntryOTelIntegration(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we plan to add system tests to start otel span and compare auto-generated otel spanId with the injected logEntry spanId?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cindy-peng I just added an integration/system test for the exact same test case, just with different span setups.

// Some slight modifications need to be done for testing ToLogEntry
// for the OpenTelemetry integration, so they are in a separate function.
u := &url.URL{Scheme: "http"}
otelTraceID, _ := trace.TraceIDFromHex(strings.Repeat("a", 32))
otelSpanID, _ := trace.SpanIDFromHex(strings.Repeat("f", 16))
otelTraceFlags := trace.FlagsSampled // tracesampled = true
tests := []struct {
name string
in logging.Entry
spanContextConfig trace.SpanContextConfig // default = noop span context
want *logpb.LogEntry
}{
{
name: "Using OpenTelemetry with a valid span",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
},
},
},
spanContextConfig: trace.SpanContextConfig{
TraceID: otelTraceID,
SpanID: otelSpanID,
TraceFlags: otelTraceFlags,
},
want: &logpb.LogEntry{
Trace: "projects/P/traces/" + otelTraceID.String(),
SpanId: otelSpanID.String(),
TraceSampled: otelTraceFlags.IsSampled(),
},
},
{
name: "OpenTelemetry only with a noop span",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
},
},
},
want: &logpb.LogEntry{},
},
{
name: "Using OpenTelemetry only with a valid span + valid traceparent headers (precedence test)",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
Header: http.Header{
"Traceparent": {"00-105445aa7843bc8bf206b12000100012-000000000000004a-01"},
},
},
},
},
spanContextConfig: trace.SpanContextConfig{
TraceID: otelTraceID,
SpanID: otelSpanID,
TraceFlags: otelTraceFlags,
},
want: &logpb.LogEntry{
Trace: "projects/P/traces/" + otelTraceID.String(),
SpanId: otelSpanID.String(),
TraceSampled: otelTraceFlags.IsSampled(),
},
},
{
name: "Using OpenTelemetry only with a valid span + valid XCTC headers (precedence test)",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
Header: http.Header{
"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120000000/0000000000000bbb;o=1"},
},
},
},
},
spanContextConfig: trace.SpanContextConfig{
TraceID: otelTraceID,
SpanID: otelSpanID,
TraceFlags: otelTraceFlags,
},
want: &logpb.LogEntry{
Trace: "projects/P/traces/" + otelTraceID.String(),
SpanId: otelSpanID.String(),
TraceSampled: otelTraceFlags.IsSampled(),
},
},
{
name: "Using OpenTelemetry only with a noop span + valid traceparent headers (edge case)",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
Header: http.Header{
"Traceparent": {"00-105445aa7843bc8bf206b12000100012-000000000000004a-01"},
},
},
},
},
want: &logpb.LogEntry{
Trace: "projects/P/traces/105445aa7843bc8bf206b12000100012",
SpanId: "000000000000004a",
TraceSampled: false,
},
},
{
name: "Using OpenTelemetry with a valid span + trace info set in Entry object",
in: logging.Entry{
HTTPRequest: &logging.HTTPRequest{
Request: &http.Request{
URL: u,
},
},
Trace: "abc",
SpanID: "def",
TraceSampled: false,
},
spanContextConfig: trace.SpanContextConfig{
TraceID: otelTraceID,
SpanID: otelSpanID,
TraceFlags: otelTraceFlags,
},
want: &logpb.LogEntry{
Trace: "abc",
SpanId: "def",
TraceSampled: false,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()

// Need to set a span context for the context so it's not a noop
spanContext := trace.NewSpanContext(test.spanContextConfig)
ctx = trace.ContextWithSpanContext(ctx, spanContext)
ctx, span := noop.NewTracerProvider().Tracer("test tracer").Start(ctx, "test span")
defer span.End()

if test.in.HTTPRequest != nil && test.in.HTTPRequest.Request != nil {
test.in.HTTPRequest.Request = test.in.HTTPRequest.Request.WithContext(ctx)
}

e, err := logging.ToLogEntry(test.in, "projects/P")
if err != nil {
t.Fatalf("Unexpected error: %+v: %v", test.in, err)
}
if got := e.Trace; got != test.want.Trace {
t.Errorf("TraceId: %+v: SpanContext: %+v: got %q, want %q", test.in, spanContext, got, test.want.Trace)
}
if got := e.SpanId; got != test.want.SpanId {
t.Errorf("SpanId: %+v: SpanContext: %+v: got %q, want %q", test.in, spanContext, got, test.want.SpanId)
}
if got := e.TraceSampled; got != test.want.TraceSampled {
t.Errorf("TraceSampled: %+v: SpanContext: %+v: got %t, want %t", test.in, spanContext, got, test.want.TraceSampled)
}

})
}
}

// compareEntries compares most fields list of Entries against expected. compareEntries does not compare:
// - HTTPRequest
// - Operation
Expand Down