diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a0389e6540..1ea870546e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The next release will require at least [Go 1.25]. ### Fixed +- Fix `rpc.response.status_code` attribute in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` to use SCREAMING_SNAKE_CASE (e.g. `DEADLINE_EXCEEDED`) as required by the OpenTelemetry RPC semantic conventions, instead of PascalCase (e.g. `DeadlineExceeded`). (#8543) - Change the `rpc.server.call.duration` metric value from milliseconds to seconds in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`. (#8509) - Enforce that `client_certificate_file` and `client_key_file` are provided together in `go.opentelemetry.io/contrib/otelconf`. (#8450) - Fixed broken CSS and JavaScript CDN URLs in `go.opentelemetry.io/contrib/zpages` by replacing the inaccessible code.getmdl.io CDN with cdnjs.cloudflare.com. (#8502) diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/grpc_stats_handler_test.go b/instrumentation/google.golang.org/grpc/otelgrpc/grpc_stats_handler_test.go index 3317e32778e..4ff33b06208 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/grpc_stats_handler_test.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/grpc_stats_handler_test.go @@ -176,7 +176,7 @@ func checkClientSpans(t *testing.T, spans []trace.ReadOnlySpan, addr string) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/EmptyCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress(host), semconv.ServerPort(port), testSpanAttr, @@ -208,7 +208,7 @@ func checkClientSpans(t *testing.T, spans []trace.ReadOnlySpan, addr string) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/UnaryCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress(host), semconv.ServerPort(port), testSpanAttr, @@ -268,7 +268,7 @@ func checkClientSpans(t *testing.T, spans []trace.ReadOnlySpan, addr string) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/StreamingInputCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress(host), semconv.ServerPort(port), testSpanAttr, @@ -327,7 +327,7 @@ func checkClientSpans(t *testing.T, spans []trace.ReadOnlySpan, addr string) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/StreamingOutputCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress(host), semconv.ServerPort(port), testSpanAttr, @@ -413,7 +413,7 @@ func checkClientSpans(t *testing.T, spans []trace.ReadOnlySpan, addr string) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/FullDuplexCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress(host), semconv.ServerPort(port), testSpanAttr, @@ -451,7 +451,7 @@ func checkServerSpans(t *testing.T, spans []trace.ReadOnlySpan) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/EmptyCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress("127.0.0.1"), port, testSpanAttr, @@ -485,7 +485,7 @@ func checkServerSpans(t *testing.T, spans []trace.ReadOnlySpan) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/UnaryCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress("127.0.0.1"), port, testSpanAttr, @@ -547,7 +547,7 @@ func checkServerSpans(t *testing.T, spans []trace.ReadOnlySpan) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/StreamingInputCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress("127.0.0.1"), port, testSpanAttr, @@ -608,7 +608,7 @@ func checkServerSpans(t *testing.T, spans []trace.ReadOnlySpan) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/StreamingOutputCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress("127.0.0.1"), port, testSpanAttr, @@ -696,7 +696,7 @@ func checkServerSpans(t *testing.T, spans []trace.ReadOnlySpan) { assert.ElementsMatch(t, []attribute.KeyValue{ semconv.RPCMethodKey.String("grpc.testing.TestService/FullDuplexCall"), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.ServerAddress("127.0.0.1"), port, testSpanAttr, @@ -725,35 +725,35 @@ func checkClientMetrics(t *testing.T, reader metric.Reader) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/EmptyCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/UnaryCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/StreamingInputCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/StreamingOutputCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/FullDuplexCall"), semconv.RPCSystemNameGRPC, testMetricAttr), @@ -928,35 +928,35 @@ func checkServerMetrics(t *testing.T, reader metric.Reader) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/EmptyCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/UnaryCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/StreamingInputCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/StreamingOutputCall"), semconv.RPCSystemNameGRPC, testMetricAttr), }, { Attributes: attribute.NewSet( - semconv.RPCResponseStatusCode(codes.OK.String()), + semconv.RPCResponseStatusCode("OK"), semconv.RPCMethod("grpc.testing.TestService/FullDuplexCall"), semconv.RPCSystemNameGRPC, testMetricAttr), diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go index 9e1820d7832..5b381a67eaf 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go @@ -252,6 +252,49 @@ func (h *clientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { ) } +// grpcCodeString converts a gRPC status code to its SCREAMING_SNAKE_CASE string +// representation as required by the OpenTelemetry RPC semantic conventions. +func grpcCodeString(c grpc_codes.Code) string { + switch c { + case grpc_codes.OK: + return "OK" + case grpc_codes.Canceled: + return "CANCELLED" + case grpc_codes.Unknown: + return "UNKNOWN" + case grpc_codes.InvalidArgument: + return "INVALID_ARGUMENT" + case grpc_codes.DeadlineExceeded: + return "DEADLINE_EXCEEDED" + case grpc_codes.NotFound: + return "NOT_FOUND" + case grpc_codes.AlreadyExists: + return "ALREADY_EXISTS" + case grpc_codes.PermissionDenied: + return "PERMISSION_DENIED" + case grpc_codes.ResourceExhausted: + return "RESOURCE_EXHAUSTED" + case grpc_codes.FailedPrecondition: + return "FAILED_PRECONDITION" + case grpc_codes.Aborted: + return "ABORTED" + case grpc_codes.OutOfRange: + return "OUT_OF_RANGE" + case grpc_codes.Unimplemented: + return "UNIMPLEMENTED" + case grpc_codes.Internal: + return "INTERNAL" + case grpc_codes.Unavailable: + return "UNAVAILABLE" + case grpc_codes.DataLoss: + return "DATA_LOSS" + case grpc_codes.Unauthenticated: + return "UNAUTHENTICATED" + default: + return c.String() + } +} + // TagConn can attach some information to the given context. func (*clientHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { return ctx @@ -328,9 +371,9 @@ func (c *config) handleRPC( var s *status.Status if rs.Error != nil { s, _ = status.FromError(rs.Error) - rpcStatusAttr = semconv.RPCResponseStatusCode(s.Code().String()) + rpcStatusAttr = semconv.RPCResponseStatusCode(grpcCodeString(s.Code())) } else { - rpcStatusAttr = semconv.RPCResponseStatusCode(grpc_codes.OK.String()) + rpcStatusAttr = semconv.RPCResponseStatusCode(grpcCodeString(grpc_codes.OK)) } if span.IsRecording() { if s != nil { diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handlertest_test.go b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handlertest_test.go index 3cce365ebd5..837f252b1be 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handlertest_test.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handlertest_test.go @@ -24,6 +24,49 @@ import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" ) +// grpcCodeToString mirrors the production grpcCodeString function, converting +// gRPC status codes to SCREAMING_SNAKE_CASE as required by the OTel RPC spec. +func grpcCodeToString(c grpc_codes.Code) string { + switch c { + case grpc_codes.OK: + return "OK" + case grpc_codes.Canceled: + return "CANCELLED" + case grpc_codes.Unknown: + return "UNKNOWN" + case grpc_codes.InvalidArgument: + return "INVALID_ARGUMENT" + case grpc_codes.DeadlineExceeded: + return "DEADLINE_EXCEEDED" + case grpc_codes.NotFound: + return "NOT_FOUND" + case grpc_codes.AlreadyExists: + return "ALREADY_EXISTS" + case grpc_codes.PermissionDenied: + return "PERMISSION_DENIED" + case grpc_codes.ResourceExhausted: + return "RESOURCE_EXHAUSTED" + case grpc_codes.FailedPrecondition: + return "FAILED_PRECONDITION" + case grpc_codes.Aborted: + return "ABORTED" + case grpc_codes.OutOfRange: + return "OUT_OF_RANGE" + case grpc_codes.Unimplemented: + return "UNIMPLEMENTED" + case grpc_codes.Internal: + return "INTERNAL" + case grpc_codes.Unavailable: + return "UNAVAILABLE" + case grpc_codes.DataLoss: + return "DATA_LOSS" + case grpc_codes.Unauthenticated: + return "UNAUTHENTICATED" + default: + return c.String() + } +} + func getSpanFromRecorder(sr *tracetest.SpanRecorder, name string) (trace.ReadOnlySpan, bool) { for _, s := range sr.Ended() { if s.Name() == name { @@ -181,7 +224,7 @@ func assertServerSpan(t *testing.T, wantSpanCode otelcode.Code, wantSpanStatusDe } require.True(t, codeAttr.Valid(), "attributes contain gRPC status code") - assert.Equal(t, attribute.StringValue(wantGrpcCode.String()), codeAttr.Value) + assert.Equal(t, attribute.StringValue(grpcCodeToString(wantGrpcCode)), codeAttr.Value) } func assertStatsHandlerServerMetrics(t *testing.T, reader metric.Reader, serviceName, name string, code grpc_codes.Code) { @@ -199,7 +242,7 @@ func assertStatsHandlerServerMetrics(t *testing.T, reader metric.Reader, service Attributes: attribute.NewSet( semconv.RPCMethod(serviceName+"/"+name), semconv.RPCSystemNameGRPC, - semconv.RPCResponseStatusCode(code.String()), + semconv.RPCResponseStatusCode(grpcCodeToString(code)), testMetricAttr, ), },