Repository: grpc/grpc-go
File: stats/opentelemetry/trace.go (Go)

Code before:
// invariants of the RPC lifecycle. It ends the span, triggering its export.
// This function handles attempt spans on the client-side and call spans on the
// server-side.
func populateSpan(rs stats.RPCStats, ai *attemptInfo) {
	if ai == nil || ai.traceSpan == nil {
		// Shouldn't happen, tagRPC call comes before this function gets called
		// which populates this information.
		logger.Error("ctx passed into stats handler tracing event handling has no traceSpan present")
		return
	}
	span := ai.traceSpan

	switch rs := rs.(type) {
	case *stats.Begin:
		// Note: Go always added Client and FailFast attributes even though they are not
		// defined by the OpenCensus gRPC spec. Thus, they are unimportant for
		// correctness.
		span.SetAttributes(
			attribute.Bool("Client", rs.Client),
			attribute.Bool("FailFast", rs.Client),
			attribute.Int64("previous-rpc-attempts", int64(ai.previousRPCAttempts)),
			attribute.Bool("transparent-retry", rs.IsTransparentRetryAttempt),
		)
		// increment previous rpc attempts applicable for next attempt
		atomic.AddUint32(&ai.previousRPCAttempts, 1)
		if ai.nameResolutionDelayed {
			span.AddEvent("Delayed name resolution complete")
		}
	case *stats.PickerUpdated:
		span.AddEvent("Delayed LB pick complete")
	case *stats.InPayload:
		// message id - "must be calculated as two different counters starting
		// from one for sent messages and one for received messages."
		ai.countRecvMsg++
		span.AddEvent("Inbound compressed message", trace.WithAttributes(
			attribute.Int64("sequence-number", int64(ai.countRecvMsg)),
			attribute.Int64("message-size", int64(rs.Length)),
			attribute.Int64("message-size-compressed", int64(rs.CompressedLength)),
		))
	case *stats.OutPayload:
		ai.countSentMsg++
		span.AddEvent("Outbound compressed message", trace.WithAttributes(
			attribute.Int64("sequence-number", int64(ai.countSentMsg)),
			attribute.Int64("message-size", int64(rs.Length)),
			attribute.Int64("message-size-compressed", int64(rs.CompressedLength)),
		))
	case *stats.End:
		if rs.Error != nil {
			s := status.Convert(rs.Error)
			span.SetStatus(otelcodes.Error, s.Message())
		} else {

Review comment (bug):
this is wrong. As per https://github.com/grpc/proposal/blob/master/A72-open-telemetry-tracing.md#tracing-information, "Delayed name resolution" should be an event in the call span not attempt span.

This should be right place https://github.com/grpc/grpc-go/blob/master/stats/opentelemetry/client_tracing.go#L34. Before creating the attempt span, you need to retrieve the current call span using `trace.SpanFromContext` and add an event to that span. Before that, also check if that event already exist or not. And only add, if it exist.

Code after:
// invariants of the RPC lifecycle. It ends the span, triggering its export.
// This function handles attempt spans on the client-side and call spans on the
// server-side.
func populateSpan(rs stats.RPCStats, ai *attemptInfo) {
	if ai == nil || ai.traceSpan == nil {
		// Shouldn't happen, tagRPC call comes before this function gets called
		// which populates this information.
		logger.Error("ctx passed into stats handler tracing event handling has no traceSpan present")
		return
	}
	span := ai.traceSpan

	switch rs := rs.(type) {
	case *stats.Begin:
		// Note: Go always added Client and FailFast attributes even though they are not
		// defined by the OpenCensus gRPC spec. Thus, they are unimportant for
		// correctness.
		span.SetAttributes(
			attribute.Bool("Client", rs.Client),
			attribute.Bool("FailFast", rs.FailFast),
			attribute.Int64("previous-rpc-attempts", int64(ai.previousRPCAttempts)),
			attribute.Bool("transparent-retry", rs.IsTransparentRetryAttempt),
		)
		// increment previous rpc attempts applicable for next attempt
		atomic.AddUint32(&ai.previousRPCAttempts, 1)
	case *stats.PickerUpdated:
		span.AddEvent("Delayed LB pick complete")
	case *stats.InPayload:
		// message id - "must be calculated as two different counters starting
		// from one for sent messages and one for received messages."
		ai.countRecvMsg++
		span.AddEvent("Inbound compressed message", trace.WithAttributes(
			attribute.Int64("sequence-number", int64(ai.countRecvMsg)),
			attribute.Int64("message-size", int64(rs.Length)),
			attribute.Int64("message-size-compressed", int64(rs.CompressedLength)),
		))
	case *stats.OutPayload:
		ai.countSentMsg++
		span.AddEvent("Outbound compressed message", trace.WithAttributes(
			attribute.Int64("sequence-number", int64(ai.countSentMsg)),
			attribute.Int64("message-size", int64(rs.Length)),
			attribute.Int64("message-size-compressed", int64(rs.CompressedLength)),
		))
	case *stats.End:
		if rs.Error != nil {
			s := status.Convert(rs.Error)
			span.SetStatus(otelcodes.Error, s.Message())
		} else {
			span.SetStatus(otelcodes.Ok, "Ok")
		}
		span.End()