Split out from #9358 as requested in #9358 (comment). Same defect class as the otelhttp report, confirmed independently against otelgin.
Symptom
When a client disconnects mid-request, the handler observes the cancelled request context and typically returns an error, which surfaces as a 500. otelgin then records http.response.status_code=500 and sets the span status to Error, and sets no error.type at all, so a client disconnect is indistinguishable from a genuine server fault.
Middleware reads the status from Gin's own c.Writer.Status() after c.Next() and never inspects c.Request.Context().Err():
|
c.Next() |
|
|
|
status := c.Writer.Status() |
|
span.SetStatus(sc.Status(status)) |
|
span.SetAttributes(sc.ResponseTraceAttrs(semconv.ResponseTelemetry{ |
|
StatusCode: status, |
|
WriteBytes: int64(c.Writer.Size()), |
|
})...) |
|
|
|
if len(c.Errors) > 0 { |
|
span.SetStatus(codes.Error, c.Errors.String()) |
|
if len(c.Errors) == 1 { |
|
span.SetAttributes(otelsemconv.ErrorType(c.Errors[0].Err)) |
|
} else { |
|
span.SetAttributes(otelsemconv.ErrorTypeOther) |
|
} |
|
} |
otelgin does set error.type, but only from c.Errors. A client disconnect does not populate c.Errors, so that path does not fire. Because the middleware uses Gin's ResponseWriter rather than a wrapper, there is also no observed response-write error, and the request body is not wrapped, so there is no observed body-read error either. The metric path (sc.RecordMetrics) carries StatusCode only and no error.type.
Reproduction
Confirmed on main at 5f30960 with a real HTTP/1.1 disconnect. The handler waits on c.Request.Context().Done() and then writes 500:
router.Use(otelgin.Middleware("srv", otelgin.WithTracerProvider(tp)))
router.GET("/hello", func(c *gin.Context) {
<-c.Request.Context().Done()
c.String(http.StatusInternalServerError, "cancelled")
})
The client issues the request with a cancellable context and cancels it after 100ms. Recorded span:
span name = "GET /hello"
span status = Error ""
attr http.route = /hello
attr http.response.body.size = 9
attr http.response.status_code = 500
No error.type. Identical outcome to the otelhttp reproduction in #9358.
Suggested fix
Mirroring the triage in #9358 (comment), adapted to Gin:
- Prefer an observed failure cause over the bare status code — request-context error, and any response-write or request-body-read error if
otelgin starts observing those.
- Set span status to
Error and add a predictable, low-cardinality error.type for that cause.
- Preserve the observed/selected HTTP status code; for an error status with no more specific cause, use the status-code string as
error.type. Keep the existing c.Errors classification working, and decide precedence between a c.Errors entry and a request-context error.
- Add
error.type to the metric attributes recorded by sc.RecordMetrics, per the HTTP metric conventions.
- Add a real disconnect regression test so cancellation is detected before the middleware returns.
Note on shared code
otelgin/internal/semconv/server.go is generated from internal/shared/semconv/server.go.tmpl and is byte-identical to the copy vendored into otelhttp (verified by diff). The same is true for otelmux, otelecho and otelrestful. So whatever error.type classification lands in the shared template is picked up by all of them, but each middleware still needs its own wiring to detect and pass the failure cause, plus its own regression test.
Relevant requirements: HTTP span conventions, HTTP metric conventions, and the dropped-connection example.
Split out from #9358 as requested in #9358 (comment). Same defect class as the
otelhttpreport, confirmed independently againstotelgin.Symptom
When a client disconnects mid-request, the handler observes the cancelled request context and typically returns an error, which surfaces as a 500.
otelginthen recordshttp.response.status_code=500and sets the span status toError, and sets noerror.typeat all, so a client disconnect is indistinguishable from a genuine server fault.Middlewarereads the status from Gin's ownc.Writer.Status()afterc.Next()and never inspectsc.Request.Context().Err():opentelemetry-go-contrib/instrumentation/github.com/gin-gonic/gin/otelgin/gin.go
Lines 113 to 129 in 5f30960
otelgindoes seterror.type, but only fromc.Errors. A client disconnect does not populatec.Errors, so that path does not fire. Because the middleware uses Gin'sResponseWriterrather than a wrapper, there is also no observed response-write error, and the request body is not wrapped, so there is no observed body-read error either. The metric path (sc.RecordMetrics) carriesStatusCodeonly and noerror.type.Reproduction
Confirmed on
mainat 5f30960 with a real HTTP/1.1 disconnect. The handler waits onc.Request.Context().Done()and then writes 500:The client issues the request with a cancellable context and cancels it after 100ms. Recorded span:
No
error.type. Identical outcome to theotelhttpreproduction in #9358.Suggested fix
Mirroring the triage in #9358 (comment), adapted to Gin:
otelginstarts observing those.Errorand add a predictable, low-cardinalityerror.typefor that cause.error.type. Keep the existingc.Errorsclassification working, and decide precedence between ac.Errorsentry and a request-context error.error.typeto the metric attributes recorded bysc.RecordMetrics, per the HTTP metric conventions.Note on shared code
otelgin/internal/semconv/server.gois generated frominternal/shared/semconv/server.go.tmpland is byte-identical to the copy vendored intootelhttp(verified bydiff). The same is true forotelmux,otelechoandotelrestful. So whatevererror.typeclassification lands in the shared template is picked up by all of them, but each middleware still needs its own wiring to detect and pass the failure cause, plus its own regression test.Relevant requirements: HTTP span conventions, HTTP metric conventions, and the dropped-connection example.