diff --git a/cache/jetstreamkv/jetstream.go b/cache/jetstreamkv/jetstream.go index 5f6fc27..1dd529a 100644 --- a/cache/jetstreamkv/jetstream.go +++ b/cache/jetstreamkv/jetstream.go @@ -42,6 +42,7 @@ func New(opts ...cache.Option) (cache.RawCache, error) { js, err := natsConn.JetStream() if err != nil { + natsConn.Close() return nil, err } // Create the client @@ -58,15 +59,18 @@ func New(opts ...cache.Option) (cache.RawCache, error) { // If the bucket already exists, just get a handle to it. client, err = js.KeyValue(cacheOpts.Name) if err != nil { + natsConn.Close() return nil, err } } else { // Another error occurred during creation. + natsConn.Close() return nil, err } } if _, err = client.Status(); err != nil { + natsConn.Close() return nil, err } diff --git a/cache/redis/redis.go b/cache/redis/redis.go index 880bfe1..c0fc05c 100644 --- a/cache/redis/redis.go +++ b/cache/redis/redis.go @@ -41,6 +41,7 @@ func New(opts ...cache.Option) (cache.RawCache, error) { err = client.Ping(ctx).Err() if err != nil { + _ = client.Close() return nil, err } diff --git a/events/events_test.go b/events/events_test.go index 611d108..3f075e3 100644 --- a/events/events_test.go +++ b/events/events_test.go @@ -3,6 +3,7 @@ package events_test import ( "context" "fmt" + "sync/atomic" "testing" "time" @@ -23,7 +24,7 @@ type EventsTestSuite struct { type MessageToTest struct { Service *frame.Service - Count int + Count atomic.Int64 } func (event *MessageToTest) Name() string { @@ -46,7 +47,7 @@ func (event *MessageToTest) Execute(ctx context.Context, payload any) error { message := *m logger := event.Service.Log(ctx).WithField("payload", message).WithField("type", event.Name()) logger.Info("handling event") - event.Count++ + event.Count.Add(1) return nil } @@ -200,7 +201,8 @@ func (s *EventsTestSuite) TestServiceEventsPublishingWorks() { frametests.WithNoopDriver(), ) - testEvent := MessageToTest{Service: svc, Count: tc.initialCount} + testEvent := MessageToTest{Service: svc} + testEvent.Count.Store(int64(tc.initialCount)) events := frame.WithRegisterEvents(&testEvent) svc.Init(ctx, events) @@ -215,8 +217,8 @@ func (s *EventsTestSuite) TestServiceEventsPublishingWorks() { time.Sleep(2 * time.Second) require.Equal( t, - tc.expectedCount, - testEvent.Count, + int64(tc.expectedCount), + testEvent.Count.Load(), "event should be processed and count incremented", ) } diff --git a/events/manager.go b/events/manager.go index e89ba1b..79cfb57 100644 --- a/events/manager.go +++ b/events/manager.go @@ -3,6 +3,7 @@ package events import ( "context" "errors" + "sync" "github.com/pitabwire/util" @@ -14,14 +15,19 @@ type manager struct { qm queue.Manager cfg config.ConfigurationEvents + mu sync.RWMutex eventRegistry map[string]EventI } func (m *manager) Add(evt EventI) { + m.mu.Lock() + defer m.mu.Unlock() m.eventRegistry[evt.Name()] = evt } func (m *manager) Get(eventName string) (EventI, error) { + m.mu.RLock() + defer m.mu.RUnlock() evt, ok := m.eventRegistry[eventName] if !ok { return nil, errors.New("event not found in registry: " + eventName) diff --git a/frametests/driver.go b/frametests/driver.go index 5be333f..f955560 100644 --- a/frametests/driver.go +++ b/frametests/driver.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/http/httptest" + "sync" "github.com/pitabwire/util" @@ -28,25 +29,34 @@ func GetFreePort(ctx context.Context) (int, error) { } type testDriver struct { + mu sync.RWMutex srv *httptest.Server } func (t *testDriver) ListenAndServe(_ string, h http.Handler) error { + t.mu.Lock() + defer t.mu.Unlock() t.srv = httptest.NewServer(h) return nil } func (t *testDriver) ListenAndServeTLS(_, _, _ string, h http.Handler) error { + t.mu.Lock() + defer t.mu.Unlock() t.srv = httptest.NewTLSServer(h) return nil } func (t *testDriver) Shutdown(_ context.Context) error { + t.mu.RLock() + defer t.mu.RUnlock() t.srv.Close() return nil } func (t *testDriver) GetTestServer() *httptest.Server { + t.mu.RLock() + defer t.mu.RUnlock() return t.srv } diff --git a/go.mod b/go.mod index a9ffd09..b4da1b6 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,11 @@ require ( github.com/exaring/otelpgx v0.10.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.8.0 - github.com/lmittmann/tint v1.1.2 + github.com/lmittmann/tint v1.1.3 github.com/nats-io/nats.go v1.48.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/panjf2000/ants/v2 v2.11.4 - github.com/pitabwire/natspubsub v0.7.11 + github.com/pitabwire/natspubsub v0.7.12 github.com/pitabwire/util v0.4.0 github.com/redis/go-redis/v9 v9.17.3 github.com/rs/xid v1.6.0 @@ -28,17 +28,17 @@ require ( github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 github.com/testcontainers/testcontainers-go/modules/valkey v0.40.0 github.com/valkey-io/valkey-go v1.0.71 - go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 - go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 - go.opentelemetry.io/otel v1.39.0 - go.opentelemetry.io/otel/log v0.15.0 - go.opentelemetry.io/otel/metric v1.39.0 - go.opentelemetry.io/otel/sdk v1.39.0 - go.opentelemetry.io/otel/sdk/log v0.15.0 - go.opentelemetry.io/otel/sdk/metric v1.39.0 - go.opentelemetry.io/otel/trace v1.39.0 + go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/log v0.16.0 + go.opentelemetry.io/otel/metric v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/sdk/log v0.16.0 + go.opentelemetry.io/otel/sdk/metric v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 gocloud.dev v0.44.0 golang.org/x/net v0.49.0 golang.org/x/oauth2 v0.34.0 @@ -76,7 +76,7 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/cel-go v0.27.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/gax-go/v2 v2.16.0 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -114,32 +114,32 @@ require ( github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.61.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.264.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7c855cd..1171f84 100644 --- a/go.sum +++ b/go.sum @@ -98,16 +98,16 @@ github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= -github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= -github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= -github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -132,8 +132,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= -github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= @@ -164,8 +164,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.12.3 h1:KRv+1n7lddMVgkJPQer+pt36TcO0ENxjilBmeWdjcHs= -github.com/nats-io/nats-server/v2 v2.12.3/go.mod h1:MQXjG9WjyXKz9koWzUc3jYUMKD8x3CLmTNy91IQQz3Y= +github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= +github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= @@ -182,8 +182,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/panjf2000/ants/v2 v2.11.4 h1:UJQbtN1jIcI5CYNocTj0fuAUYvsLjPoYi0YuhqV/Y48= github.com/panjf2000/ants/v2 v2.11.4/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= -github.com/pitabwire/natspubsub v0.7.11 h1:JF5YA4ja0MMvTgKKK7WprXPKztomZGr3OgtU3XelHxA= -github.com/pitabwire/natspubsub v0.7.11/go.mod h1:zWlwRkjaiHxGjygTolthEjuRpaartLDChQ3hM28r3MA= +github.com/pitabwire/natspubsub v0.7.12 h1:C2LlsLr9EHH3VtbFV8dZRjPbCjhd6Pq7odOw+f8dSTc= +github.com/pitabwire/natspubsub v0.7.12/go.mod h1:IdDbxIsKJDPU77VCD11YtMnfxWRO5SStlNXbNMZpvJc= github.com/pitabwire/util v0.4.0 h1:P2NNbrPaMVecGMnaVuoDURe5pUhVtVM/tb3s1VUADY4= github.com/pitabwire/util v0.4.0/go.mod h1:dT1AZyJRyjERodwQKCeht2R5wi8yCYtElW+k0GzAPZU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -241,64 +241,64 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= -go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= -go.opentelemetry.io/contrib/bridges/prometheus v0.64.0 h1:7TYhBCu6Xz6vDJGNtEslWZLuuX2IJ/aH50hBY4MVeUg= -go.opentelemetry.io/contrib/bridges/prometheus v0.64.0/go.mod h1:tHQctZfAe7e4PBPGyt3kae6mQFXNpj+iiDJa3ithM50= -go.opentelemetry.io/contrib/exporters/autoexport v0.64.0 h1:9pzPj3RFyKOxBAMkM2w84LpT+rdHam1XoFA+QhARiRw= -go.opentelemetry.io/contrib/exporters/autoexport v0.64.0/go.mod h1:hlVZx1btWH0XTfXpuGX9dsquB50s+tc3fYFOO5elo2M= +go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4= +go.opentelemetry.io/contrib/bridges/otelslog v0.15.0/go.mod h1:CvaNVqIfcybc+7xqZNubbE+26K6P7AKZF/l0lE2kdCk= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= -go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA= -go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE= -go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo= -go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY= -go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= -go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY= -go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo= -go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8= -go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg= -go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0= -go.opentelemetry.io/otel/exporters/prometheus v0.61.0/go.mod h1:iivMuj3xpR2DkUrUya3TPS/Z9h3dz7h01GxU+fQBRNg= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s= +go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE= +go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04= +go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos= +go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg= +go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU= +go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0= +go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I= +go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -313,8 +313,8 @@ gocloud.dev v0.44.0 h1:iVyMAqFl2r6xUy7M4mfqwlN+21UpJoEtgHEcfiLMUXs= gocloud.dev v0.44.0/go.mod h1:ZmjROXGdC/eKZLF1N+RujDlFRx3D+4Av2thREKDMVxY= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= -golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -339,12 +339,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.264.0 h1:+Fo3DQXBK8gLdf8rFZ3uLu39JpOnhvzJrLMQSoSYZJM= google.golang.org/api v0.264.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/options_datastore.go b/options_datastore.go index 57e9f26..2d16810 100644 --- a/options_datastore.go +++ b/options_datastore.go @@ -3,8 +3,6 @@ package frame import ( "context" - "github.com/pitabwire/util" - "github.com/pitabwire/frame/config" "github.com/pitabwire/frame/datastore" "github.com/pitabwire/frame/datastore/manager" @@ -19,7 +17,8 @@ func WithDatastoreManager() Option { var err error s.datastoreManager, err = manager.NewManager(ctx) if err != nil { - util.Log(ctx).WithError(err).Fatal("error initiating datastore manager") + s.AddStartupError(err) + return } // Register cleanup method @@ -62,7 +61,7 @@ func WithDatastoreConnectionWithOptions(name string, opts ...pool.Option) Option err := dbPool.AddConnection(ctx, opts...) if err != nil { - util.Log(ctx).WithError(err).Fatal("error initiating datastore connection") + s.AddStartupError(err) } } } diff --git a/options_queue.go b/options_queue.go index 2e57b31..c3e470c 100644 --- a/options_queue.go +++ b/options_queue.go @@ -2,6 +2,8 @@ package frame import ( "context" + "errors" + "fmt" "strings" _ "github.com/pitabwire/natspubsub" // required for NATS pubsub driver registration @@ -13,15 +15,17 @@ import ( // WithRegisterPublisher Option to register publishing path referenced within the system. func WithRegisterPublisher(reference string, queueURL string) Option { - // Validate inputs immediately - fail fast - if strings.TrimSpace(reference) == "" { - panic("publisher reference cannot be empty") - } - if !data.DSN(queueURL).Valid() { - panic("publisher queueURL cannot be invalid") - } - return func(_ context.Context, s *Service) { + // Validate inputs and report via startup errors instead of panicking + if strings.TrimSpace(reference) == "" { + s.AddStartupError(errors.New("publisher reference cannot be empty")) + return + } + if !data.DSN(queueURL).Valid() { + s.AddStartupError(fmt.Errorf("publisher queueURL is invalid: %s", queueURL)) + return + } + // QueueManager manager is initialized after options are applied, // so defer registration to pre-start phase // Publishers must be registered before subscribers (for mem:// driver) @@ -41,15 +45,17 @@ func WithRegisterPublisher(reference string, queueURL string) Option { // WithRegisterSubscriber Option to register a new subscription handlers. func WithRegisterSubscriber(reference string, queueURL string, handlers ...queue.SubscribeWorker) Option { - // Validate inputs immediately - fail fast - if strings.TrimSpace(reference) == "" { - panic("subscriber reference cannot be empty") - } - if !data.DSN(queueURL).Valid() { - panic("subscriber queueURL cannot be invalid") - } - return func(_ context.Context, s *Service) { + // Validate inputs and report via startup errors instead of panicking + if strings.TrimSpace(reference) == "" { + s.AddStartupError(errors.New("subscriber reference cannot be empty")) + return + } + if !data.DSN(queueURL).Valid() { + s.AddStartupError(fmt.Errorf("subscriber queueURL is invalid: %s", queueURL)) + return + } + // QueueManager manager is initialized after options are applied, // so defer registration to pre-start phase // Subscribers must be registered after publishers (for mem:// driver) diff --git a/options_queue_test.go b/options_queue_test.go index 970c864..c84c758 100644 --- a/options_queue_test.go +++ b/options_queue_test.go @@ -22,53 +22,68 @@ func TestOptionEarlyFailure(t *testing.T) { name string reference string queueURL string - panicMsg string + errMsg string + isSub bool }{ { - name: "WithRegisterPublisher empty reference panics", + name: "WithRegisterPublisher empty reference reports error", reference: "", queueURL: "mem://test", - panicMsg: "publisher reference cannot be empty", + errMsg: "publisher reference cannot be empty", + isSub: false, }, { - name: "WithRegisterPublisher empty queueURL panics", + name: "WithRegisterPublisher invalid queueURL reports error", reference: "test", queueURL: "", - panicMsg: "publisher queueURL cannot be invalid", + errMsg: "publisher queueURL is invalid", + isSub: false, }, { - name: "WithRegisterSubscriber empty reference panics", + name: "WithRegisterSubscriber empty reference reports error", reference: "", queueURL: "mem://test", - panicMsg: "subscriber reference cannot be empty", + errMsg: "subscriber reference cannot be empty", + isSub: true, }, { - name: "WithRegisterSubscriber empty queueURL panics", + name: "WithRegisterSubscriber invalid queueURL reports error", reference: "test", queueURL: "", - panicMsg: "subscriber queueURL cannot be invalid", + errMsg: "subscriber queueURL is invalid", + isSub: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defer func() { - if r := recover(); r != nil { - if r != tt.panicMsg { - t.Errorf("expected panic message '%s', got '%v'", tt.panicMsg, r) - } - } else { - t.Errorf("expected panic with message '%s', but no panic occurred", tt.panicMsg) - } - }() - - if strings.Contains(tt.name, "Publisher") { - _ = frame.WithRegisterPublisher(tt.reference, tt.queueURL) - } else { + var opt frame.Option + if tt.isSub { handler := &msgHandler{f: func(_ context.Context, _ map[string]string, _ []byte) error { return nil }} - _ = frame.WithRegisterSubscriber(tt.reference, tt.queueURL, handler) + opt = frame.WithRegisterSubscriber(tt.reference, tt.queueURL, handler) + } else { + opt = frame.WithRegisterPublisher(tt.reference, tt.queueURL) + } + + ctx, svc := frame.NewService(opt) + defer svc.Stop(ctx) + + errs := svc.GetStartupErrors() + if len(errs) == 0 { + t.Fatalf("expected startup error containing '%s', but got none", tt.errMsg) + } + + found := false + for _, err := range errs { + if err != nil && strings.Contains(err.Error(), tt.errMsg) { + found = true + break + } + } + if !found { + t.Errorf("expected startup error containing '%s', got %v", tt.errMsg, errs) } }) } diff --git a/options_telemetry.go b/options_telemetry.go index 6d1b123..6563525 100644 --- a/options_telemetry.go +++ b/options_telemetry.go @@ -32,7 +32,7 @@ func WithTelemetry(opts ...telemetry.Option) Option { s.telemetryManager = telemetry.NewManager(ctx, cfg, extOpts...) err := s.telemetryManager.Init(ctx) if err != nil { - util.Log(ctx).WithError(err).Fatal("failed to initialize telemetry") + s.AddStartupError(err) } } } diff --git a/options_worker.go b/options_worker.go index 47e143c..be06f44 100644 --- a/options_worker.go +++ b/options_worker.go @@ -24,7 +24,12 @@ func WithWorkerPoolOptions(options ...workerpool.Option) Option { return } - s.workerPoolManager = workerpool.NewManager(ctx, cfg, s.sendStopError, options...) + wpm, err := workerpool.NewManager(ctx, cfg, s.sendStopError, options...) + if err != nil { + s.AddStartupError(err) + return + } + s.workerPoolManager = wpm } } diff --git a/queue/queue_test.go b/queue/queue_test.go index 17b5ad9..7e063ca 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -543,8 +543,8 @@ func (s *QueueTestSuite) TestServiceSubscriberValidateJetstreamMessages() { handler := &msgHandler{ f: func(_ context.Context, _ map[string]string, message []byte) error { var data map[string]any - if err = json.Unmarshal(message, &data); err != nil { - return err + if unmarshalErr := json.Unmarshal(message, &data); unmarshalErr != nil { + return unmarshalErr } mu.Lock() receivedCount++ diff --git a/queue/subscriber.go b/queue/subscriber.go index 31e2df6..b1726e4 100644 --- a/queue/subscriber.go +++ b/queue/subscriber.go @@ -11,10 +11,12 @@ import ( "github.com/pitabwire/util" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" "gocloud.dev/pubsub" "github.com/pitabwire/frame/localization" "github.com/pitabwire/frame/security" + "github.com/pitabwire/frame/telemetry" "github.com/pitabwire/frame/workerpool" ) @@ -32,6 +34,7 @@ type subscriber struct { isInit atomic.Bool state SubscriberState metrics *subscriberMetrics + tracer telemetry.Tracer workManager workerpool.Manager } @@ -227,7 +230,19 @@ func (s *subscriber) processReceivedMessage(ctx context.Context, msg *pubsub.Mes pCtx = util.SetTenancy(pCtx, authClaim) } - pCtx = otel.GetTextMapPropagator().Extract(pCtx, metadata) + // Extract remote span context for linking, not parenting. + // This prevents zombie parent traces from publisher-subscriber propagation. + extractedCtx := otel.GetTextMapPropagator().Extract(pCtx, metadata) + remoteSpanCtx := trace.SpanContextFromContext(extractedCtx) + + var spanOpts []trace.SpanStartOption + spanOpts = append(spanOpts, trace.WithNewRoot()) + if remoteSpanCtx.IsValid() { + spanOpts = append(spanOpts, trace.WithLinks(trace.Link{SpanContext: remoteSpanCtx})) + } + + pCtx, span := s.tracer.Start(pCtx, "process", spanOpts...) + defer func() { s.tracer.End(pCtx, span, err) }() languages := localization.FromMap(metadata) if len(languages) > 0 { @@ -265,6 +280,12 @@ func (s *subscriber) processReceivedMessage(ctx context.Context, msg *pubsub.Mes return nil } +// detachedTraceContext returns a context with an empty span context, +// preventing gocloud.dev's internal tracing from creating spans on every poll. +func detachedTraceContext(ctx context.Context) context.Context { + return trace.ContextWithSpanContext(ctx, trace.SpanContext{}) +} + func (s *subscriber) listen(ctx context.Context) { logger := util.Log(ctx). WithField("name", s.reference). @@ -284,7 +305,7 @@ func (s *subscriber) listen(ctx context.Context) { return } - msg, err := s.Receive(ctx) + msg, err := s.Receive(detachedTraceContext(ctx)) if err != nil { if ctx.Err() != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { // Context cancelled or deadline exceeded, loop again to check ctx.Done() @@ -336,6 +357,7 @@ func newSubscriber( reference: reference, url: queueURL, handlers: handlers, + tracer: telemetry.NewTracer("queue/subscriber/" + reference), metrics: &subscriberMetrics{ ActiveMessages: &atomic.Int64{}, LastActivity: &atomic.Int64{}, diff --git a/queue/validation_test.go b/queue/validation_test.go index a2f88a3..42a21ef 100644 --- a/queue/validation_test.go +++ b/queue/validation_test.go @@ -20,7 +20,10 @@ func (t *testConfig) GetExpiryDuration() time.Duration { return time.Minute } func TestAddSubscriberValidation(t *testing.T) { ctx := context.Background() cfg := &testConfig{} - workPool := workerpool.NewManager(ctx, cfg, func(_ context.Context, _ error) {}) + workPool, err := workerpool.NewManager(ctx, cfg, func(_ context.Context, _ error) {}) + if err != nil { + t.Fatalf("failed to create worker pool manager: %v", err) + } qm := queue.NewQueueManager(ctx, workPool) tests := []struct { @@ -69,18 +72,18 @@ func TestAddSubscriberValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := qm.AddSubscriber(ctx, tt.reference, tt.queueURL) + subErr := qm.AddSubscriber(ctx, tt.reference, tt.queueURL) if tt.expectError { - if err == nil { + if subErr == nil { t.Errorf("expected error but got none") return } - if err.Error() != tt.errorMsg { - t.Errorf("expected error message '%s', got '%s'", tt.errorMsg, err.Error()) + if subErr.Error() != tt.errorMsg { + t.Errorf("expected error message '%s', got '%s'", tt.errorMsg, subErr.Error()) } - } else if err != nil { - t.Errorf("expected no error but got: %v", err) + } else if subErr != nil { + t.Errorf("expected no error but got: %v", subErr) } }) } @@ -89,7 +92,10 @@ func TestAddSubscriberValidation(t *testing.T) { func TestAddPublisherValidation(t *testing.T) { ctx := context.Background() cfg := &testConfig{} - workPool := workerpool.NewManager(ctx, cfg, func(_ context.Context, _ error) {}) + workPool, err := workerpool.NewManager(ctx, cfg, func(_ context.Context, _ error) {}) + if err != nil { + t.Fatalf("failed to create worker pool manager: %v", err) + } qm := queue.NewQueueManager(ctx, workPool) tests := []struct { @@ -138,18 +144,18 @@ func TestAddPublisherValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := qm.AddPublisher(ctx, tt.reference, tt.queueURL) + pubErr := qm.AddPublisher(ctx, tt.reference, tt.queueURL) if tt.expectError { - if err == nil { + if pubErr == nil { t.Errorf("expected error but got none") return } - if err.Error() != tt.errorMsg { - t.Errorf("expected error message '%s', got '%s'", tt.errorMsg, err.Error()) + if pubErr.Error() != tt.errorMsg { + t.Errorf("expected error message '%s', got '%s'", tt.errorMsg, pubErr.Error()) } - } else if err != nil { - t.Errorf("expected no error but got: %v", err) + } else if pubErr != nil { + t.Errorf("expected no error but got: %v", pubErr) } }) } diff --git a/security/interfaces.go b/security/interfaces.go index f903446..98fe555 100644 --- a/security/interfaces.go +++ b/security/interfaces.go @@ -65,4 +65,6 @@ type Manager interface { GetOauth2ClientRegistrar(ctx context.Context) Oauth2ClientRegistrar GetAuthenticator(ctx context.Context) Authenticator GetAuthorizer(ctx context.Context) Authorizer + // Close releases resources held by the security manager and its components. + Close() } diff --git a/security/manager/security_manager.go b/security/manager/security_manager.go index 4b82849..161aa82 100644 --- a/security/manager/security_manager.go +++ b/security/manager/security_manager.go @@ -52,3 +52,14 @@ func (s *managerImpl) GetAuthenticator(_ context.Context) security.Authenticator func (s *managerImpl) GetAuthorizer(_ context.Context) security.Authorizer { return s.authorizer } + +// Close releases resources held by the security manager components. +func (s *managerImpl) Close() { + type closer interface { + Close() + } + + if c, ok := s.authenticator.(closer); ok { + c.Close() + } +} diff --git a/security/openid/jwt_token_authenticator.go b/security/openid/jwt_token_authenticator.go index 7e69f5f..2234fe8 100644 --- a/security/openid/jwt_token_authenticator.go +++ b/security/openid/jwt_token_authenticator.go @@ -54,6 +54,13 @@ func NewJwtTokenAuthenticator(cfg config.ConfigurationJWTVerification) security. } } +// Close stops the background JWKS refresh goroutine. +func (a *jwtTokenAuthenticator) Close() { + if a.tokenAuthenticator != nil { + a.tokenAuthenticator.Stop() + } +} + func (a *jwtTokenAuthenticator) Authenticate( ctx context.Context, jwtToken string, @@ -131,6 +138,7 @@ type TokenAuthenticator struct { keys map[string]any lastErr error stopChan chan struct{} + stopOnce sync.Once } // ------------------------------ @@ -173,7 +181,9 @@ func (a *TokenAuthenticator) Start() { } func (a *TokenAuthenticator) Stop() { - close(a.stopChan) + a.stopOnce.Do(func() { + close(a.stopChan) + }) } // GetKeyCount returns the number of currently loaded keys (for testing purposes). diff --git a/server.go b/server.go index 7fa9452..769d4eb 100644 --- a/server.go +++ b/server.go @@ -130,7 +130,7 @@ type grpcDriver struct { internalHTTPDriver ServerDriver grpcPort string - errorChannel chan error + reportError func(ctx context.Context, err error) grpcServer *grpc.Server @@ -141,7 +141,7 @@ func (gd *grpcDriver) ListenAndServe(addr string, h http.Handler) error { go func(address string) { ln, err2 := getListener(gd.ctx, address, "", "", gd.grpcListener) if err2 != nil { - gd.errorChannel <- err2 + gd.reportError(gd.ctx, err2) return } log := util.Log(gd.ctx).WithField("grpc port", address) @@ -149,7 +149,7 @@ func (gd *grpcDriver) ListenAndServe(addr string, h http.Handler) error { err2 = gd.grpcServer.Serve(ln) if err2 != nil { - gd.errorChannel <- err2 + gd.reportError(gd.ctx, err2) return } }(gd.grpcPort) @@ -161,7 +161,7 @@ func (gd *grpcDriver) ListenAndServeTLS(addr, certFile, certKeyFile string, h ht go func(address, certPath, certKeyPath string) { ln, err2 := getListener(gd.ctx, address, certPath, certKeyPath, gd.grpcListener) if err2 != nil { - gd.errorChannel <- err2 + gd.reportError(gd.ctx, err2) return } @@ -170,7 +170,7 @@ func (gd *grpcDriver) ListenAndServeTLS(addr, certFile, certKeyFile string, h ht err2 = gd.grpcServer.Serve(ln) if err2 != nil { - gd.errorChannel <- err2 + gd.reportError(gd.ctx, err2) return } }(gd.grpcPort, certFile, certKeyFile) diff --git a/service.go b/service.go index 1741964..359dd9b 100644 --- a/service.go +++ b/service.go @@ -3,6 +3,7 @@ package frame import ( "context" "errors" + "fmt" "net" "net/http" "os/signal" @@ -130,10 +131,11 @@ func NewServiceWithContext(ctx context.Context, opts ...Option) (context.Context cfg, _ := config.FromEnv[config.ConfigurationDefault]() svc := &Service{ - name: cfg.Name(), - cancelFunc: signalCancelFunc, // Store its cancel function - errorChannel: make(chan error, 1), - configuration: &cfg, + name: cfg.Name(), + cancelFunc: signalCancelFunc, // Store its cancel function + errorChannel: make(chan error, 1), + configuration: &cfg, + profilerServer: profiler.NewServer(), } opts = append( @@ -149,6 +151,13 @@ func NewServiceWithContext(ctx context.Context, opts ...Option) (context.Context } svc.securityManager = securityManager.NewManager(ctx, svc.name, svc.environment, smCfg, svc.clientManager) + // Register cleanup for the security manager to stop background goroutines (e.g., JWKS refresh) + svc.AddCleanupMethod(func(_ context.Context) { + if svc.securityManager != nil { + svc.securityManager.Close() + } + }) + if svc.registerOauth2Cli { sm := svc.SecurityManager() clr := sm.GetOauth2ClientRegistrar(ctx) @@ -156,7 +165,7 @@ func NewServiceWithContext(ctx context.Context, opts ...Option) (context.Context // Register for JWT err = clr.RegisterForJwt(ctx, sm) if err != nil { - svc.Log(ctx).WithError(err).Fatal("could not register server client for jwt") + svc.AddStartupError(fmt.Errorf("could not register server client for jwt: %w", err)) } } @@ -164,7 +173,10 @@ func NewServiceWithContext(ctx context.Context, opts ...Option) (context.Context if !ok { wkpCfg = &cfg } - svc.workerPoolManager = workerpool.NewManager(ctx, wkpCfg, svc.sendStopError) + svc.workerPoolManager, err = workerpool.NewManager(ctx, wkpCfg, svc.sendStopError) + if err != nil { + svc.AddStartupError(err) + } svc.queueManager = queue.NewQueueManager(ctx, svc.workerPoolManager) @@ -172,7 +184,7 @@ func NewServiceWithContext(ctx context.Context, opts ...Option) (context.Context // This registers the internal events publisher/subscriber err = svc.setupEventsQueue(ctx) if err != nil { - svc.Log(ctx).WithError(err).Panic("could not setup application events") + svc.AddStartupError(fmt.Errorf("could not setup application events: %w", err)) } // Execute pre-start methods now that queue manager is initialized @@ -506,6 +518,7 @@ func (s *Service) initializeServerDrivers(ctx context.Context, httpPort string) ctx: ctx, internalHTTPDriver: s.driver, // Embed the fully configured defaultServer grpcPort: s.grpcPort, + reportError: s.sendStopError, grpcServer: s.grpcServer, grpcListener: s.grpcListener, // Use the primary listener established for gRPC } @@ -603,7 +616,6 @@ func (s *Service) startProfilerIfEnabled(ctx context.Context) error { return nil } - s.profilerServer = profiler.NewServer() return s.profilerServer.StartIfEnabled(ctx, cfg) } diff --git a/telemetry/manager.go b/telemetry/manager.go index 00d1bb0..d0d65ae 100644 --- a/telemetry/manager.go +++ b/telemetry/manager.go @@ -17,7 +17,7 @@ import ( sdkmetrics "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" "github.com/pitabwire/frame/config" "github.com/pitabwire/frame/version" diff --git a/workerpool/manager.go b/workerpool/manager.go index 21e43bb..7770675 100644 --- a/workerpool/manager.go +++ b/workerpool/manager.go @@ -89,7 +89,7 @@ func NewManager( cfg config.ConfigurationWorkerPool, stopOnErr func(ctx context.Context, err error), opts ...Option, -) Manager { +) (Manager, error) { log := util.Log(ctx) poolOpts := defaultWorkerPoolOpts(cfg, log) @@ -100,13 +100,13 @@ func NewManager( pool, err := setupWorkerPool(ctx, poolOpts) if err != nil { - log.WithError(err).Panic("could not create a default worker pool") + return nil, fmt.Errorf("could not create a default worker pool: %w", err) } return &manager{ pool: pool, stopErr: stopOnErr, - } + }, nil } func (m manager) GetPool() (WorkerPool, error) {