Skip to content

Commit 78eebff

Browse files
authored
stats/opentelemetry: Introduce Tracing API (#7852)
1 parent 7e1c9b2 commit 78eebff

File tree

8 files changed

+1334
-59
lines changed

8 files changed

+1334
-59
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Package opentelemetry is EXPERIMENTAL and will be moved to stats/opentelemetry
18+
// package in a later release.
19+
package opentelemetry
20+
21+
import (
22+
"go.opentelemetry.io/otel/propagation"
23+
"go.opentelemetry.io/otel/trace"
24+
)
25+
26+
// TraceOptions contains the tracing settings for OpenTelemetry instrumentation.
27+
type TraceOptions struct {
28+
// TracerProvider is the OpenTelemetry tracer which is required to
29+
// record traces/trace spans for instrumentation. If unset, tracing
30+
// will not be recorded.
31+
TracerProvider trace.TracerProvider
32+
33+
// TextMapPropagator propagates span context through text map carrier.
34+
// If unset, tracing will not be recorded.
35+
TextMapPropagator propagation.TextMapPropagator
36+
}

stats/opentelemetry/client_metrics.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import (
2121
"sync/atomic"
2222
"time"
2323

24+
otelcodes "go.opentelemetry.io/otel/codes"
25+
"go.opentelemetry.io/otel/trace"
2426
"google.golang.org/grpc"
27+
grpccodes "google.golang.org/grpc/codes"
2528
estats "google.golang.org/grpc/experimental/stats"
2629
istats "google.golang.org/grpc/internal/stats"
2730
"google.golang.org/grpc/metadata"
@@ -85,8 +88,12 @@ func (h *clientStatsHandler) unaryInterceptor(ctx context.Context, method string
8588
}
8689

8790
startTime := time.Now()
91+
var span trace.Span
92+
if h.options.isTracingEnabled() {
93+
ctx, span = h.createCallTraceSpan(ctx, method)
94+
}
8895
err := invoker(ctx, method, req, reply, cc, opts...)
89-
h.perCallMetrics(ctx, err, startTime, ci)
96+
h.perCallTracesAndMetrics(ctx, err, startTime, ci, span)
9097
return err
9198
}
9299

@@ -119,22 +126,37 @@ func (h *clientStatsHandler) streamInterceptor(ctx context.Context, desc *grpc.S
119126
}
120127

121128
startTime := time.Now()
122-
129+
var span trace.Span
130+
if h.options.isTracingEnabled() {
131+
ctx, span = h.createCallTraceSpan(ctx, method)
132+
}
123133
callback := func(err error) {
124-
h.perCallMetrics(ctx, err, startTime, ci)
134+
h.perCallTracesAndMetrics(ctx, err, startTime, ci, span)
125135
}
126136
opts = append([]grpc.CallOption{grpc.OnFinish(callback)}, opts...)
127137
return streamer(ctx, desc, cc, method, opts...)
128138
}
129139

130-
func (h *clientStatsHandler) perCallMetrics(ctx context.Context, err error, startTime time.Time, ci *callInfo) {
131-
callLatency := float64(time.Since(startTime)) / float64(time.Second) // calculate ASAP
132-
attrs := otelmetric.WithAttributeSet(otelattribute.NewSet(
133-
otelattribute.String("grpc.method", ci.method),
134-
otelattribute.String("grpc.target", ci.target),
135-
otelattribute.String("grpc.status", canonicalString(status.Code(err))),
136-
))
137-
h.clientMetrics.callDuration.Record(ctx, callLatency, attrs)
140+
// perCallTracesAndMetrics records per call trace spans and metrics.
141+
func (h *clientStatsHandler) perCallTracesAndMetrics(ctx context.Context, err error, startTime time.Time, ci *callInfo, ts trace.Span) {
142+
if h.options.isTracingEnabled() {
143+
s := status.Convert(err)
144+
if s.Code() == grpccodes.OK {
145+
ts.SetStatus(otelcodes.Ok, s.Message())
146+
} else {
147+
ts.SetStatus(otelcodes.Error, s.Message())
148+
}
149+
ts.End()
150+
}
151+
if h.options.isMetricsEnabled() {
152+
callLatency := float64(time.Since(startTime)) / float64(time.Second)
153+
attrs := otelmetric.WithAttributeSet(otelattribute.NewSet(
154+
otelattribute.String("grpc.method", ci.method),
155+
otelattribute.String("grpc.target", ci.target),
156+
otelattribute.String("grpc.status", canonicalString(status.Code(err))),
157+
))
158+
h.clientMetrics.callDuration.Record(ctx, callLatency, attrs)
159+
}
138160
}
139161

140162
// TagConn exists to satisfy stats.Handler.
@@ -163,15 +185,17 @@ func (h *clientStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo)
163185
}
164186
ctx = istats.SetLabels(ctx, labels)
165187
}
166-
ai := &attemptInfo{ // populates information about RPC start.
188+
ai := &attemptInfo{
167189
startTime: time.Now(),
168190
xdsLabels: labels.TelemetryLabels,
169-
method: info.FullMethodName,
191+
method: removeLeadingSlash(info.FullMethodName),
170192
}
171-
ri := &rpcInfo{
172-
ai: ai,
193+
if h.options.isTracingEnabled() {
194+
ctx, ai = h.traceTagRPC(ctx, ai)
173195
}
174-
return setRPCInfo(ctx, ri)
196+
return setRPCInfo(ctx, &rpcInfo{
197+
ai: ai,
198+
})
175199
}
176200

177201
func (h *clientStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
@@ -180,7 +204,12 @@ func (h *clientStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
180204
logger.Error("ctx passed into client side stats handler metrics event handling has no client attempt data present")
181205
return
182206
}
183-
h.processRPCEvent(ctx, rs, ri.ai)
207+
if h.options.isMetricsEnabled() {
208+
h.processRPCEvent(ctx, rs, ri.ai)
209+
}
210+
if h.options.isTracingEnabled() {
211+
populateSpan(rs, ri.ai)
212+
}
184213
}
185214

186215
func (h *clientStatsHandler) processRPCEvent(ctx context.Context, s stats.RPCStats, ai *attemptInfo) {

stats/opentelemetry/client_tracing.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package opentelemetry
18+
19+
import (
20+
"context"
21+
"strings"
22+
23+
"go.opentelemetry.io/otel"
24+
"go.opentelemetry.io/otel/trace"
25+
otelinternaltracing "google.golang.org/grpc/stats/opentelemetry/internal/tracing"
26+
)
27+
28+
// traceTagRPC populates provided context with a new span using the
29+
// TextMapPropagator supplied in trace options and internal itracing.carrier.
30+
// It creates a new outgoing carrier which serializes information about this
31+
// span into gRPC Metadata, if TextMapPropagator is provided in the trace
32+
// options. if TextMapPropagator is not provided, it returns the context as is.
33+
func (h *clientStatsHandler) traceTagRPC(ctx context.Context, ai *attemptInfo) (context.Context, *attemptInfo) {
34+
mn := "Attempt." + strings.Replace(ai.method, "/", ".", -1)
35+
tracer := otel.Tracer("grpc-open-telemetry")
36+
ctx, span := tracer.Start(ctx, mn)
37+
carrier := otelinternaltracing.NewOutgoingCarrier(ctx)
38+
otel.GetTextMapPropagator().Inject(ctx, carrier)
39+
ai.traceSpan = span
40+
return carrier.Context(), ai
41+
}
42+
43+
// createCallTraceSpan creates a call span to put in the provided context using
44+
// provided TraceProvider. If TraceProvider is nil, it returns context as is.
45+
func (h *clientStatsHandler) createCallTraceSpan(ctx context.Context, method string) (context.Context, trace.Span) {
46+
if h.options.TraceOptions.TracerProvider == nil {
47+
logger.Error("TraceProvider is not provided in trace options")
48+
return ctx, nil
49+
}
50+
mn := strings.Replace(removeLeadingSlash(method), "/", ".", -1)
51+
tracer := otel.Tracer("grpc-open-telemetry")
52+
ctx, span := tracer.Start(ctx, mn, trace.WithSpanKind(trace.SpanKindClient))
53+
return ctx, span
54+
}

0 commit comments

Comments
 (0)