client: end the otelhttp span after a successful hijack

setupHijackConn only closed the response body when the upgrade failed.
otelhttp ends its client span when the body is closed, so the span of
every successful ContainerAttach, ContainerExecAttach and DialHijack
call was never ended. A 101 response has no body, so closing it does not
touch the hijacked connection.

Signed-off-by: berce <75228263+bercedev@users.noreply.github.com>
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
berce authored and Paweł Gronowski committed 2026-10-29 11:29:10 +01:00
1 parent 9f07d9222f
commit 7dcd61b4a8
3 files changed
+44 -3

No files matched your search

+3 -1
View File
@@ -100,8 +100,10 @@ func (cli *Client) setupHijackConn(req *http.Request, proto string) (retConn net
if err != nil {
return nil, "", err
}
// Closing the body ends the otelhttp span. A 101 response has no body,
// so this does not close the connection.
_ = resp.Body.Close()
if resp.StatusCode != http.StatusSwitchingProtocols {
_ = resp.Body.Close()
return nil, "", fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
}
+38 -1
View File
@@ -10,9 +10,12 @@ import (
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@@ -132,7 +135,7 @@ func TestHijackCancellationOwnership(t *testing.T) {
}
}
func TestHijackHooks(t *testing.T) {
func TestHijackHooksAndTracing(t *testing.T) {
const (
reqHeaderKey = "X-Test-Request"
reqHeaderValue = "request"
@@ -162,9 +165,11 @@ func TestHijackHooks(t *testing.T) {
serverURL, err := url.Parse(ts.URL)
assert.NilError(t, err)
tp := &spanCounter{}
var gotResponseHeader string
c, err := New(
WithHost("tcp://"+serverURL.Host),
WithTraceProvider(tp),
WithHTTPRequestHook(func(req *http.Request) error {
req.Header.Set(reqHeaderKey, reqHeaderValue)
return nil
@@ -180,6 +185,38 @@ func TestHijackHooks(t *testing.T) {
defer func() { _ = conn.Close() }()
assert.Equal(t, gotResponseHeader, respHeaderValue)
assert.Check(t, is.Equal(tp.started.Load(), int32(1)))
assert.Check(t, is.Equal(tp.ended.Load(), int32(1)))
}
// spanCounter counts the spans started and ended through it.
type spanCounter struct {
noop.TracerProvider
started, ended atomic.Int32
}
func (p *spanCounter) Tracer(string, ...trace.TracerOption) trace.Tracer {
return countingTracer{p: p}
}
type countingTracer struct {
noop.Tracer
p *spanCounter
}
func (t countingTracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
t.p.started.Add(1)
ctx, span := t.Tracer.Start(ctx, name, opts...)
return ctx, countingSpan{Span: span, p: t.p}
}
type countingSpan struct {
trace.Span
p *spanCounter
}
func (s countingSpan) End(...trace.SpanEndOption) {
s.p.ended.Add(1)
}
func TestTLSCloseWriter(t *testing.T) {
+3 -1
View File
@@ -100,8 +100,10 @@ func (cli *Client) setupHijackConn(req *http.Request, proto string) (retConn net
if err != nil {
return nil, "", err
}
// Closing the body ends the otelhttp span. A 101 response has no body,
// so this does not close the connection.
_ = resp.Body.Close()
if resp.StatusCode != http.StatusSwitchingProtocols {
_ = resp.Body.Close()
return nil, "", fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
}