From 290ef2c2b92694076c2cb2fc0920f439cb4ec735 Mon Sep 17 00:00:00 2001 From: Norbert Schneider Date: Tue, 16 Jun 2026 08:59:34 +0200 Subject: [PATCH 1/3] feat: support otel --- e2e/integration_test.go | 2 +- exthttpcheck/bandwidth.go | 4 +-- exthttpcheck/bandwidthChecker.go | 17 +++++++---- exthttpcheck/bandwidth_test.go | 6 ++-- exthttpcheck/check.go | 5 ++-- exthttpcheck/check_test.go | 2 +- exthttpcheck/fixAmount.go | 4 +-- exthttpcheck/httpchecker.go | 13 +++++++-- exthttpcheck/httpchecker_test.go | 4 +-- exthttpcheck/periodically.go | 4 +-- go.mod | 20 ++++++++++++- go.sum | 49 ++++++++++++++++++++++++++++++-- main.go | 3 ++ 13 files changed, 108 insertions(+), 25 deletions(-) diff --git a/e2e/integration_test.go b/e2e/integration_test.go index 9a86480..fea528c 100644 --- a/e2e/integration_test.go +++ b/e2e/integration_test.go @@ -188,7 +188,7 @@ func runHTTPCheckTests(actionID string, buildConfig func(tt testcase) map[string assert.Empty(t, metric.Metric["error"], "expected no error") assert.Equal(t, "200", metric.Metric["http_status"]) } else if tt.wantedFailure == "" { - assert.True(t, strings.Contains(metric.Metric["error"], "i/o timeout") || strings.Contains(metric.Metric["error"], "context deadline exceeded")) + assert.True(t, strings.Contains(metric.Metric["error"], "i/o timeout") || strings.Contains(metric.Metric["error"], "context deadline exceeded") || strings.Contains(metric.Metric["error"], "request canceled")) } else { assert.Contains(t, metric.Metric["error"], tt.wantedFailure) } diff --git a/exthttpcheck/bandwidth.go b/exthttpcheck/bandwidth.go index 6bf0163..d1aa368 100644 --- a/exthttpcheck/bandwidth.go +++ b/exthttpcheck/bandwidth.go @@ -225,7 +225,7 @@ func (a *httpCheckActionBandwidth) Describe() action_kit_api.ActionDescription { return description } -func (a *httpCheckActionBandwidth) Prepare(_ context.Context, state *BandwidthCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { +func (a *httpCheckActionBandwidth) Prepare(ctx context.Context, state *BandwidthCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { // Parse URL urlString, ok := request.Config["url"] if !ok { @@ -301,7 +301,7 @@ func (a *httpCheckActionBandwidth) Prepare(_ context.Context, state *BandwidthCh }, nil } - bandwidthCheckers.Store(state.ExecutionID, newBandwidthChecker(state)) + bandwidthCheckers.Store(state.ExecutionID, newBandwidthChecker(ctx, state)) return nil, nil } diff --git a/exthttpcheck/bandwidthChecker.go b/exthttpcheck/bandwidthChecker.go index d144090..251d5d1 100644 --- a/exthttpcheck/bandwidthChecker.go +++ b/exthttpcheck/bandwidthChecker.go @@ -20,6 +20,8 @@ import ( "github.com/rs/zerolog/log" "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/extension-kit/extbuild" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/trace" ) type bandwidthChecker struct { @@ -40,7 +42,8 @@ type bandwidthChecker struct { counterRequestsCompleted atomic.Uint64 counterRequestsErrored atomic.Uint64 - // Control + // Control. ctx carries the action's OTel span context so probe spans are + // children of the action's server span, and cancelling it stops the workers. ctx context.Context cancel context.CancelFunc state *BandwidthCheckState @@ -48,8 +51,9 @@ type bandwidthChecker struct { var bandwidthCheckers = sync.Map{} -func newBandwidthChecker(state *BandwidthCheckState) *bandwidthChecker { - ctx, cancel := context.WithCancel(context.Background()) +func newBandwidthChecker(parentCtx context.Context, state *BandwidthCheckState) *bandwidthChecker { + spanCtx := trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(parentCtx)) + ctx, cancel := context.WithCancel(spanCtx) return &bandwidthChecker{ ctx: ctx, cancel: cancel, @@ -97,8 +101,11 @@ func (c *bandwidthChecker) performBandwidthRequests() { ResponseHeaderTimeout: c.state.ReadTimeout, } // Don't set client.Timeout - it would limit the entire request including body read - // For bandwidth testing, we want to allow large downloads to complete - client := http.Client{Transport: transport} + // For bandwidth testing, we want to allow large downloads to complete. + // otelhttp.NewTransport injects traceparent/baggage into outgoing requests and + // creates a client span per probe. High-volume bandwidth runs should control + // span volume via the standard OTEL sampler env vars. + client := http.Client{Transport: otelhttp.NewTransport(transport)} if !c.state.FollowRedirects { client.CheckRedirect = func(req *http.Request, via []*http.Request) error { diff --git a/exthttpcheck/bandwidth_test.go b/exthttpcheck/bandwidth_test.go index fa8711e..281ea43 100644 --- a/exthttpcheck/bandwidth_test.go +++ b/exthttpcheck/bandwidth_test.go @@ -371,7 +371,7 @@ func TestBandwidthChecker_WindowClassification(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := newBandwidthChecker(tt.state) + c := newBandwidthChecker(context.Background(), tt.state) c.windowStartTime = time.Now().Add(-1 * time.Second) c.windowBytesDownloaded = tt.bytesDownloaded c.windowErrorCount = tt.errorCount @@ -396,7 +396,7 @@ func TestBandwidthCheckAction_AllRequestsFailingFailsCheck(t *testing.T) { state.ExecutionID = uuid.New() state.SuccessRate = 100 - checker := newBandwidthChecker(&state) + checker := newBandwidthChecker(context.Background(), &state) checker.counterWindowSuccess.Store(5) checker.counterRequestsErrored.Store(5) bandwidthCheckers.Store(state.ExecutionID, checker) @@ -417,7 +417,7 @@ func TestBandwidthCheckAction_InFlightDownloadDoesNotFalselyFail(t *testing.T) { state.ExecutionID = uuid.New() state.SuccessRate = 100 - checker := newBandwidthChecker(&state) + checker := newBandwidthChecker(context.Background(), &state) checker.counterWindowSuccess.Store(5) bandwidthCheckers.Store(state.ExecutionID, checker) diff --git a/exthttpcheck/check.go b/exthttpcheck/check.go index 3ce2de9..0b25bef 100644 --- a/exthttpcheck/check.go +++ b/exthttpcheck/check.go @@ -5,6 +5,7 @@ package exthttpcheck import ( + "context" "fmt" "net/url" "sync" @@ -41,7 +42,7 @@ type HTTPCheckState struct { InsecureSkipVerify bool } -func prepare(request action_kit_api.PrepareActionRequestBody, state *HTTPCheckState) (*action_kit_api.PrepareResult, error) { +func prepare(ctx context.Context, request action_kit_api.PrepareActionRequestBody, state *HTTPCheckState) (*action_kit_api.PrepareResult, error) { state.Timeout = time.Now().Add(time.Duration(extutil.ToInt64(request.Config["duration"])) * time.Millisecond) expectedStatusCodes, statusCodeErr := resolveStatusCodeExpression(extutil.ToString(request.Config["statusCode"])) if statusCodeErr != nil { @@ -90,7 +91,7 @@ func prepare(request action_kit_api.PrepareActionRequestBody, state *HTTPCheckSt } state.URL = *parsedUrl - checker := newHttpChecker(state) + checker := newHttpChecker(ctx, state) httpCheckers.Store(state.ExecutionID, checker) return nil, nil diff --git a/exthttpcheck/check_test.go b/exthttpcheck/check_test.go index f14345f..68a0c08 100644 --- a/exthttpcheck/check_test.go +++ b/exthttpcheck/check_test.go @@ -110,7 +110,7 @@ func TestAction_Prepare(t *testing.T) { state := HTTPCheckState{} request := tt.requestBody //When - _, err := prepare(request, &state) + _, err := prepare(context.Background(), request, &state) //Then if tt.wantedError != nil { diff --git a/exthttpcheck/fixAmount.go b/exthttpcheck/fixAmount.go index 195aade..96a0a74 100644 --- a/exthttpcheck/fixAmount.go +++ b/exthttpcheck/fixAmount.go @@ -130,7 +130,7 @@ func (l *httpCheckActionFixedAmount) Describe() action_kit_api.ActionDescription return description } -func (l *httpCheckActionFixedAmount) Prepare(_ context.Context, state *HTTPCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { +func (l *httpCheckActionFixedAmount) Prepare(ctx context.Context, state *HTTPCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { duration := time.Duration(extutil.ToInt64(request.Config["duration"])) * time.Millisecond if duration <= 0 { return nil, errors.New("duration must be greater than 0") @@ -151,7 +151,7 @@ func (l *httpCheckActionFixedAmount) Prepare(_ context.Context, state *HTTPCheck }, nil } - return prepare(request, state) + return prepare(ctx, request, state) } func (l *httpCheckActionFixedAmount) Start(_ context.Context, state *HTTPCheckState) (*action_kit_api.StartResult, error) { diff --git a/exthttpcheck/httpchecker.go b/exthttpcheck/httpchecker.go index d380354..384b407 100644 --- a/exthttpcheck/httpchecker.go +++ b/exthttpcheck/httpchecker.go @@ -20,6 +20,8 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/steadybit/action-kit/go/action_kit_api/v2" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/trace" ) type counters struct { @@ -42,8 +44,11 @@ type httpChecker struct { httpClient http.Client } -func newHttpChecker(state *HTTPCheckState) *httpChecker { +func newHttpChecker(parentCtx context.Context, state *HTTPCheckState) *httpChecker { + // Preserve the agent's OTel span context as the parent for probe spans, but + // decouple from parentCtx's lifetime (it is cancelled when Prepare returns). ctx, cancel := context.WithCancel(context.Background()) + ctx = trace.ContextWithSpanContext(ctx, trace.SpanContextFromContext(parentCtx)) checker := &httpChecker{ work: make(chan struct{}, state.MaxConcurrent), ctx: ctx, @@ -190,7 +195,11 @@ func createHttpClient(state *HTTPCheckState) http.Client { InsecureSkipVerify: state.InsecureSkipVerify, }, } - client := http.Client{Timeout: state.ReadTimeout, Transport: transport} + // otelhttp.NewTransport injects traceparent/baggage into outgoing requests and + // creates a client span per probe. High-volume checks should control span volume + // via the standard OTEL sampler env vars (OTEL_TRACES_SAMPLER=parentbased_traceidratio, + // OTEL_TRACES_SAMPLER_ARG=); no per-extension tuning is applied here. + client := http.Client{Timeout: state.ReadTimeout, Transport: otelhttp.NewTransport(transport)} if !state.FollowRedirects { client.CheckRedirect = func(req *http.Request, via []*http.Request) error { diff --git a/exthttpcheck/httpchecker_test.go b/exthttpcheck/httpchecker_test.go index de57b4c..44070bf 100644 --- a/exthttpcheck/httpchecker_test.go +++ b/exthttpcheck/httpchecker_test.go @@ -36,7 +36,7 @@ func TestHttpChecker_ExecutesExactlyMaxRequests(t *testing.T) { ConnectionTimeout: 5 * time.Second, } - checker := newHttpChecker(state) + checker := newHttpChecker(context.Background(), state) checker.start() assert.Eventually(t, func() bool { @@ -70,7 +70,7 @@ func TestHttpChecker_ShutdownCancelsInFlightRequests(t *testing.T) { ConnectionTimeout: 5 * time.Second, } - checker := newHttpChecker(state) + checker := newHttpChecker(context.Background(), state) checker.start() time.Sleep(200 * time.Millisecond) diff --git a/exthttpcheck/periodically.go b/exthttpcheck/periodically.go index 6008d9f..6ed14e2 100644 --- a/exthttpcheck/periodically.go +++ b/exthttpcheck/periodically.go @@ -125,7 +125,7 @@ func getDelayBetweenRequests(requestsPerSecond uint64) time.Duration { return time.Second } -func (l *httpCheckActionPeriodically) Prepare(_ context.Context, state *HTTPCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { +func (l *httpCheckActionPeriodically) Prepare(ctx context.Context, state *HTTPCheckState, request action_kit_api.PrepareActionRequestBody) (*action_kit_api.PrepareResult, error) { requestsPerSecond := extutil.ToUInt64(request.Config["requestsPerSecond"]) state.DelayBetweenRequests = getDelayBetweenRequests(requestsPerSecond) if state.DelayBetweenRequests < time.Millisecond { @@ -135,7 +135,7 @@ func (l *httpCheckActionPeriodically) Prepare(_ context.Context, state *HTTPChec }, }, nil } - return prepare(request, state) + return prepare(ctx, request, state) } func (l *httpCheckActionPeriodically) Start(_ context.Context, state *HTTPCheckState) (*action_kit_api.StartResult, error) { diff --git a/go.mod b/go.mod index f3d07b5..9077f8b 100644 --- a/go.mod +++ b/go.mod @@ -12,19 +12,25 @@ require ( github.com/steadybit/action-kit/go/action_kit_test v1.4.7 github.com/steadybit/discovery-kit/go/discovery_kit_api v1.7.1 github.com/steadybit/discovery-kit/go/discovery_kit_sdk v1.3.5 - github.com/steadybit/extension-kit v1.10.5 + github.com/steadybit/extension-kit v1.10.5-0.20260616064401-60139058dfcf github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 + go.opentelemetry.io/otel/trace v1.43.0 ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.134.0 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/swag v0.25.4 // indirect @@ -42,6 +48,7 @@ require ( github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.6 // indirect @@ -68,6 +75,14 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/zmwangx/debounce v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.55.0 // indirect @@ -77,6 +92,9 @@ require ( golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 09f3477..012aca1 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -18,12 +22,17 @@ github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU= github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= @@ -64,6 +73,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -77,6 +88,8 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -163,8 +176,8 @@ github.com/steadybit/discovery-kit/go/discovery_kit_sdk v1.3.5 h1:1yT/4Aw+lQvYXS github.com/steadybit/discovery-kit/go/discovery_kit_sdk v1.3.5/go.mod h1:gOPfHZ3hw/7TMxEYCCTYnOz9tZ2ybNYa+MKmUbPXKIQ= github.com/steadybit/discovery-kit/go/discovery_kit_test v1.2.1 h1:CabRtfE70gt/4H/TgL/TRm54OkxWKbmPhTX2qEhzKZ4= github.com/steadybit/discovery-kit/go/discovery_kit_test v1.2.1/go.mod h1:PPJh5gSdVRKG/0qJCGJK5XnGxXat/v6UT8/2ilIbbX8= -github.com/steadybit/extension-kit v1.10.5 h1:KSZP2vUA97QnFDhI3UAAmNg1iOv4n0ckXI/jjKrB3xc= -github.com/steadybit/extension-kit v1.10.5/go.mod h1:n1PMz8AwjGvl/M/CWSVjoHCE8qM74Tx+nfC4iiuhEPA= +github.com/steadybit/extension-kit v1.10.5-0.20260616064401-60139058dfcf h1:YsehAhKGw+e8DF4EMNRKPdTozcslNxHA99s6miLbK0g= +github.com/steadybit/extension-kit v1.10.5-0.20260616064401-60139058dfcf/go.mod h1:bmShTtQS4U1ft/DDI/ylkaUrDYaVkNE3355T8maryd4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= @@ -181,6 +194,30 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJx github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/zmwangx/debounce v1.0.0 h1:Dyf+WfLESjc2bqFKHgI1dZTW9oh6CJm8SBDkhXrwLB4= github.com/zmwangx/debounce v1.0.0/go.mod h1:U+/QHt+bSMdUh8XKOb6U+MQV5Ew4eS8M3ua5WJ7Ns6I= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -204,6 +241,14 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index f2ba999..97780a5 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/steadybit/extension-kit/exthealth" "github.com/steadybit/extension-kit/exthttp" "github.com/steadybit/extension-kit/extlogging" + "github.com/steadybit/extension-kit/extotel" "github.com/steadybit/extension-kit/extruntime" "github.com/steadybit/extension-kit/extsignals" ) @@ -35,6 +36,8 @@ func main() { // - to set the log level to debug, set the environment variable STEADYBIT_LOG_LEVEL="debug" extlogging.InitZeroLog() + extotel.InitOpenTelemetry() + // Build information is set at compile-time. This line writes the build information to the log. // The information is mostly handy for debugging purposes. extbuild.PrintBuildInformation() From c952fcbabdc15c510bff49554ecf4e61b8bdc50c Mon Sep 17 00:00:00 2001 From: Norbert Schneider Date: Wed, 17 Jun 2026 09:39:42 +0200 Subject: [PATCH 2/3] fix: guard requestTracer timing fields against data race The httptrace callbacks populating requestTracer run on net/http's internal writeLoop and readLoop goroutines, which can execute concurrently with the worker goroutine that reads the timings once Do returns. Wrapping the transport with otelhttp widened that window enough for the race detector to fail the audit build reliably. Protect the time fields with a mutex so timing reads are safe regardless of which goroutine the callbacks fire on. --- exthttpcheck/httpchecker.go | 2 +- exthttpcheck/requestTracer.go | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/exthttpcheck/httpchecker.go b/exthttpcheck/httpchecker.go index 384b407..9f203b2 100644 --- a/exthttpcheck/httpchecker.go +++ b/exthttpcheck/httpchecker.go @@ -239,7 +239,7 @@ func (c *httpChecker) onResponse(req *http.Request, res *http.Response, tracer * "response_time_constraints_fulfilled": strconv.FormatBool(responseTimeWasSuccessful), }, Value: float64(tracer.responseTime().Milliseconds()), - Timestamp: tracer.firstByteReceived, + Timestamp: tracer.firstByteReceivedTime(), } if responseStatusWasExpected && responseBodyWasSuccessful && responseTimeWasSuccessful { diff --git a/exthttpcheck/requestTracer.go b/exthttpcheck/requestTracer.go index 0a9fe40..f96a0e6 100644 --- a/exthttpcheck/requestTracer.go +++ b/exthttpcheck/requestTracer.go @@ -5,26 +5,45 @@ package exthttpcheck import ( "net/http/httptrace" + "sync" "time" ) +// requestTracer records request/response timing via httptrace callbacks. The +// callbacks are invoked from net/http's internal goroutines (WroteRequest from +// the connection's writeLoop, GotFirstResponseByte from the readLoop), which can +// run concurrently with the worker goroutine reading the timings after Do +// returns. The mutex guards the time fields against that data race. type requestTracer struct { httptrace.ClientTrace + mu sync.Mutex requestWritten, firstByteReceived time.Time } -func (t requestTracer) responseTime() time.Duration { +func (t *requestTracer) responseTime() time.Duration { + t.mu.Lock() + defer t.mu.Unlock() return t.firstByteReceived.Sub(t.requestWritten) } +func (t *requestTracer) firstByteReceivedTime() time.Time { + t.mu.Lock() + defer t.mu.Unlock() + return t.firstByteReceived +} + func newRequestTracer() *requestTracer { t := &requestTracer{} t.ClientTrace = httptrace.ClientTrace{ WroteRequest: func(info httptrace.WroteRequestInfo) { + t.mu.Lock() + defer t.mu.Unlock() t.requestWritten = time.Now() }, GotFirstResponseByte: func() { + t.mu.Lock() + defer t.mu.Unlock() t.firstByteReceived = time.Now() }, } From 34d975fa43864b822d934795e331916fda19a7de Mon Sep 17 00:00:00 2001 From: Norbert Schneider Date: Wed, 17 Jun 2026 09:40:05 +0200 Subject: [PATCH 3/3] chore: push PR docker images --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ae4fd..e681b05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: with: go_version: '^1.26.4' build_linux_packages: true + force_push_docker_image: true VERSION_BUMPER_APPID: ${{ vars.GH_APP_STEADYBIT_APP_ID }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}