Skip to content

Commit

Permalink
Fix HTTP instrumentations to set proper span status (#2427)
Browse files Browse the repository at this point in the history
Add TestSpanStatus tests
  • Loading branch information
pellared committed Jun 24, 2022
1 parent f2c368a commit 18e11ae
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- The `go.opentelemetry.io/contrib/propagators/autoprop` package to provide configuration of propagators with useful defaults and envar support. (#2258)

### Fixed

- Fix the `otelhttp`, `otelgin`, `otelmacaron`, `otelrestful` middlewares
by using `SpanKindServer` when deciding the `SpanStatus`.
This makes `4xx` response codes to not be an error anymore. (#2427)

## [1.7.0/0.32.0] - 2022-04-28

### Added
Expand Down
Expand Up @@ -65,7 +65,7 @@ func OTelFilter(service string, opts ...Option) restful.FilterFunction {
chain.ProcessFilter(req, resp)

attrs := semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode())
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(resp.StatusCode())
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(resp.StatusCode(), oteltrace.SpanKindServer)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)
}
Expand Down
Expand Up @@ -17,6 +17,7 @@ package test
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/emicklei/go-restful/v3"
Expand All @@ -26,6 +27,7 @@ import (
"go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
oteltrace "go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -171,6 +173,37 @@ func TestMultiFilters(t *testing.T) {
assertSpan(t, spans[2], "/library/{name}")
}

func TestSpanStatus(t *testing.T) {
testCases := []struct {
httpStatusCode int
wantSpanStatus codes.Code
}{
{200, codes.Unset},
{400, codes.Unset},
{500, codes.Error},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(tc.httpStatusCode), func(t *testing.T) {
sr := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider()
provider.RegisterSpanProcessor(sr)
handlerFunc := func(req *restful.Request, resp *restful.Response) {
resp.WriteHeader(tc.httpStatusCode)
}
ws := &restful.WebService{}
ws.Route(ws.GET("/").To(handlerFunc))
container := restful.NewContainer()
container.Filter(otelrestful.OTelFilter("my-service", otelrestful.WithTracerProvider(provider)))
container.Add(ws)

container.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

require.Len(t, sr.Ended(), 1, "should emit a span")
assert.Equal(t, sr.Ended()[0].Status().Code, tc.wantSpanStatus, "should only set Error status for HTTP statuses >= 500")
})
}
}

func assertSpan(t *testing.T, span sdktrace.ReadOnlySpan, name string, attrs ...attribute.KeyValue) {
assert.Equal(t, name, span.Name())
assert.Equal(t, oteltrace.SpanKindServer, span.SpanKind())
Expand Down
Expand Up @@ -81,7 +81,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc {

status := c.Writer.Status()
attrs := semconv.HTTPAttributesFromHTTPStatusCode(status)
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status)
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(status, oteltrace.SpanKindServer)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)
if len(c.Errors) > 0 {
Expand Down
Expand Up @@ -21,6 +21,7 @@ import (
"html/template"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -136,6 +137,34 @@ func TestError(t *testing.T) {
assert.Equal(t, codes.Error, span.Status().Code)
}

func TestSpanStatus(t *testing.T) {
testCases := []struct {
httpStatusCode int
wantSpanStatus codes.Code
}{
{200, codes.Unset},
{400, codes.Unset},
{500, codes.Error},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(tc.httpStatusCode), func(t *testing.T) {
sr := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider()
provider.RegisterSpanProcessor(sr)
router := gin.New()
router.Use(otelgin.Middleware("foobar", otelgin.WithTracerProvider(provider)))
router.GET("/", func(c *gin.Context) {
c.Status(tc.httpStatusCode)
})

router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

require.Len(t, sr.Ended(), 1, "should emit a span")
assert.Equal(t, sr.Ended()[0].Status().Code, tc.wantSpanStatus, "should only set Error status for HTTP statuses >= 500")
})
}
}

func TestHTML(t *testing.T) {
sr := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/gopkg.in/macaron.v1/otelmacaron/macaron.go
Expand Up @@ -64,7 +64,7 @@ func Middleware(service string, opts ...Option) macaron.Handler {

status := c.Resp.Status()
attrs := semconv.HTTPAttributesFromHTTPStatusCode(status)
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status)
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(status, oteltrace.SpanKindServer)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)
}
Expand Down
Expand Up @@ -17,6 +17,7 @@ package test
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -26,6 +27,7 @@ import (
"go.opentelemetry.io/contrib/instrumentation/gopkg.in/macaron.v1/otelmacaron"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
oteltrace "go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -94,3 +96,31 @@ func TestChildSpanNames(t *testing.T) {
assert.Contains(t, attrs, attribute.String("http.method", "GET"))
assert.Contains(t, attrs, attribute.String("http.target", "/book/foo"))
}

func TestSpanStatus(t *testing.T) {
testCases := []struct {
httpStatusCode int
wantSpanStatus codes.Code
}{
{200, codes.Unset},
{400, codes.Unset},
{500, codes.Error},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(tc.httpStatusCode), func(t *testing.T) {
sr := tracetest.NewSpanRecorder()
provider := trace.NewTracerProvider()
provider.RegisterSpanProcessor(sr)
m := macaron.Classic()
m.Use(otelmacaron.Middleware("foobar", otelmacaron.WithTracerProvider(provider)))
m.Get("/", func(ctx *macaron.Context) {
ctx.Resp.WriteHeader(tc.httpStatusCode)
})

m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

require.Len(t, sr.Ended(), 1, "should emit a span")
assert.Equal(t, sr.Ended()[0].Status().Code, tc.wantSpanStatus, "should only set Error status for HTTP statuses >= 500")
})
}
}
2 changes: 1 addition & 1 deletion instrumentation/net/http/otelhttp/handler.go
Expand Up @@ -227,7 +227,7 @@ func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int,
}
if statusCode > 0 {
attributes = append(attributes, semconv.HTTPAttributesFromHTTPStatusCode(statusCode)...)
span.SetStatus(semconv.SpanStatusFromHTTPStatusCode(statusCode))
span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(statusCode, trace.SpanKindServer))
}
if werr != nil && werr != io.EOF {
attributes = append(attributes, WriteErrorKey.String(werr.Error()))
Expand Down
31 changes: 31 additions & 0 deletions instrumentation/net/http/otelhttp/test/handler_test.go
Expand Up @@ -21,6 +21,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"

Expand All @@ -29,6 +30,7 @@ import (

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric/metrictest"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
Expand Down Expand Up @@ -187,3 +189,32 @@ func TestWithPublicEndpoint(t *testing.T) {
require.Len(t, done[0].Links(), 1, "should contain link")
require.True(t, sc.Equal(done[0].Links()[0].SpanContext), "should link incoming span context")
}

func TestSpanStatus(t *testing.T) {
testCases := []struct {
httpStatusCode int
wantSpanStatus codes.Code
}{
{200, codes.Unset},
{400, codes.Unset},
{500, codes.Error},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(tc.httpStatusCode), func(t *testing.T) {
sr := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider()
provider.RegisterSpanProcessor(sr)
h := otelhttp.NewHandler(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.httpStatusCode)
}), "test_handler",
otelhttp.WithTracerProvider(provider),
)

h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

require.Len(t, sr.Ended(), 1, "should emit a span")
assert.Equal(t, sr.Ended()[0].Status().Code, tc.wantSpanStatus, "should only set Error status for HTTP statuses >= 500")
})
}
}

0 comments on commit 18e11ae

Please sign in to comment.