diff --git a/gqlgen.go b/gqlgen.go index d0eca18..ba6974f 100644 --- a/gqlgen.go +++ b/gqlgen.go @@ -17,6 +17,9 @@ package otelgqlgen import ( "context" "fmt" + "net/http" + "reflect" + "strconv" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler/extension" @@ -24,6 +27,7 @@ import ( otelcontrib "go.opentelemetry.io/contrib" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -96,11 +100,31 @@ func (a Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHand } resp := next(ctx) + var statusCode = http.StatusOK if resp != nil && len(resp.Errors) > 0 { + statusCode = http.StatusInternalServerError span.SetStatus(codes.Error, resp.Errors.Error()) span.RecordError(fmt.Errorf(resp.Errors.Error())) span.SetAttributes(ResolverErrors(resp.Errors)...) - } + if resp.Errors[0].Extensions["code"] != nil { + var code = resp.Errors[0].Extensions["code"] + if reflect.TypeOf(code).Kind() == reflect.Int { + statusCode = code.(int) + } else if reflect.TypeOf(code).Kind() == reflect.Int32 { + statusCode = int(code.(int32)) + } else if reflect.TypeOf(code).Kind() == reflect.String { + status, err := strconv.Atoi(code.(string)) + if err == nil { + statusCode = status + } + } + } + } + + // Stable: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes + span.SetAttributes(semconv.HTTPResponseStatusCode(statusCode)) + // Experimental: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md#common-attributes + span.SetAttributes(semconv.HTTPStatusCode(statusCode)) // nolint:staticcheck return resp } @@ -131,11 +155,31 @@ func (a Tracer) InterceptField(ctx context.Context, next graphql.Resolver) (inte resp, err := next(ctx) errList := graphql.GetFieldErrors(ctx, fc) + var statusCode = http.StatusOK if len(errList) != 0 { + statusCode = http.StatusInternalServerError span.SetStatus(codes.Error, errList.Error()) span.RecordError(fmt.Errorf(errList.Error())) span.SetAttributes(ResolverErrors(errList)...) - } + if errList[0].Extensions["code"] != nil { + var code = errList[0].Extensions["code"] + if reflect.TypeOf(code).Kind() == reflect.Int { + statusCode = code.(int) + } else if reflect.TypeOf(code).Kind() == reflect.Int32 { + statusCode = int(code.(int32)) + } else if reflect.TypeOf(code).Kind() == reflect.String { + status, err := strconv.Atoi(code.(string)) + if err == nil { + statusCode = status + } + } + } + } + + // Stable: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes + span.SetAttributes(semconv.HTTPResponseStatusCode(statusCode)) + // Experimental: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md#common-attributes + span.SetAttributes(semconv.HTTPStatusCode(statusCode)) // nolint:staticcheck return resp, err } diff --git a/gqlgen_test.go b/gqlgen_test.go index 408d29f..839e320 100644 --- a/gqlgen_test.go +++ b/gqlgen_test.go @@ -37,6 +37,7 @@ import ( "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" "go.opentelemetry.io/otel/trace" ) @@ -376,9 +377,10 @@ func TestVariablesAttributes(t *testing.T) { testSpans(t, spanRecorder, namelessQueryName, codes.Unset) spans := spanRecorder.Ended() - assert.Len(t, spans[1].Attributes(), 2) + assert.Len(t, spans[1].Attributes(), 4) assert.Equal(t, attribute.Key("gql.request.query"), spans[1].Attributes()[0].Key) assert.Equal(t, attribute.Key("gql.request.variables.id"), spans[1].Attributes()[1].Key) + assert.Equal(t, semconv.HTTPResponseStatusCodeKey, spans[1].Attributes()[2].Key) assert.Equal(t, http.StatusOK, w.Code, w.Body.String()) } @@ -414,9 +416,10 @@ func TestVariablesAttributesCustomBuilder(t *testing.T) { testSpans(t, spanRecorder, namelessQueryName, codes.Unset) spans := spanRecorder.Ended() - assert.Len(t, spans[1].Attributes(), 2) + assert.Len(t, spans[1].Attributes(), 4) assert.Equal(t, attribute.Key("gql.request.query"), spans[1].Attributes()[0].Key) assert.Equal(t, attribute.Key("id"), spans[1].Attributes()[1].Key) + assert.Equal(t, semconv.HTTPResponseStatusCodeKey, spans[1].Attributes()[2].Key) assert.Equal(t, http.StatusOK, w.Code, w.Body.String()) } @@ -444,9 +447,9 @@ func TestVariablesAttributesDisabled(t *testing.T) { testSpans(t, spanRecorder, namelessQueryName, codes.Unset) spans := spanRecorder.Ended() - assert.Len(t, spans[1].Attributes(), 1) + assert.Len(t, spans[1].Attributes(), 3) assert.Equal(t, attribute.Key("gql.request.query"), spans[1].Attributes()[0].Key) - + assert.Equal(t, semconv.HTTPResponseStatusCodeKey, spans[1].Attributes()[1].Key) // nolint:staticcheck assert.Equal(t, http.StatusOK, w.Code, w.Body.String()) }