From 9133b352ca2a07904917ec994564b474650cc261 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 13 May 2026 12:43:39 +0200 Subject: [PATCH 1/5] feat: migrate RPC surface to Connect-RPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plugin.go RPC() now returns (string, http.Handler) via metricsV1connect.NewMetricsServiceHandler from api-go v6.0.0-beta.9 - rpc.go reshaped to the Connect handler interface with 6 methods: Add / Sub / Observe / Set / Declare / Unregister - handler-side lookup with typed Connect codes: - unknown collector → CodeNotFound - operation unsupported by collector kind → CodeFailedPrecondition - missing labels on a *Vec collector → CodeInvalidArgument - prometheus registry errors → CodeInternal - vecMetric / vecObserver generic helpers consolidate the GaugeVec / CounterVec / HistogramVec / SummaryVec label-validation + lookup path - tests/metrics_test.go: 15 goridge client.Call sites + 14 client constructions collapsed into a single newMetricsClient helper; tests now call typed Connect methods with toProtoCollector translating metrics.NamedCollector → metricsV1.NamedCollector - new tests/metrics_api_test.go covers Connect, plain HTTP+protojson, plain gRPC against the same handler, plus HTTP-GET idempotency (all 6 methods POST-only — no GET candidates) - new tests/configs/.rr-metrics-api.yaml minimal config for the API tests - bump api-go to v6.0.0-beta.9, goridge to v4.0.0-beta.2, rpc/v6 to v6.0.0-beta.4, http/v6 to v6.0.0-beta.7 (typecheck-fix upstream) - replace http.NewRequest/http.Get with NewRequestWithContext + Client.Do in unrelated test helpers (lint surfaced after http/v6 typecheck started working) --- go.mod | 6 + go.sum | 34 +++ plugin.go | 10 +- rpc.go | 413 +++++++++++--------------- tests/configs/.rr-metrics-api.yaml | 15 + tests/go.mod | 18 +- tests/go.sum | 24 +- tests/metrics_api_test.go | 263 +++++++++++++++++ tests/metrics_test.go | 450 ++++++++++++----------------- 9 files changed, 697 insertions(+), 536 deletions(-) create mode 100644 tests/configs/.rr-metrics-api.yaml create mode 100644 tests/metrics_api_test.go diff --git a/go.mod b/go.mod index d6acda0..247a2c8 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.26 toolchain go1.26.0 require ( + connectrpc.com/connect v1.19.2 github.com/prometheus/client_golang v1.23.2 + github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9 github.com/roadrunner-server/endure/v2 v2.6.2 github.com/roadrunner-server/errors v1.5.0 github.com/stretchr/testify v1.11.1 @@ -26,6 +28,10 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 254bda4..437c0e6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -5,8 +7,16 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -27,6 +37,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9 h1:iD09+1Sc6ZMxEOJsj7YIblzEN1ORZ+NLH7YYO0nqASs= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9/go.mod h1:4BbPXAqT0sOG2EVRcymESMwxaKSpHl4hG+UDoCepWu0= github.com/roadrunner-server/endure/v2 v2.6.2 h1:sIB4kTyE7gtT3fDhuYWUYn6Vt/dcPtiA6FoNS1eS+84= github.com/roadrunner-server/endure/v2 v2.6.2/go.mod h1:t/2+xpNYgGBwhzn83y2MDhvhZ19UVq1REcvqn7j7RB8= github.com/roadrunner-server/errors v1.5.0 h1:unG7LKIZrSzkCCF3YLRLA5VyqE0KKomofXVJUXJe00g= @@ -39,12 +51,34 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +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.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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= 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/plugin.go b/plugin.go index b6b4811..3f8fd69 100644 --- a/plugin.go +++ b/plugin.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/roadrunner-server/api-go/v6/metrics/v1/metricsV1connect" "github.com/roadrunner-server/endure/v2/dep" "github.com/roadrunner-server/errors" "golang.org/x/sys/cpu" @@ -261,10 +262,7 @@ func (p *Plugin) Name() string { return PluginName } -// RPC interface satisfaction -func (p *Plugin) RPC() any { - return &rpc{ - p: p, - log: p.log, - } +// RPC returns the Connect-RPC handler mount for the metrics service. +func (p *Plugin) RPC() (string, http.Handler) { + return metricsV1connect.NewMetricsServiceHandler(&rpc{p: p, log: p.log}) } diff --git a/rpc.go b/rpc.go index 37804fa..18e227d 100644 --- a/rpc.go +++ b/rpc.go @@ -1,330 +1,263 @@ package metrics import ( + "context" + stderr "errors" + "fmt" "log/slog" + "connectrpc.com/connect" "github.com/prometheus/client_golang/prometheus" + metricsV1 "github.com/roadrunner-server/api-go/v6/metrics/v1" "github.com/roadrunner-server/errors" ) +var ( + errUndefinedCollector = stderr.New("undefined collector") + errRequiredLabels = stderr.New("required labels for collector") + errUnsupportedOpForCol = stderr.New("collector does not support the requested operation") + errUnknownCollectorTyp = stderr.New("unknown collector type") +) + type rpc struct { p *Plugin log *slog.Logger } -// Metric represents a single metric produced by the application. -type Metric struct { - // Collector name. - Name string `msgpack:"alias:name"` - // Collector value. - Value float64 `msgpack:"alias:value"` - // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values. - Labels []string `msgpack:"alias:labels"` -} +func (r *rpc) Add(_ context.Context, req *connect.Request[metricsV1.AddRequest]) (*connect.Response[metricsV1.Response], error) { + m := req.Msg.GetMetric() + r.log.Debug("adding metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) -// Add new metric to the designated collector. -func (r *rpc) Add(m *Metric, ok *bool) error { - const op = errors.Op("metrics_plugin_add") - r.log.Debug("adding metric", "name", m.Name, "value", m.Value, "labels", m.Labels) - c, exist := r.p.collectors.Load(m.Name) - if !exist { - r.log.Error("undefined collector", "collector", m.Name) - return errors.E(op, errors.Errorf("undefined collector %s, try first Declare the desired collector", m.Name)) + col, code, err := r.lookupCollector(m.GetName()) + if err != nil { + return nil, connect.NewError(code, err) } - col := c.(*collector) - - switch c := col.col.(type) { + switch c := col.(type) { case prometheus.Gauge: - c.Add(m.Value) - + c.Add(m.GetValue()) case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - r.log.Error("required labels for collector", "collector", m.Name) - return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) - } - - gauge, err := c.GetMetricWithLabelValues(m.Labels...) + gv, err := vecMetric(c, m) if err != nil { - r.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) - return errors.E(op, err) + return nil, err } - gauge.Add(m.Value) + gv.Add(m.GetValue()) case prometheus.Counter: - c.Add(m.Value) - + c.Add(m.GetValue()) case *prometheus.CounterVec: - if len(m.Labels) == 0 { - return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) - } - - gauge, err := c.GetMetricWithLabelValues(m.Labels...) + cv, err := vecMetric(c, m) if err != nil { - r.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) - return errors.E(op, err) + return nil, err } - gauge.Add(m.Value) - + cv.Add(m.GetValue()) default: - return errors.E(op, errors.Errorf("collector %s does not support method `Add`", m.Name)) + return nil, connect.NewError(connect.CodeFailedPrecondition, + fmt.Errorf("%w: %s does not support Add", errUnsupportedOpForCol, m.GetName())) } - // RPC, set ok to true as return value. Need by r.Call reply argument - *ok = true - r.log.Debug("metric successfully added", "name", m.Name, "labels", m.Labels, "value", m.Value) - return nil + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } -// Sub subtract the value from the specific metric (gauge only). -func (r *rpc) Sub(m *Metric, ok *bool) error { - const op = errors.Op("metrics_plugin_sub") - r.log.Debug("subtracting value from metric", "name", m.Name, "value", m.Value, "labels", m.Labels) - c, exist := r.p.collectors.Load(m.Name) - if !exist { - r.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) - } - if c == nil { - // can it be a nil ??? I guess can't - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) - } +func (r *rpc) Sub(_ context.Context, req *connect.Request[metricsV1.SubRequest]) (*connect.Response[metricsV1.Response], error) { + m := req.Msg.GetMetric() + r.log.Debug("subtracting metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) - col := c.(*collector) + col, code, err := r.lookupCollector(m.GetName()) + if err != nil { + return nil, connect.NewError(code, err) + } - switch c := col.col.(type) { + switch c := col.(type) { case prometheus.Gauge: - c.Sub(m.Value) - + c.Sub(m.GetValue()) case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - r.log.Error("required labels for collector, but none was provided", "name", m.Name, "value", m.Value) - return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) - } - - gauge, err := c.GetMetricWithLabelValues(m.Labels...) + gv, err := vecMetric(c, m) if err != nil { - r.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) - return errors.E(op, err) + return nil, err } - gauge.Sub(m.Value) + gv.Sub(m.GetValue()) default: - return errors.E(op, errors.Errorf("collector `%s` does not support method `Sub`", m.Name)) + return nil, connect.NewError(connect.CodeFailedPrecondition, + fmt.Errorf("%w: %s does not support Sub", errUnsupportedOpForCol, m.GetName())) } - r.log.Debug("subtracting operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) - *ok = true - return nil + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } -// Observe the value (histogram and summary only). -func (r *rpc) Observe(m *Metric, ok *bool) error { - const op = errors.Op("metrics_plugin_observe") - r.log.Debug("observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) +func (r *rpc) Observe(_ context.Context, req *connect.Request[metricsV1.ObserveRequest]) (*connect.Response[metricsV1.Response], error) { + m := req.Msg.GetMetric() + r.log.Debug("observing metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) - c, exist := r.p.collectors.Load(m.Name) - if !exist { - r.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) - } - if c == nil { - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + col, code, err := r.lookupCollector(m.GetName()) + if err != nil { + return nil, connect.NewError(code, err) } - col := c.(*collector) - - switch c := col.col.(type) { - case *prometheus.SummaryVec: - if len(m.Labels) == 0 { - return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) - } - - observer, err := c.GetMetricWithLabelValues(m.Labels...) - if err != nil { - return errors.E(op, err) - } - observer.Observe(m.Value) - + switch c := col.(type) { case prometheus.Histogram: - c.Observe(m.Value) - + c.Observe(m.GetValue()) case *prometheus.HistogramVec: - if len(m.Labels) == 0 { - return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + ov, err := vecObserver(c, m) + if err != nil { + return nil, err } - - observer, err := c.GetMetricWithLabelValues(m.Labels...) + ov.Observe(m.GetValue()) + case *prometheus.SummaryVec: + ov, err := vecObserver(c, m) if err != nil { - r.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) - return errors.E(op, err) + return nil, err } - observer.Observe(m.Value) + ov.Observe(m.GetValue()) default: - return errors.E(op, errors.Errorf("collector `%s` does not support method `Observe`", m.Name)) + return nil, connect.NewError(connect.CodeFailedPrecondition, + fmt.Errorf("%w: %s does not support Observe", errUnsupportedOpForCol, m.GetName())) } - r.log.Debug("observe operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) - - *ok = true - return nil + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } -// Declare is used to register new collector in prometheus -func (r *rpc) Declare(nc *NamedCollector, ok *bool) error { - const op = errors.Op("metrics_plugin_declare") - r.p.mu.Lock() - defer r.p.mu.Unlock() +func (r *rpc) Set(_ context.Context, req *connect.Request[metricsV1.SetRequest]) (*connect.Response[metricsV1.Response], error) { + m := req.Msg.GetMetric() + r.log.Debug("setting metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) - r.log.Debug("declaring new metric", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) - _, exist := r.p.collectors.Load(nc.Name) - if exist { - r.log.Warn("metric with provided name already exist", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) - *ok = true - return nil + col, code, err := r.lookupCollector(m.GetName()) + if err != nil { + return nil, connect.NewError(code, err) } - var promCol prometheus.Collector - switch nc.Type { - case Histogram: - opts := prometheus.HistogramOpts{ - Name: nc.Name, - Namespace: nc.Namespace, - Subsystem: nc.Subsystem, - Help: nc.Help, - Buckets: nc.Buckets, - } - - if len(nc.Labels) != 0 { - promCol = prometheus.NewHistogramVec(opts, nc.Labels) - } else { - promCol = prometheus.NewHistogram(opts) - } - case Gauge: - opts := prometheus.GaugeOpts{ - Name: nc.Name, - Namespace: nc.Namespace, - Subsystem: nc.Subsystem, - Help: nc.Help, + switch c := col.(type) { + case prometheus.Gauge: + c.Set(m.GetValue()) + case *prometheus.GaugeVec: + gv, err := vecMetric(c, m) + if err != nil { + return nil, err } + gv.Set(m.GetValue()) + default: + return nil, connect.NewError(connect.CodeFailedPrecondition, + fmt.Errorf("%w: %s does not support Set", errUnsupportedOpForCol, m.GetName())) + } - if len(nc.Labels) != 0 { - promCol = prometheus.NewGaugeVec(opts, nc.Labels) - } else { - promCol = prometheus.NewGauge(opts) - } - case Counter: - opts := prometheus.CounterOpts{ - Name: nc.Name, - Namespace: nc.Namespace, - Subsystem: nc.Subsystem, - Help: nc.Help, - } + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil +} - if len(nc.Labels) != 0 { - promCol = prometheus.NewCounterVec(opts, nc.Labels) - } else { - promCol = prometheus.NewCounter(opts) - } - case Summary: - opts := prometheus.SummaryOpts{ - Name: nc.Name, - Namespace: nc.Namespace, - Subsystem: nc.Subsystem, - Help: nc.Help, - Objectives: nc.Objectives, - } +func (r *rpc) Declare(_ context.Context, req *connect.Request[metricsV1.DeclareRequest]) (*connect.Response[metricsV1.Response], error) { + const op = errors.Op("metrics_rpc_declare") - if len(nc.Labels) != 0 { - promCol = prometheus.NewSummaryVec(opts, nc.Labels) - } else { - promCol = prometheus.NewSummary(opts) - } + nc := req.Msg.GetCollector() + r.p.mu.Lock() + defer r.p.mu.Unlock() - default: - return errors.E(op, errors.Errorf("unknown collector type %s", nc.Type)) + r.log.Debug("declaring metric", "name", nc.GetName(), "type", nc.GetCollector().GetType(), "namespace", nc.GetCollector().GetNamespace()) + if _, exist := r.p.collectors.Load(nc.GetName()); exist { + r.log.Warn("metric with provided name already exist", "name", nc.GetName()) + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } - // that method might panic, we handle it by recover - err := r.p.Register(promCol) + promCol, err := buildPromCollector(nc) if err != nil { - *ok = false - return errors.E(op, err) + return nil, connect.NewError(connect.CodeInvalidArgument, errors.E(op, err)) } - col := &collector{ - col: promCol, - registered: true, + if err := r.p.Register(promCol); err != nil { + return nil, connect.NewError(connect.CodeInternal, errors.E(op, err)) } - // add collector to sync.Map - r.p.collectors.Store(nc.Name, col) - - r.log.Debug("metric successfully added", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) - - *ok = true - return nil + r.p.collectors.Store(nc.GetName(), &collector{col: promCol, registered: true}) + r.log.Debug("metric registered", "name", nc.GetName()) + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } -// Unregister removes collector from the prometheus registry -func (r *rpc) Unregister(name string, ok *bool) error { - const op = errors.Op("metrics_plugin_unregister") - +func (r *rpc) Unregister(_ context.Context, req *connect.Request[metricsV1.UnregisterRequest]) (*connect.Response[metricsV1.Response], error) { + name := req.Msg.GetName() r.log.Debug("unregistering collector", "name", name) c, exist := r.p.collectors.LoadAndDelete(name) if !exist || c == nil { - return errors.E(op, errors.Errorf("undefined collector %s", name)) + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("%w: %s", errUndefinedCollector, name)) } - if col, k := c.(*collector); k { - if r.p.registry.Unregister(col.col) { - *ok = true - r.log.Debug("collector was successfully unregistered", "name", name) - return nil - } - - r.log.Debug("collector was deleted from the RR registry but not from the prometheus collector", "name", name) + col, ok := c.(*collector) + if !ok { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("collectors map held non-*collector for %s", name)) } - - return nil + if r.p.registry.Unregister(col.col) { + r.log.Debug("collector unregistered", "name", name) + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil + } + r.log.Debug("collector deleted from RR but not from prometheus registry", "name", name) + return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } -// Set the metric value (only for gaude). -func (r *rpc) Set(m *Metric, ok *bool) (err error) { - const op = errors.Op("metrics_plugin_set") - r.log.Debug("observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) - - c, exist := r.p.collectors.Load(m.Name) - if !exist { - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) +func (r *rpc) lookupCollector(name string) (prometheus.Collector, connect.Code, error) { + c, exist := r.p.collectors.Load(name) + if !exist || c == nil { + r.log.Error("undefined collector", "collector", name) + return nil, connect.CodeNotFound, fmt.Errorf("%w: %s", errUndefinedCollector, name) } - if c == nil { - return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + col, ok := c.(*collector) + if !ok { + return nil, connect.CodeInternal, fmt.Errorf("collectors map held non-*collector for %s", name) } + return col.col, 0, nil +} - col := c.(*collector) +func vecMetric[T interface { + GetMetricWithLabelValues(lvs ...string) (V, error) +}, V any](c T, m *metricsV1.Metric) (V, error) { + var zero V + if len(m.GetLabels()) == 0 { + return zero, connect.NewError(connect.CodeInvalidArgument, + fmt.Errorf("%w: %s", errRequiredLabels, m.GetName())) + } + v, err := c.GetMetricWithLabelValues(m.GetLabels()...) + if err != nil { + return zero, connect.NewError(connect.CodeInternal, err) + } + return v, nil +} - switch c := col.col.(type) { - case prometheus.Gauge: - c.Set(m.Value) +func vecObserver[T interface { + GetMetricWithLabelValues(lvs ...string) (prometheus.Observer, error) +}](c T, m *metricsV1.Metric) (prometheus.Observer, error) { + return vecMetric[T, prometheus.Observer](c, m) +} - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - r.log.Error("required labels for collector", "collector", m.Name) - return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) +func buildPromCollector(nc *metricsV1.NamedCollector) (prometheus.Collector, error) { + col := nc.GetCollector() + switch col.GetType() { + case metricsV1.CollectorType_COLLECTOR_TYPE_HISTOGRAM: + opts := prometheus.HistogramOpts{Name: nc.GetName(), Namespace: col.GetNamespace(), Subsystem: col.GetSubsystem(), Help: col.GetHelp(), Buckets: col.GetBuckets()} + if len(col.GetLabels()) != 0 { + return prometheus.NewHistogramVec(opts, col.GetLabels()), nil } - gauge, err := c.GetMetricWithLabelValues(m.Labels...) - if err != nil { - r.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) - return errors.E(op, err) + return prometheus.NewHistogram(opts), nil + case metricsV1.CollectorType_COLLECTOR_TYPE_GAUGE: + opts := prometheus.GaugeOpts{Name: nc.GetName(), Namespace: col.GetNamespace(), Subsystem: col.GetSubsystem(), Help: col.GetHelp()} + if len(col.GetLabels()) != 0 { + return prometheus.NewGaugeVec(opts, col.GetLabels()), nil } - gauge.Set(m.Value) - + return prometheus.NewGauge(opts), nil + case metricsV1.CollectorType_COLLECTOR_TYPE_COUNTER: + opts := prometheus.CounterOpts{Name: nc.GetName(), Namespace: col.GetNamespace(), Subsystem: col.GetSubsystem(), Help: col.GetHelp()} + if len(col.GetLabels()) != 0 { + return prometheus.NewCounterVec(opts, col.GetLabels()), nil + } + return prometheus.NewCounter(opts), nil + case metricsV1.CollectorType_COLLECTOR_TYPE_SUMMARY: + objectives := make(map[float64]float64, len(col.GetObjectives())) + for _, o := range col.GetObjectives() { + objectives[o.GetQuantile()] = o.GetError() + } + opts := prometheus.SummaryOpts{Name: nc.GetName(), Namespace: col.GetNamespace(), Subsystem: col.GetSubsystem(), Help: col.GetHelp(), Objectives: objectives} + if len(col.GetLabels()) != 0 { + return prometheus.NewSummaryVec(opts, col.GetLabels()), nil + } + return prometheus.NewSummary(opts), nil + case metricsV1.CollectorType_COLLECTOR_TYPE_UNSPECIFIED: + return nil, fmt.Errorf("%w: unspecified", errUnknownCollectorTyp) default: - return errors.E(op, errors.Errorf("collector `%s` does not support method Set", m.Name)) + return nil, fmt.Errorf("%w: %v", errUnknownCollectorTyp, col.GetType()) } - - r.log.Debug("set operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) - - *ok = true - return nil } diff --git a/tests/configs/.rr-metrics-api.yaml b/tests/configs/.rr-metrics-api.yaml new file mode 100644 index 0000000..c3a5a68 --- /dev/null +++ b/tests/configs/.rr-metrics-api.yaml @@ -0,0 +1,15 @@ +version: '3' + +rpc: + listen: tcp://127.0.0.1:6001 + +metrics: + address: "127.0.0.1:2115" + collect: + app_metric_counter: + type: counter + help: "Counter for API tests" + +logs: + mode: development + level: error diff --git a/tests/go.mod b/tests/go.mod index 37c3f8a..d9b34f8 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,22 +5,27 @@ go 1.26 toolchain go1.26.0 require ( + connectrpc.com/connect v1.19.2 github.com/prometheus/client_golang v1.23.2 + github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9 github.com/roadrunner-server/config/v6 v6.0.0-beta.3 github.com/roadrunner-server/endure/v2 v2.6.2 - github.com/roadrunner-server/goridge/v4 v4.0.0-beta.1 - github.com/roadrunner-server/http/v6 v6.0.0-beta.6 + github.com/roadrunner-server/http/v6 v6.0.0-beta.7 github.com/roadrunner-server/logger/v6 v6.0.0-beta.3 github.com/roadrunner-server/metrics/v6 v6.0.0 github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2 - github.com/roadrunner-server/rpc/v6 v6.0.0-beta.3 + github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4 github.com/roadrunner-server/server/v6 v6.0.0-beta.5 github.com/stretchr/testify v1.11.1 + golang.org/x/net v0.54.0 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 ) replace github.com/roadrunner-server/metrics/v6 => ../ require ( + connectrpc.com/grpcreflect v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/caddyserver/certmagic v0.25.3 // indirect github.com/caddyserver/zerossl v0.1.5 // indirect @@ -50,11 +55,11 @@ require ( github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect - github.com/roadrunner-server/api-go/v6 v6.0.0-beta.4 // indirect github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 // indirect github.com/roadrunner-server/context v1.3.0 // indirect github.com/roadrunner-server/errors v1.5.0 // indirect github.com/roadrunner-server/events v1.0.1 // indirect + github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2 // indirect github.com/roadrunner-server/pool/v2 v2.0.0-beta.1 // indirect github.com/roadrunner-server/tcplisten v1.5.2 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -82,14 +87,11 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.51.0 // indirect golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect - google.golang.org/grpc v1.81.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 1a82496..8fbdb99 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1,5 +1,9 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= +connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= @@ -82,8 +86,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/roadrunner-server/api-go/v6 v6.0.0-beta.4 h1:wX8IezPUeeBJzlzaBEFSZBE5Bc/Le1Uf/GdFRdFO3HQ= -github.com/roadrunner-server/api-go/v6 v6.0.0-beta.4/go.mod h1:jI30i64yCAxJh7KHc8e1B8NgDcvcnSTI1OIK8lTE+Y0= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9 h1:iD09+1Sc6ZMxEOJsj7YIblzEN1ORZ+NLH7YYO0nqASs= +github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9/go.mod h1:4BbPXAqT0sOG2EVRcymESMwxaKSpHl4hG+UDoCepWu0= github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 h1:GqsZzWQ5jMXRF1O/b8IqFz9PLpS7Ui0K4OyACLql2MI= github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2/go.mod h1:2v4yUK5Kvbvq8C3IkDoBkuamq9h+7i/JLjyf7k1j5JM= github.com/roadrunner-server/config/v6 v6.0.0-beta.3 h1:G0EUzJ6Yw4UnleM6BhnOBbYPXKDHRmCJiGhC3nXDBwI= @@ -96,18 +100,18 @@ github.com/roadrunner-server/errors v1.5.0 h1:unG7LKIZrSzkCCF3YLRLA5VyqE0KKomofX github.com/roadrunner-server/errors v1.5.0/go.mod h1:g9fo/T2C13cWRDR9PW1r0ZAOSQfNhWAZawyfkGiaHuI= github.com/roadrunner-server/events v1.0.1 h1:waCkKhxhzdK3VcI1xG22l+h+0J+Nfdpxjhyy01Un+kI= github.com/roadrunner-server/events v1.0.1/go.mod h1:WZRqoEVaFm209t52EuoT7ISUtvX6BrCi6bI/7pjkVC0= -github.com/roadrunner-server/goridge/v4 v4.0.0-beta.1 h1:dO1wKnuMr4xMmH6DY2ZaZ6FWS+Vo50+C7fuAcyO/xBk= -github.com/roadrunner-server/goridge/v4 v4.0.0-beta.1/go.mod h1:+gKla9HAyYlk0TsC9VktwtOL63aimsWT3oPsuCLh4/o= -github.com/roadrunner-server/http/v6 v6.0.0-beta.6 h1:Bhb99PMomKm3Vjn7MtsrEsZaH71yfJBAUw/gOwozXTE= -github.com/roadrunner-server/http/v6 v6.0.0-beta.6/go.mod h1:FFEYeboAk0EmdnjTM6GB2bMnpvVqWks69gctxFtZmn4= +github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2 h1:MgH6oiSgcl+vphsQ6JpyedkXQ/DPf8zVpn0z7rdBp10= +github.com/roadrunner-server/goridge/v4 v4.0.0-beta.2/go.mod h1:Wv9CBO9VIU92e5iZIuehLHKakXgMkOzxoT4/oHDjIUA= +github.com/roadrunner-server/http/v6 v6.0.0-beta.7 h1:uCKQBlD5gOCuGlJQA4h4q+IDK5C3tSGBPCPTUJjRztY= +github.com/roadrunner-server/http/v6 v6.0.0-beta.7/go.mod h1:T5XNZsqAsUMozEthOD5PmRhQYl7HYx7JqV7NAuA459Q= github.com/roadrunner-server/logger/v6 v6.0.0-beta.3 h1:eoJKXAUSyykDfVX6eTUhmAn6Y8pS/LyI5fDP4H+G5rQ= github.com/roadrunner-server/logger/v6 v6.0.0-beta.3/go.mod h1:MwHb3AbltHYtu7nRpml5NeYu7O+W8rCpDBeNTTEoE1M= github.com/roadrunner-server/pool/v2 v2.0.0-beta.1 h1:jpYXFtdD6QGAdAGPgMxrNi3j1CegCRpb2y+A+3GnXFA= github.com/roadrunner-server/pool/v2 v2.0.0-beta.1/go.mod h1:Bo1wT7RtL3eyQHXBUohNhtj/yAmRt6Rq8smuBg5pWkY= github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2 h1:e6Z5YFRwi1Tcr9T5sgecfqcOUo3XC7fTkcoGVKP6mnw= github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2/go.mod h1:OYfupkw6fQjZHf83T87KUXNx4/IwCMlBA3J3sqDx5Wg= -github.com/roadrunner-server/rpc/v6 v6.0.0-beta.3 h1:hvVEDIMB9MKI8uWX++MrBzHRzq404ygU0fDs6U2V/3Y= -github.com/roadrunner-server/rpc/v6 v6.0.0-beta.3/go.mod h1:BpDpd2/UceDdsDJNP0iMfmegbXthxiZM4MU6GOJoSXo= +github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4 h1:Qj2nrHIWOHE9Tys+FBG2IdoPtzgIUh6juQ5wXLGGDMw= +github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4/go.mod h1:k5KT3fpnJVd27m0HbGGBiTPXlWI6eJdd6C+ohp5IE0U= github.com/roadrunner-server/server/v6 v6.0.0-beta.5 h1:poCPSHc768UtMqJRgW3rZT5pDboOnwOD4qc28hhXJ4I= github.com/roadrunner-server/server/v6 v6.0.0-beta.5/go.mod h1:SbODuCzC2gcbFhAmJDWvjf34pPrUWP5NxxVsTRQDuZ4= github.com/roadrunner-server/tcplisten v1.5.2 h1:nn8yXYrhRDkfQ9AAu4V075uT4fZRmOnpxkawgE+bWPA= @@ -196,8 +200,8 @@ 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 v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/tests/metrics_api_test.go b/tests/metrics_api_test.go new file mode 100644 index 0000000..d3c06f6 --- /dev/null +++ b/tests/metrics_api_test.go @@ -0,0 +1,263 @@ +package metrics + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "io" + "log/slog" + "net" + "net/http" + "net/url" + "sync" + "testing" + "time" + + "connectrpc.com/connect" + metricsV1 "github.com/roadrunner-server/api-go/v6/metrics/v1" + "github.com/roadrunner-server/api-go/v6/metrics/v1/metricsV1connect" + "github.com/roadrunner-server/config/v6" + "github.com/roadrunner-server/endure/v2" + "github.com/roadrunner-server/logger/v6" + "github.com/roadrunner-server/metrics/v6" + rpcPlugin "github.com/roadrunner-server/rpc/v6" + "github.com/stretchr/testify/require" + "golang.org/x/net/http2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +const metricsAPIAddr = "127.0.0.1:6001" + +// startMetricsAPIContainer brings up rpc + metrics + logger with one pre- +// configured counter (`app_metric_counter`) — the API tests exercise the +// wire surface against this collector and against runtime-declared ones. +func startMetricsAPIContainer(t *testing.T) func() { + t.Helper() + + cont := endure.New(slog.LevelError) + cfg := &config.Plugin{ + Version: "2024.2.0", + Path: "configs/.rr-metrics-api.yaml", + } + + require.NoError(t, cont.RegisterAll( + cfg, + &logger.Plugin{}, + &rpcPlugin.Plugin{}, + &metrics.Plugin{}, + )) + require.NoError(t, cont.Init()) + + ch, err := cont.Serve() + require.NoError(t, err) + + wg := &sync.WaitGroup{} + stop := make(chan struct{}) + wg.Go(func() { + select { + case e := <-ch: + t.Errorf("container reported error: %v", e.Error) + case <-stop: + } + }) + + time.Sleep(500 * time.Millisecond) + + return func() { + close(stop) + require.NoError(t, cont.Stop()) + wg.Wait() + } +} + +func TestMetricsConnectAPI(t *testing.T) { + stop := startMetricsAPIContainer(t) + defer stop() + + httpc := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return (&net.Dialer{Timeout: 30 * time.Second}).DialContext(ctx, network, addr) + }, + }, + } + client := metricsV1connect.NewMetricsServiceClient(httpc, "http://"+metricsAPIAddr) + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + + // Declare a new gauge, set it, then read it back via Add. + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "api_gauge", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_GAUGE, + Help: "API test gauge", + }, + }, + })) + require.NoError(t, err) + require.True(t, declareResp.Msg.GetOk()) + + setResp, err := client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "api_gauge", Value: 42.0}, + })) + require.NoError(t, err) + require.True(t, setResp.Msg.GetOk()) + + // Unknown collector → CodeNotFound. + _, err = client.Add(ctx, connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "does-not-exist", Value: 1}, + })) + require.Error(t, err) + require.Equal(t, connect.CodeNotFound, connect.CodeOf(err)) + + // Operation unsupported by collector kind → CodeFailedPrecondition. + // Histogram doesn't support Set. + _, err = client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "api_histogram", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_HISTOGRAM, + Help: "API test histogram", + Buckets: []float64{0.1, 1.0}, + }, + }, + })) + require.NoError(t, err) + _, err = client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "api_histogram", Value: 0.5}, + })) + require.Error(t, err) + require.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err)) + + // Unregister an existing collector → ok. + unregResp, err := client.Unregister(ctx, connect.NewRequest(&metricsV1.UnregisterRequest{Name: "api_gauge"})) + require.NoError(t, err) + require.True(t, unregResp.Msg.GetOk()) + + // Unregister a missing collector → CodeNotFound. + _, err = client.Unregister(ctx, connect.NewRequest(&metricsV1.UnregisterRequest{Name: "missing"})) + require.Error(t, err) + require.Equal(t, connect.CodeNotFound, connect.CodeOf(err)) +} + +// TestMetricsHTTPApi exercises the metrics RPCs through plain HTTP/1.1 with +// a protojson body — the shape any non-Connect HTTP client uses against this +// handler. +func TestMetricsHTTPApi(t *testing.T) { + stop := startMetricsAPIContainer(t) + defer stop() + + httpc := &http.Client{Timeout: 30 * time.Second} + ctx := t.Context() + + call := func(method string, in proto.Message, out proto.Message) { + body, err := protojson.Marshal(in) + require.NoError(t, err) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + "http://"+metricsAPIAddr+"/metrics.v1.MetricsService/"+method, bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := httpc.Do(req) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equalf(t, http.StatusOK, resp.StatusCode, "method=%s body=%s", method, respBody) + require.NoError(t, protojson.Unmarshal(respBody, out)) + } + + var declareResp metricsV1.Response + call("Declare", &metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "http_counter", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_COUNTER, + Help: "HTTP API test counter", + }, + }, + }, &declareResp) + require.True(t, declareResp.GetOk()) + + var addResp metricsV1.Response + call("Add", &metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "http_counter", Value: 7.0}, + }, &addResp) + require.True(t, addResp.GetOk()) +} + +// TestMetricsGRPCApi exercises the metrics RPCs through a regular gRPC client. +// The same Connect handler serves gRPC framing off the same port. +func TestMetricsGRPCApi(t *testing.T) { + stop := startMetricsAPIContainer(t) + defer stop() + + conn, err := grpc.NewClient(metricsAPIAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { _ = conn.Close() }() + + client := metricsV1.NewMetricsServiceClient(conn) + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + + declareResp, err := client.Declare(ctx, &metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "grpc_counter", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_COUNTER, + Help: "gRPC API test counter", + }, + }, + }) + require.NoError(t, err) + require.True(t, declareResp.GetOk()) + + addResp, err := client.Add(ctx, &metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "grpc_counter", Value: 3.0}, + }) + require.NoError(t, err) + require.True(t, addResp.GetOk()) +} + +// TestMetricsHTTPGetIdempotency verifies that all six metrics RPCs reject +// HTTP GET — none of them are marked NO_SIDE_EFFECTS in the proto (every +// method mutates Prometheus state in some way). +func TestMetricsHTTPGetIdempotency(t *testing.T) { + stop := startMetricsAPIContainer(t) + defer stop() + + body, err := protojson.Marshal(&metricsV1.AddRequest{Metric: &metricsV1.Metric{Name: "probe"}}) + require.NoError(t, err) + + q := url.Values{} + q.Set("encoding", "json") + q.Set("base64", "1") + q.Set("message", base64.URLEncoding.EncodeToString(body)) + + methods := []string{"Add", "Sub", "Observe", "Set", "Declare", "Unregister"} + + httpc := &http.Client{Timeout: 30 * time.Second} + for _, m := range methods { + t.Run(m, func(t *testing.T) { + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, + "http://"+metricsAPIAddr+"/metrics.v1.MetricsService/"+m+"?"+q.Encode(), nil) + require.NoError(t, err) + + resp, err := httpc.Do(req) + require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + + require.Equalf(t, http.StatusMethodNotAllowed, resp.StatusCode, + "%s via GET should be rejected; got %s", m, resp.Status) + }) + } +} diff --git a/tests/metrics_test.go b/tests/metrics_test.go index a2dad81..96b5d41 100644 --- a/tests/metrics_test.go +++ b/tests/metrics_test.go @@ -1,11 +1,12 @@ package metrics import ( + "context" + "crypto/tls" "io" "log/slog" "net" "net/http" - "net/rpc" "os" "os/signal" "sync" @@ -13,9 +14,13 @@ import ( "testing" "time" + mocklogger "tests/mock" + + "connectrpc.com/connect" + metricsV1 "github.com/roadrunner-server/api-go/v6/metrics/v1" + "github.com/roadrunner-server/api-go/v6/metrics/v1/metricsV1connect" "github.com/roadrunner-server/config/v6" "github.com/roadrunner-server/endure/v2" - goridgeRpc "github.com/roadrunner-server/goridge/v4/pkg/rpc" httpPlugin "github.com/roadrunner-server/http/v6" "github.com/roadrunner-server/logger/v6" "github.com/roadrunner-server/metrics/v6" @@ -24,10 +29,51 @@ import ( "github.com/roadrunner-server/server/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - mocklogger "tests/mock" + "golang.org/x/net/http2" ) -const dialNetwork = "tcp" +func newMetricsClient(address string) metricsV1connect.MetricsServiceClient { + httpc := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return (&net.Dialer{Timeout: 30 * time.Second}).DialContext(ctx, network, addr) + }, + }, + } + return metricsV1connect.NewMetricsServiceClient(httpc, "http://"+address) +} + +func toProtoCollector(nc metrics.NamedCollector) *metricsV1.NamedCollector { + var t metricsV1.CollectorType + switch nc.Type { + case metrics.Histogram: + t = metricsV1.CollectorType_COLLECTOR_TYPE_HISTOGRAM + case metrics.Gauge: + t = metricsV1.CollectorType_COLLECTOR_TYPE_GAUGE + case metrics.Counter: + t = metricsV1.CollectorType_COLLECTOR_TYPE_COUNTER + case metrics.Summary: + t = metricsV1.CollectorType_COLLECTOR_TYPE_SUMMARY + } + objectives := make([]*metricsV1.Objective, 0, len(nc.Objectives)) + for q, e := range nc.Objectives { + objectives = append(objectives, &metricsV1.Objective{Quantile: q, Error: e}) + } + return &metricsV1.NamedCollector{ + Name: nc.Name, + Collector: &metricsV1.Collector{ + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Type: t, + Help: nc.Help, + Labels: nc.Labels, + Buckets: nc.Buckets, + Objectives: objectives, + }, + } +} func TestMetricsInit(t *testing.T) { cont := endure.New(slog.LevelDebug) @@ -638,31 +684,19 @@ func TestUpsertOfMetricsDeclaration(t *testing.T) { func configuredCounterMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) + client := newMetricsClient(address) + resp, err := client.Add(t.Context(), connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "app_metric_counter", Value: 100.0}, + })) assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool - - assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ - Name: "app_metric_counter", - Value: 100.0, - }, &ret)) - assert.True(t, ret) + assert.True(t, resp.Msg.GetOk()) } } func observeMetricNotEnoughLabels(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "observe_observeMetricNotEnoughLabels", @@ -675,29 +709,21 @@ func observeMetricNotEnoughLabels(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - assert.Error(t, client.Call("metrics.Observe", metrics.Metric{ - Name: "observe_observeMetric", - Value: 100.0, - Labels: []string{"test"}, - }, &ret)) - assert.False(t, ret) + assert.True(t, declareResp.Msg.GetOk()) + + _, err = client.Observe(ctx, connect.NewRequest(&metricsV1.ObserveRequest{ + Metric: &metricsV1.Metric{Name: "observe_observeMetric", Value: 100.0, Labels: []string{"test"}}, + })) + assert.Error(t, err) } } func observeMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "observe_observeMetric", @@ -710,29 +736,22 @@ func observeMetric(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) + assert.NoError(t, err) + assert.True(t, declareResp.Msg.GetOk()) + + obsResp, err := client.Observe(ctx, connect.NewRequest(&metricsV1.ObserveRequest{ + Metric: &metricsV1.Metric{Name: "observe_observeMetric", Value: 100.0, Labels: []string{"test", "test2"}}, + })) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{ - Name: "observe_observeMetric", - Value: 100.0, - Labels: []string{"test", "test2"}, - }, &ret)) - assert.True(t, ret) + assert.True(t, obsResp.Msg.GetOk()) } } func counterMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "counter_CounterMetric", @@ -745,30 +764,22 @@ func counterMetric(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false + assert.True(t, declareResp.Msg.GetOk()) - assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ - Name: "counter_CounterMetric", - Value: 100.0, - Labels: []string{"type2", "section2"}, - }, &ret)) - assert.True(t, ret) + addResp, err := client.Add(ctx, connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "counter_CounterMetric", Value: 100.0, Labels: []string{"type2", "section2"}}, + })) + assert.NoError(t, err) + assert.True(t, addResp.Msg.GetOk()) } } func registerHistogram(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "histogram_registerHistogram", @@ -779,34 +790,22 @@ func registerHistogram(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false + assert.True(t, declareResp.Msg.GetOk()) - m := metrics.Metric{ - Name: "histogram_registerHistogram", - Value: 10000, - Labels: nil, - } - - err = client.Call("metrics.Add", m, &ret) + // Histogram doesn't support Add — must surface as an error. + _, err = client.Add(ctx, connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "histogram_registerHistogram", Value: 10000}, + })) assert.Error(t, err) - assert.False(t, ret) } } func subVector(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "sub_gauge_subVector", @@ -818,43 +817,28 @@ func subVector(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - ret = false + assert.True(t, declareResp.Msg.GetOk()) - m := metrics.Metric{ - Name: "sub_gauge_subVector", - Value: 100000, - Labels: []string{"core", "first"}, - } - - err = client.Call("metrics.Add", m, &ret) + addResp, err := client.Add(ctx, connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "sub_gauge_subVector", Value: 100000, Labels: []string{"core", "first"}}, + })) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - m = metrics.Metric{ - Name: "sub_gauge_subVector", - Value: 99999, - Labels: []string{"core", "first"}, - } + assert.True(t, addResp.Msg.GetOk()) - err = client.Call("metrics.Sub", m, &ret) + subResp, err := client.Sub(ctx, connect.NewRequest(&metricsV1.SubRequest{ + Metric: &metricsV1.Metric{Name: "sub_gauge_subVector", Value: 99999, Labels: []string{"core", "first"}}, + })) assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, subResp.Msg.GetOk()) } } func subMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "sub_gauge_subMetric", @@ -865,41 +849,28 @@ func subMetric(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - m := metrics.Metric{ - Name: "sub_gauge_subMetric", - Value: 100000, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Add", m, &ret) + addResp, err := client.Add(ctx, connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "sub_gauge_subMetric", Value: 100000}, + })) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - m = metrics.Metric{ - Name: "sub_gauge_subMetric", - Value: 99999, - } + assert.True(t, addResp.Msg.GetOk()) - err = client.Call("metrics.Sub", m, &ret) + subResp, err := client.Sub(ctx, connect.NewRequest(&metricsV1.SubRequest{ + Metric: &metricsV1.Metric{Name: "sub_gauge_subMetric", Value: 99999}, + })) assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, subResp.Msg.GetOk()) } } func setOnHistogram(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "histogram_setOnHistogram", @@ -911,32 +882,22 @@ func setOnHistogram(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false - - m := metrics.Metric{ - Name: "gauge_setOnHistogram", - Value: 100.0, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + // Histogram does not support Set — must surface as an error. + _, err = client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "gauge_setOnHistogram", Value: 100.0}, + })) assert.Error(t, err) - assert.False(t, ret) } } func setWithoutLabels(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "gauge_setWithoutLabels", @@ -948,32 +909,22 @@ func setWithoutLabels(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false - - m := metrics.Metric{ - Name: "gauge_setWithoutLabels", - Value: 100.0, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + // GaugeVec requires labels — Set with empty labels must error. + _, err = client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "gauge_setWithoutLabels", Value: 100.0}, + })) assert.Error(t, err) - assert.False(t, ret) } } func missingSection(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "gauge_missing_section_collector", @@ -985,33 +936,23 @@ func missingSection(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false - - m := metrics.Metric{ - Name: "gauge_missing_section_collector", - Value: 100.0, - Labels: []string{"missing"}, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + // Two-label collector with one label value — prometheus rejects the + // call, surfaces as an error on the wire. + _, err = client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "gauge_missing_section_collector", Value: 100.0, Labels: []string{"missing"}}, + })) assert.Error(t, err) - assert.False(t, ret) } } func vectorMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "gauge_2_collector", @@ -1023,33 +964,22 @@ func vectorMetric(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - - ret = false - - m := metrics.Metric{ - Name: "gauge_2_collector", - Value: 100.0, - Labels: []string{"core", "first"}, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Set", m, &ret) + setResp, err := client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "gauge_2_collector", Value: 100.0, Labels: []string{"core", "first"}}, + })) assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, setResp.Msg.GetOk()) } } func setMetric(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool + client := newMetricsClient(address) + ctx := t.Context() nc := metrics.NamedCollector{ Name: "user_gauge_collector", @@ -1060,55 +990,32 @@ func setMetric(address string) func(t *testing.T) { }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) - ret = false - - m := metrics.Metric{ - Name: "user_gauge_collector", - Value: 100.0, - } + assert.True(t, declareResp.Msg.GetOk()) - err = client.Call("metrics.Set", m, &ret) + setResp, err := client.Set(ctx, connect.NewRequest(&metricsV1.SetRequest{ + Metric: &metricsV1.Metric{Name: "user_gauge_collector", Value: 100.0}, + })) assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, setResp.Msg.GetOk()) } } func addMetricsTest(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) + client := newMetricsClient(address) + addResp, err := client.Add(t.Context(), connect.NewRequest(&metricsV1.AddRequest{ + Metric: &metricsV1.Metric{Name: "test_metrics_named_collector", Value: 10000}, + })) assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool - - m := metrics.Metric{ - Name: "test_metrics_named_collector", - Value: 10000, - Labels: nil, - } - - err = client.Call("metrics.Add", m, &ret) - assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, addResp.Msg.GetOk()) } } func declareMetricsTest(address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) - assert.NoError(t, err) - - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool - + client := newMetricsClient(address) nc := metrics.NamedCollector{ Name: "test_metrics_named_collector", Collector: metrics.Collector{ @@ -1116,36 +1023,27 @@ func declareMetricsTest(address string) func(t *testing.T) { Subsystem: "default", Type: metrics.Counter, Help: "NO HELP!", - Labels: nil, - Buckets: nil, }, } - err = client.Call("metrics.Declare", nc, &ret) + declareResp, err := client.Declare(t.Context(), connect.NewRequest(&metricsV1.DeclareRequest{Collector: toProtoCollector(nc)})) assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, declareResp.Msg.GetOk()) } } func unregisterMetric(name string, address string) func(t *testing.T) { return func(t *testing.T) { - conn, err := net.Dial(dialNetwork, address) + client := newMetricsClient(address) + resp, err := client.Unregister(t.Context(), connect.NewRequest(&metricsV1.UnregisterRequest{Name: name})) assert.NoError(t, err) - defer func() { - _ = conn.Close() - }() - client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) - var ret bool - - err = client.Call("metrics.Unregister", name, &ret) - assert.NoError(t, err) - assert.True(t, ret) + assert.True(t, resp.Msg.GetOk()) } } func echoHTTP(port string) func(t *testing.T) { return func(t *testing.T) { - req, err := http.NewRequest("GET", "http://127.0.0.1:"+port, nil) + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://127.0.0.1:"+port, nil) assert.NoError(t, err) r, err := http.DefaultClient.Do(req) @@ -1160,7 +1058,11 @@ func echoHTTP(port string) func(t *testing.T) { // get request and return body func get(address string) (string, error) { - r, err := http.Get(address) //nolint:gosec + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, address, nil) + if err != nil { + return "", err + } + r, err := http.DefaultClient.Do(req) if err != nil { return "", err } @@ -1180,7 +1082,11 @@ func get(address string) (string, error) { // get request and return body func getIPV6(address string) (string, error) { - r, err := http.Get(address) //nolint:gosec + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, address, nil) + if err != nil { + return "", err + } + r, err := http.DefaultClient.Do(req) if err != nil { return "", err } From efb2ab14e68be4df18cf0db6e6ab97b3b9c78ca6 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 13 May 2026 12:49:50 +0200 Subject: [PATCH 2/5] fixup: address PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - restore exact legacy log-message strings to preserve TestMetricsDifferentRPCCalls count assertions ("metric successfully added", "subtracting value from metric", "subtracting operation finished successfully", "observe operation finished successfully", "set operation finished successfully", "declaring new metric", "failed to get metrics with label values", "required labels for collector", "collector was successfully unregistered") - map GetMetricWithLabelValues / missing-labels errors to CodeInvalidArgument (caller-input class, not internal) - bump test config metrics.address 127.0.0.1:2115 → :2118 to avoid port collision with .rr-metrics-http.yaml - add Summary/SummaryVec Observe + unspecified-CollectorType Declare coverage in TestMetricsConnectAPI - drop rpc_test.go: msgpack/json wire-format tests for the deleted Go-side Metric struct (relic of the goridge codec) - end-of-migration go get -u all: server/v6 beta.5→beta.6, quic-go, sysconf --- go.mod | 2 - go.sum | 4 -- rpc.go | 40 +++++++++--------- rpc_test.go | 65 ------------------------------ tests/configs/.rr-metrics-api.yaml | 2 +- tests/go.mod | 10 ++--- tests/go.sum | 24 +++++------ tests/metrics_api_test.go | 32 +++++++++++++++ 8 files changed, 68 insertions(+), 111 deletions(-) delete mode 100644 rpc_test.go diff --git a/go.mod b/go.mod index 247a2c8..507623d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/roadrunner-server/endure/v2 v2.6.2 github.com/roadrunner-server/errors v1.5.0 github.com/stretchr/testify v1.11.1 - github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/sys v0.44.0 ) @@ -26,7 +25,6 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/text v0.37.0 // indirect diff --git a/go.sum b/go.sum index 437c0e6..d35f934 100644 --- a/go.sum +++ b/go.sum @@ -47,10 +47,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= diff --git a/rpc.go b/rpc.go index 18e227d..02ffff9 100644 --- a/rpc.go +++ b/rpc.go @@ -37,7 +37,7 @@ func (r *rpc) Add(_ context.Context, req *connect.Request[metricsV1.AddRequest]) case prometheus.Gauge: c.Add(m.GetValue()) case *prometheus.GaugeVec: - gv, err := vecMetric(c, m) + gv, err := vecMetric(r, c, m) if err != nil { return nil, err } @@ -45,7 +45,7 @@ func (r *rpc) Add(_ context.Context, req *connect.Request[metricsV1.AddRequest]) case prometheus.Counter: c.Add(m.GetValue()) case *prometheus.CounterVec: - cv, err := vecMetric(c, m) + cv, err := vecMetric(r, c, m) if err != nil { return nil, err } @@ -55,12 +55,13 @@ func (r *rpc) Add(_ context.Context, req *connect.Request[metricsV1.AddRequest]) fmt.Errorf("%w: %s does not support Add", errUnsupportedOpForCol, m.GetName())) } + r.log.Debug("metric successfully added", "name", m.GetName(), "labels", m.GetLabels(), "value", m.GetValue()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } func (r *rpc) Sub(_ context.Context, req *connect.Request[metricsV1.SubRequest]) (*connect.Response[metricsV1.Response], error) { m := req.Msg.GetMetric() - r.log.Debug("subtracting metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) + r.log.Debug("subtracting value from metric", "name", m.GetName(), "value", m.GetValue(), "labels", m.GetLabels()) col, code, err := r.lookupCollector(m.GetName()) if err != nil { @@ -71,7 +72,7 @@ func (r *rpc) Sub(_ context.Context, req *connect.Request[metricsV1.SubRequest]) case prometheus.Gauge: c.Sub(m.GetValue()) case *prometheus.GaugeVec: - gv, err := vecMetric(c, m) + gv, err := vecMetric(r, c, m) if err != nil { return nil, err } @@ -81,6 +82,7 @@ func (r *rpc) Sub(_ context.Context, req *connect.Request[metricsV1.SubRequest]) fmt.Errorf("%w: %s does not support Sub", errUnsupportedOpForCol, m.GetName())) } + r.log.Debug("subtracting operation finished successfully", "name", m.GetName(), "labels", m.GetLabels(), "value", m.GetValue()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } @@ -97,13 +99,13 @@ func (r *rpc) Observe(_ context.Context, req *connect.Request[metricsV1.ObserveR case prometheus.Histogram: c.Observe(m.GetValue()) case *prometheus.HistogramVec: - ov, err := vecObserver(c, m) + ov, err := vecMetric[prometheus.Observer](r, c, m) if err != nil { return nil, err } ov.Observe(m.GetValue()) case *prometheus.SummaryVec: - ov, err := vecObserver(c, m) + ov, err := vecMetric[prometheus.Observer](r, c, m) if err != nil { return nil, err } @@ -113,6 +115,7 @@ func (r *rpc) Observe(_ context.Context, req *connect.Request[metricsV1.ObserveR fmt.Errorf("%w: %s does not support Observe", errUnsupportedOpForCol, m.GetName())) } + r.log.Debug("observe operation finished successfully", "name", m.GetName(), "labels", m.GetLabels(), "value", m.GetValue()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } @@ -129,7 +132,7 @@ func (r *rpc) Set(_ context.Context, req *connect.Request[metricsV1.SetRequest]) case prometheus.Gauge: c.Set(m.GetValue()) case *prometheus.GaugeVec: - gv, err := vecMetric(c, m) + gv, err := vecMetric(r, c, m) if err != nil { return nil, err } @@ -139,6 +142,7 @@ func (r *rpc) Set(_ context.Context, req *connect.Request[metricsV1.SetRequest]) fmt.Errorf("%w: %s does not support Set", errUnsupportedOpForCol, m.GetName())) } + r.log.Debug("set operation finished successfully", "name", m.GetName(), "labels", m.GetLabels(), "value", m.GetValue()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } @@ -149,7 +153,7 @@ func (r *rpc) Declare(_ context.Context, req *connect.Request[metricsV1.DeclareR r.p.mu.Lock() defer r.p.mu.Unlock() - r.log.Debug("declaring metric", "name", nc.GetName(), "type", nc.GetCollector().GetType(), "namespace", nc.GetCollector().GetNamespace()) + r.log.Debug("declaring new metric", "name", nc.GetName(), "type", nc.GetCollector().GetType(), "namespace", nc.GetCollector().GetNamespace()) if _, exist := r.p.collectors.Load(nc.GetName()); exist { r.log.Warn("metric with provided name already exist", "name", nc.GetName()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil @@ -165,7 +169,7 @@ func (r *rpc) Declare(_ context.Context, req *connect.Request[metricsV1.DeclareR } r.p.collectors.Store(nc.GetName(), &collector{col: promCol, registered: true}) - r.log.Debug("metric registered", "name", nc.GetName()) + r.log.Debug("metric successfully added", "name", nc.GetName(), "type", nc.GetCollector().GetType(), "namespace", nc.GetCollector().GetNamespace()) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } @@ -183,10 +187,10 @@ func (r *rpc) Unregister(_ context.Context, req *connect.Request[metricsV1.Unreg return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("collectors map held non-*collector for %s", name)) } if r.p.registry.Unregister(col.col) { - r.log.Debug("collector unregistered", "name", name) + r.log.Debug("collector was successfully unregistered", "name", name) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } - r.log.Debug("collector deleted from RR but not from prometheus registry", "name", name) + r.log.Debug("collector was deleted from the RR registry but not from the prometheus collector", "name", name) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } @@ -203,27 +207,23 @@ func (r *rpc) lookupCollector(name string) (prometheus.Collector, connect.Code, return col.col, 0, nil } -func vecMetric[T interface { +func vecMetric[V any, T interface { GetMetricWithLabelValues(lvs ...string) (V, error) -}, V any](c T, m *metricsV1.Metric) (V, error) { +}](r *rpc, c T, m *metricsV1.Metric) (V, error) { var zero V if len(m.GetLabels()) == 0 { + r.log.Error("required labels for collector", "collector", m.GetName()) return zero, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%w: %s", errRequiredLabels, m.GetName())) } v, err := c.GetMetricWithLabelValues(m.GetLabels()...) if err != nil { - return zero, connect.NewError(connect.CodeInternal, err) + r.log.Error("failed to get metrics with label values", "collector", m.GetName(), "labels", m.GetLabels()) + return zero, connect.NewError(connect.CodeInvalidArgument, err) } return v, nil } -func vecObserver[T interface { - GetMetricWithLabelValues(lvs ...string) (prometheus.Observer, error) -}](c T, m *metricsV1.Metric) (prometheus.Observer, error) { - return vecMetric[T, prometheus.Observer](c, m) -} - func buildPromCollector(nc *metricsV1.NamedCollector) (prometheus.Collector, error) { col := nc.GetCollector() switch col.GetType() { diff --git a/rpc_test.go b/rpc_test.go deleted file mode 100644 index 2989797..0000000 --- a/rpc_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package metrics - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/vmihailenco/msgpack/v5" -) - -type unmarshal func([]byte, any) error - -func Test_Metric_Unmarshal(t *testing.T) { - tests := []struct { - name string - unmarshaler unmarshal - payload []byte - want Metric - }{ - {"json", - json.Unmarshal, - []byte(`{"name":"test","value":123,"labels": ["test1", "test2"]}`), - Metric{ - Name: "test", - Value: 123, - Labels: []string{"test1", "test2"}, - }, - }, - { - "msgpack", - msgpack.Unmarshal, - // {"name":"test","value":123,"labels": ["test1", "test2"]} - []byte{0x83, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa4, 0x74, 0x65, 0x73, 0x74, 0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x7b, 0xa6, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x92, 0xa5, 0x74, 0x65, 0x73, 0x74, 0x31, 0xa5, 0x74, 0x65, 0x73, 0x74, 0x32}, - Metric{ - Name: "test", - Value: 123, - Labels: []string{"test1", "test2"}, - }, - }, - { - "msgpack pascal case", - msgpack.Unmarshal, - // {"Name":"test","Value":123,"Labels": ["test1", "test2"]} - []byte{0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65, 0xa4, 0x74, 0x65, 0x73, 0x74, 0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x7b, 0xa6, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x92, 0xa5, 0x74, 0x65, 0x73, 0x74, 0x31, 0xa5, 0x74, 0x65, 0x73, 0x74, 0x32}, - Metric{ - Name: "test", - Value: 123, - Labels: []string{"test1", "test2"}, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := Metric{} - err := test.unmarshaler(test.payload, &got) - if err != nil { - t.Error(err) - return - } - if !reflect.DeepEqual(test.want, got) { - t.Errorf("Want: %v, Got: %v", test.want, got) - } - }) - } -} diff --git a/tests/configs/.rr-metrics-api.yaml b/tests/configs/.rr-metrics-api.yaml index c3a5a68..89c862a 100644 --- a/tests/configs/.rr-metrics-api.yaml +++ b/tests/configs/.rr-metrics-api.yaml @@ -4,7 +4,7 @@ rpc: listen: tcp://127.0.0.1:6001 metrics: - address: "127.0.0.1:2115" + address: "127.0.0.1:2118" collect: app_metric_counter: type: counter diff --git a/tests/go.mod b/tests/go.mod index d9b34f8..639b5d7 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -15,7 +15,7 @@ require ( github.com/roadrunner-server/metrics/v6 v6.0.0 github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2 github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4 - github.com/roadrunner-server/server/v6 v6.0.0-beta.5 + github.com/roadrunner-server/server/v6 v6.0.0-beta.6 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.54.0 google.golang.org/grpc v1.81.0 @@ -54,7 +54,7 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/quic-go v0.59.1 // indirect github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 // indirect github.com/roadrunner-server/context v1.3.0 // indirect github.com/roadrunner-server/errors v1.5.0 // indirect @@ -69,8 +69,8 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/assert v1.3.1 // indirect github.com/zeebo/blake3 v0.2.4 // indirect @@ -91,7 +91,7 @@ require ( golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.45.0 // indirect - google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 8fbdb99..bd6e84e 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -84,8 +84,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9 h1:iD09+1Sc6ZMxEOJsj7YIblzEN1ORZ+NLH7YYO0nqASs= github.com/roadrunner-server/api-go/v6 v6.0.0-beta.9/go.mod h1:4BbPXAqT0sOG2EVRcymESMwxaKSpHl4hG+UDoCepWu0= github.com/roadrunner-server/api-plugins/v6 v6.0.0-beta.2 h1:GqsZzWQ5jMXRF1O/b8IqFz9PLpS7Ui0K4OyACLql2MI= @@ -112,8 +112,8 @@ github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2 h1:e6Z5YFRwi1Tcr9T5sgec github.com/roadrunner-server/prometheus/v6 v6.0.0-beta.2/go.mod h1:OYfupkw6fQjZHf83T87KUXNx4/IwCMlBA3J3sqDx5Wg= github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4 h1:Qj2nrHIWOHE9Tys+FBG2IdoPtzgIUh6juQ5wXLGGDMw= github.com/roadrunner-server/rpc/v6 v6.0.0-beta.4/go.mod h1:k5KT3fpnJVd27m0HbGGBiTPXlWI6eJdd6C+ohp5IE0U= -github.com/roadrunner-server/server/v6 v6.0.0-beta.5 h1:poCPSHc768UtMqJRgW3rZT5pDboOnwOD4qc28hhXJ4I= -github.com/roadrunner-server/server/v6 v6.0.0-beta.5/go.mod h1:SbODuCzC2gcbFhAmJDWvjf34pPrUWP5NxxVsTRQDuZ4= +github.com/roadrunner-server/server/v6 v6.0.0-beta.6 h1:CPtH4eIYkeRKi5cPXxb0+J+LI824cqhIGXAfcH+nkjA= +github.com/roadrunner-server/server/v6 v6.0.0-beta.6/go.mod h1:SbODuCzC2gcbFhAmJDWvjf34pPrUWP5NxxVsTRQDuZ4= github.com/roadrunner-server/tcplisten v1.5.2 h1:nn8yXYrhRDkfQ9AAu4V075uT4fZRmOnpxkawgE+bWPA= github.com/roadrunner-server/tcplisten v1.5.2/go.mod h1:DufGBz7Dlx2KrNe/4RukEvGMTqZKB0Uve1GztwcyyR8= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -134,14 +134,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= @@ -198,8 +194,8 @@ golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= 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 v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= -google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= +google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 h1:rhBdfmsOlOZIvz3Y5/BdUzPg2CkO8L7QQPKj96B8554= +google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60/go.mod h1:8xo2Pj1b20ZOCpzlU3B9qieMwVIAXx1QVZWLMlPL6sM= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= diff --git a/tests/metrics_api_test.go b/tests/metrics_api_test.go index d3c06f6..c214c28 100644 --- a/tests/metrics_api_test.go +++ b/tests/metrics_api_test.go @@ -145,6 +145,38 @@ func TestMetricsConnectAPI(t *testing.T) { _, err = client.Unregister(ctx, connect.NewRequest(&metricsV1.UnregisterRequest{Name: "missing"})) require.Error(t, err) require.Equal(t, connect.CodeNotFound, connect.CodeOf(err)) + + // Declare with an unspecified collector type → CodeInvalidArgument (the + // zero value when the proto Type field is omitted). + _, err = client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "api_unspecified", + Collector: &metricsV1.Collector{Help: "no type"}, + }, + })) + require.Error(t, err) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + + // SummaryVec Observe — declare a summary with labels + objectives, then + // observe with the right label values. Locks in the SummaryVec branch of + // the rpc.go Observe type-switch. + _, err = client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "api_summary_vec", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_SUMMARY, + Help: "API test summary", + Labels: []string{"region"}, + Objectives: []*metricsV1.Objective{{Quantile: 0.5, Error: 0.05}, {Quantile: 0.99, Error: 0.001}}, + }, + }, + })) + require.NoError(t, err) + obsResp, err := client.Observe(ctx, connect.NewRequest(&metricsV1.ObserveRequest{ + Metric: &metricsV1.Metric{Name: "api_summary_vec", Value: 0.42, Labels: []string{"eu"}}, + })) + require.NoError(t, err) + require.True(t, obsResp.Msg.GetOk()) } // TestMetricsHTTPApi exercises the metrics RPCs through plain HTTP/1.1 with From 947e840d650c19dc788003d6d0fc60e9005fa2bb Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 13 May 2026 16:47:11 +0200 Subject: [PATCH 3/5] fixup: preserve legacy Unregister Ok=false contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When prometheus.registry.Unregister returns false, the old goridge handler left the RPC return value at its zero (false) — collector removed from RR map but not from prometheus, caller could detect the divergence. I had collapsed both branches to Ok:true. Restoring the legacy contract. --- rpc.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rpc.go b/rpc.go index 02ffff9..e766477 100644 --- a/rpc.go +++ b/rpc.go @@ -190,8 +190,11 @@ func (r *rpc) Unregister(_ context.Context, req *connect.Request[metricsV1.Unreg r.log.Debug("collector was successfully unregistered", "name", name) return connect.NewResponse(&metricsV1.Response{Ok: true}), nil } + // Preserves legacy contract: prometheus refused to unregister (already + // gone, or never registered there). The collector is removed from our map + // either way, but the caller deserves to know prometheus state diverged. r.log.Debug("collector was deleted from the RR registry but not from the prometheus collector", "name", name) - return connect.NewResponse(&metricsV1.Response{Ok: true}), nil + return connect.NewResponse(&metricsV1.Response{Ok: false}), nil } func (r *rpc) lookupCollector(name string) (prometheus.Collector, connect.Code, error) { From acc1fac50881b9968127b084248980a46fdeaffe Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 13 May 2026 16:53:11 +0200 Subject: [PATCH 4/5] ci: drop rpc_test.go from unit-test file list rpc_test.go was removed in efb2ab1 (msgpack/json wire-format tests for the deleted Go-side Metric struct, goridge codec relic). The workflow's explicit file list still referenced it, so unit tests failed with 'no Go files' or 'file not found' on the runner. --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index de28b9b..03ac9a8 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -69,7 +69,7 @@ jobs: - name: Run metrics unit tests with coverage run: | mkdir ./tests/coverage-ci - go test -timeout 20m -v -race -cover -tags=debug -failfast -coverpkg=$(cat ./tests/pkgs.txt) -coverprofile=./tests/coverage-ci/metrics_u.out -covermode=atomic config.go config_test.go plugin.go rpc.go rpc_test.go + go test -timeout 20m -v -race -cover -tags=debug -failfast -coverpkg=$(cat ./tests/pkgs.txt) -coverprofile=./tests/coverage-ci/metrics_u.out -covermode=atomic config.go config_test.go plugin.go rpc.go - name: Run metrics e2e tests with coverage run: | From d8dc27b88d499d609151ab9ee0bb34711513ae6d Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 13 May 2026 17:33:48 +0200 Subject: [PATCH 5/5] test(metrics): cover scalar Summary Observe via the Histogram type-switch branch prometheus.Histogram and prometheus.Summary share identical method sets, so a scalar Summary value matches the existing case prometheus.Histogram branch (Go type-switch picks the first interface match in source order). The new test locks this in; the type-switch comment explains why a dedicated Summary case would be dead code (staticcheck SA4020). --- go.work.sum | 135 +++++++++++++++++++++++++++++++++++++- rpc.go | 4 ++ tests/metrics_api_test.go | 20 ++++++ 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/go.work.sum b/go.work.sum index 4326aa1..0fefaae 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,4 @@ +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= @@ -7,55 +8,80 @@ cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/accessapproval v1.8.6/go.mod h1:FfmTs7Emex5UvfnnpMkhuNkRCP85URnBFt5ClLxhZaQ= cloud.google.com/go/accessapproval v1.8.8/go.mod h1:RFwPY9JDKseP4gJrX1BlAVsP5O6kI8NdGlTmaeDefmk= +cloud.google.com/go/accessapproval v1.13.0/go.mod h1:7bmInw17bQX+ZPi7YmReC3xKymDrMmxXaUnaI6zQOqI= cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk= cloud.google.com/go/accesscontextmanager v1.9.7/go.mod h1:i6e0nd5CPcrh7+YwGq4bKvju5YB9sgoAip+mXU73aMM= +cloud.google.com/go/accesscontextmanager v1.14.0/go.mod h1:VO15iVnsM0FO9Dt8hSFPgkuHRZjq6LEYZq1szJ27U2k= cloud.google.com/go/aiplatform v1.86.0/go.mod h1:xp3wFix8imliXkVpgMRkjnreJYTaNzLF44GOrnIENto= cloud.google.com/go/aiplatform v1.114.0/go.mod h1:W5yMrpIuHG/CSK8iF7XnwIfCJu6dcLRQ0cTqGR5vwwE= +cloud.google.com/go/aiplatform v1.125.0/go.mod h1:yWTZiCunYDnyxeWWD14tDo6+BMlvAUCC5VxuxhvbrVI= cloud.google.com/go/analytics v0.28.1/go.mod h1:iPaIVr5iXPB3JzkKPW1JddswksACRFl3NSHgVHsuYC4= cloud.google.com/go/analytics v0.30.1/go.mod h1:V/FnINU5kMOsttZnKPnXfKi6clJUHTEXUKQjHxcNK8A= +cloud.google.com/go/analytics v0.35.0/go.mod h1:V9Qef2N0y8GDqQ9FTlmM2XpDEMYonZJRPSUNGZlPCcc= cloud.google.com/go/apigateway v1.7.6/go.mod h1:SiBx36VPjShaOCk8Emf63M2t2c1yF+I7mYZaId7OHiA= cloud.google.com/go/apigateway v1.7.7/go.mod h1:j1bCmrUK1BzVHpiIyTApxB7cRyhivKzltqLmp6j6i7U= +cloud.google.com/go/apigateway v1.12.0/go.mod h1:f3Sk8Tdh1Ty5HR7kgbWB6Yu1M82LM+nIr5DTMZnLZWk= cloud.google.com/go/apigeeconnect v1.7.6/go.mod h1:zqDhHY99YSn2li6OeEjFpAlhXYnXKl6DFb/fGu0ye2w= cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U= +cloud.google.com/go/apigeeconnect v1.12.0/go.mod h1:mYJekCKZHc2ia5yZX5lwtexTn9CzsOfb6+sh/2hi42Q= cloud.google.com/go/apigeeregistry v0.9.6/go.mod h1:AFEepJBKPtGDfgabG2HWaLH453VVWWFFs3P4W00jbPs= cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg= +cloud.google.com/go/apigeeregistry v0.15.0/go.mod h1:o+j6eA8hYhTWX5gEqMMBVDWY+/QQFrYe/YJBsO19pn0= cloud.google.com/go/appengine v1.9.6/go.mod h1:jPp9T7Opvzl97qytaRGPwoH7pFI3GAcLDaui1K8PNjY= cloud.google.com/go/appengine v1.9.7/go.mod h1:y1XpGVeAhbsNzHida79cHbr3pFRsym0ob8xnC8yphbo= +cloud.google.com/go/appengine v1.14.0/go.mod h1:JMjrVFg+YgfksZCWbtA3TgbKbPfZZtapB9cGL/5WVnM= cloud.google.com/go/area120 v0.9.6/go.mod h1:qKSokqe0iTmwBDA3tbLWonMEnh0pMAH4YxiceiHUed4= cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY= +cloud.google.com/go/area120 v0.15.0/go.mod h1:jD1fw9W4xxIZMY68g7PpbCPleoeGddFs5jPcdhfg3+Y= cloud.google.com/go/artifactregistry v1.17.1/go.mod h1:06gLv5QwQPWtaudI2fWO37gfwwRUHwxm3gA8Fe568Hc= cloud.google.com/go/artifactregistry v1.19.0/go.mod h1:UEAPCgHDFC1q+A8nnVxXHPEy9KCVOeavFBF1fEChQvU= +cloud.google.com/go/artifactregistry v1.25.0/go.mod h1:aMmdtqKVmbuxCCb/NGDJYZHsK6AtqlcyvD05ACzs1n8= cloud.google.com/go/asset v1.21.0/go.mod h1:0lMJ0STdyImZDSCB8B3i/+lzIquLBpJ9KZ4pyRvzccM= cloud.google.com/go/asset v1.22.0/go.mod h1:q80JP2TeWWzMCazYnrAfDf36aQKf1QiKzzpNLflJwf8= +cloud.google.com/go/asset v1.27.0/go.mod h1:+HaDReZQAh/0syAf0uTMeUrMfXikr+KKyDtCdvf7j4M= cloud.google.com/go/assuredworkloads v1.12.6/go.mod h1:QyZHd7nH08fmZ+G4ElihV1zoZ7H0FQCpgS0YWtwjCKo= cloud.google.com/go/assuredworkloads v1.13.0/go.mod h1:o/oHEOnUlribR+uJWTKQo8A5RhSl9K9FNeMOew4TJ3M= +cloud.google.com/go/assuredworkloads v1.18.0/go.mod h1:zBnVYn0E+sDW/mhEmcg1R8+8tguXrtBgmfGY0q34kss= cloud.google.com/go/automl v1.14.7/go.mod h1:8a4XbIH5pdvrReOU72oB+H3pOw2JBxo9XTk39oljObE= cloud.google.com/go/automl v1.15.0/go.mod h1:U9zOtQb8zVrFNGTuW3BfxeqmLyeleLgT9B12EaXfODg= +cloud.google.com/go/automl v1.20.0/go.mod h1:OkHxjbVDblDafhwuP8yEkz1xcUJhgcbhbsieCW7GaiI= cloud.google.com/go/baremetalsolution v1.3.6/go.mod h1:7/CS0LzpLccRGO0HL3q2Rofxas2JwjREKut414sE9iM= cloud.google.com/go/baremetalsolution v1.4.0/go.mod h1:K6C6g4aS8LW95I0fEHZiBsBlh0UxwDLGf+S/vyfXbvg= +cloud.google.com/go/baremetalsolution v1.9.0/go.mod h1:o+stutiS8t+HmjNIG92Gkn8H9+5/q27d6lQp7e9GWdg= cloud.google.com/go/batch v1.12.2/go.mod h1:tbnuTN/Iw59/n1yjAYKV2aZUjvMM2VJqAgvUgft6UEU= cloud.google.com/go/batch v1.14.0/go.mod h1:oeQveyG6NDS/ks2ilOP4LzKRmuIaI7GLe0CkR7WF6pk= +cloud.google.com/go/batch v1.19.0/go.mod h1:dpWfhLmLQZqsTBAFYjZA3pS04fCY5ttTenZcWmSeILw= cloud.google.com/go/beyondcorp v1.1.6/go.mod h1:V1PigSWPGh5L/vRRmyutfnjAbkxLI2aWqJDdxKbwvsQ= cloud.google.com/go/beyondcorp v1.2.0/go.mod h1:sszcgxpPPBEfLzbI0aYCTg6tT1tyt3CmKav3NZIUcvI= +cloud.google.com/go/beyondcorp v1.7.0/go.mod h1:vujdO0wfsBV2y1egrJxGtwKZr5P5V6bIHKWp1phWHBY= cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.67.0/go.mod h1:HQeP1AHFuAz0Y55heDSb0cjZIhnEkuwFRBGo6EEKHug= cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= +cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU= cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= +cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= cloud.google.com/go/billing v1.20.4/go.mod h1:hBm7iUmGKGCnBm6Wp439YgEdt+OnefEq/Ib9SlJYxIU= cloud.google.com/go/billing v1.21.0/go.mod h1:ZGairB3EVnb3i09E2SxFxo50p5unPaMTuo1jh6jW9js= +cloud.google.com/go/billing v1.26.0/go.mod h1:axqDO1uHegh7u5qngkTfqN1djAeLGsWAFAblERgmgEk= cloud.google.com/go/binaryauthorization v1.9.5/go.mod h1:CV5GkS2eiY461Bzv+OH3r5/AsuB6zny+MruRju3ccB8= cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw= +cloud.google.com/go/binaryauthorization v1.15.0/go.mod h1:+0CndCJPtcHuVCNok+qQskWvbP5Sp5m6eGL8Vpu5mss= cloud.google.com/go/certificatemanager v1.9.5/go.mod h1:kn7gxT/80oVGhjL8rurMUYD36AOimgtzSBPadtAeffs= cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= +cloud.google.com/go/certificatemanager v1.14.0/go.mod h1:QOA8qRoM6/Ik03+srLnBykenGTy0fk78dnPcx5ZWOW8= cloud.google.com/go/channel v1.19.5/go.mod h1:vevu+LK8Oy1Yuf7lcpDbkQQQm5I7oiY5fFTn3uwfQLY= cloud.google.com/go/channel v1.21.0/go.mod h1:8v3TwHtgLmFxTpL2U+e10CLFOQN8u/Vr9RhYcJUS3y8= +cloud.google.com/go/channel v1.26.0/go.mod h1:04T5Wjq+mHlvEUNzExydnBW1vO64q3Q2Wsblp/dpBxY= cloud.google.com/go/cloudbuild v1.22.2/go.mod h1:rPyXfINSgMqMZvuTk1DbZcbKYtvbYF/i9IXQ7eeEMIM= cloud.google.com/go/cloudbuild v1.25.0/go.mod h1:lCu+T6IPkobPo2Nw+vCE7wuaAl9HbXLzdPx/tcF+oWo= +cloud.google.com/go/cloudbuild v1.30.0/go.mod h1:rg52xEmndQQPiC9NV/8sCaVtKxHMU9D9MeU+oE9VGKA= cloud.google.com/go/clouddms v1.8.7/go.mod h1:DhWLd3nzHP8GoHkA6hOhso0R9Iou+IGggNqlVaq/KZ4= cloud.google.com/go/clouddms v1.8.8/go.mod h1:QtCyw+a73dlkDb2q20aTAPvfaTZCepDDi6Gb1AKq0a4= +cloud.google.com/go/clouddms v1.13.0/go.mod h1:aMgrOZ+/EKF/PL+h1sDbS+7fAIYV5rTwD+G/apCeHQk= cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36GeomEuy9gL4gLmU= cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4= +cloud.google.com/go/cloudtasks v1.18.0/go.mod h1:3KeCxwtGEyaySL7CR3lMmEa2I4mq1ynXdgmfNiO4RYE= cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= @@ -65,57 +91,82 @@ cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1Yl cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute v1.37.0/go.mod h1:AsK4VqrSyXBo4SMbRtfAO1VfaMjUEjEwv1UB/AwVp5Q= cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= +cloud.google.com/go/compute v1.62.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= cloud.google.com/go/contactcenterinsights v1.17.4/go.mod h1:kZe6yOnKDfpPz2GphDHynxk/Spx+53UX/pGf+SmWAKM= +cloud.google.com/go/contactcenterinsights v1.22.0/go.mod h1:2Crd36H59Lwkt4gWrLgmnbnF59IIZIa3XYt1gtNqJkQ= cloud.google.com/go/container v1.42.4/go.mod h1:wf9lKc3ayWVbbV/IxKIDzT7E+1KQgzkzdxEJpj1pebE= cloud.google.com/go/container v1.46.0/go.mod h1:A7gMqdQduTk46+zssWDTKbGS2z46UsJNXfKqvMI1ZO4= +cloud.google.com/go/container v1.49.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= cloud.google.com/go/containeranalysis v0.14.1/go.mod h1:28e+tlZgauWGHmEbnI5UfIsjMmrkoR1tFN0K2i71jBI= cloud.google.com/go/containeranalysis v0.14.2/go.mod h1:FjppROiUtP9cyMegdWdY/TsBSGc6kqh1GjA2NOJXXL8= +cloud.google.com/go/containeranalysis v0.19.0/go.mod h1:Zq0XHzUIa0oTa7H6aSR8HWqeJnoRI9syUcYJzfozjZQ= cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= +cloud.google.com/go/datacatalog v1.31.0/go.mod h1:MP8V3kNuESnwMk4mB6zdWmw/4KQ5xZ8dyUNVsggqN5I= cloud.google.com/go/dataflow v0.11.0/go.mod h1:gNHC9fUjlV9miu0hd4oQaXibIuVYTQvZhMdPievKsPk= cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y= +cloud.google.com/go/dataflow v0.16.0/go.mod h1:BWhSrIGmsMfuYj3J+nJ2Tw7tplRR6r28kvRiqCD3WlQ= cloud.google.com/go/dataform v0.11.2/go.mod h1:IMmueJPEKpptT2ZLWlvIYjw6P/mYHHxA7/SUBiXqZUY= cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE= +cloud.google.com/go/dataform v0.19.0/go.mod h1:i1a0zkS751kvrY1IIPpUQZ77H5doxx7cs0AP3hnXTMk= cloud.google.com/go/datafusion v1.8.6/go.mod h1:fCyKJF2zUKC+O3hc2F9ja5EUCAbT4zcH692z8HiFZFw= cloud.google.com/go/datafusion v1.8.7/go.mod h1:4dkFb1la41qCEXh1AzYtFwl842bu2ikTUXyKhjvFCb0= +cloud.google.com/go/datafusion v1.13.0/go.mod h1:MQdANs3I/4gitzY+mTBx27rrQyMiUg8uc2Z4TPLWWfc= cloud.google.com/go/datalabeling v0.9.6/go.mod h1:n7o4x0vtPensZOoFwFa4UfZgkSZm8Qs0Pg/T3kQjXSM= cloud.google.com/go/datalabeling v0.9.7/go.mod h1:EEUVn+wNn3jl19P2S13FqE1s9LsKzRsPuuMRq2CMsOk= +cloud.google.com/go/datalabeling v0.14.0/go.mod h1:DYjvP4RhQ0332YgO22APYlBjCebb+SCaS0e2KApDq/Q= cloud.google.com/go/dataplex v1.25.2/go.mod h1:AH2/a7eCYvFP58scJGR7YlSY9qEhM8jq5IeOA/32IZ0= cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= +cloud.google.com/go/dataplex v1.34.0/go.mod h1:sOazL+Bs/PTxiMHQ5yBboBvEW9qPrpGogx3+RAgfIt8= cloud.google.com/go/dataproc/v2 v2.11.2/go.mod h1:xwukBjtfiO4vMEa1VdqyFLqJmcv7t3lo+PbLDcTEw+g= cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= +cloud.google.com/go/dataproc/v2 v2.21.0/go.mod h1:oARVSa38kAHvSuG+cozsrY2sE6UajGuvOOf9vS+ADHI= cloud.google.com/go/dataqna v0.9.7/go.mod h1:4ac3r7zm7Wqm8NAc8sDIDM0v7Dz7d1e/1Ka1yMFanUM= cloud.google.com/go/dataqna v0.9.8/go.mod h1:2lHKmGPOqzzuqCc5NI0+Xrd5om4ulxGwPpLB4AnFgpA= +cloud.google.com/go/dataqna v0.13.0/go.mod h1:XiVVFTOEJLBSvm3ILbyjXngGQYpjb/66MSksqz/56fs= cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= +cloud.google.com/go/datastore v1.23.0/go.mod h1:bOvQQekv4VACRJmH/MBy12MT6M3udfTuCyxw+tzY+8s= cloud.google.com/go/datastream v1.14.1/go.mod h1:JqMKXq/e0OMkEgfYe0nP+lDye5G2IhIlmencWxmesMo= cloud.google.com/go/datastream v1.15.1/go.mod h1:aV1Grr9LFon0YvqryE5/gF1XAhcau2uxN2OvQJPpqRw= +cloud.google.com/go/datastream v1.20.0/go.mod h1:uoWTtfP20W8MXuV2DPcl5zqnVsxQ9QEmmBHX858oYTQ= cloud.google.com/go/deploy v1.27.1/go.mod h1:il2gxiMgV3AMlySoQYe54/xpgVDoEh185nj4XjJ+GRk= cloud.google.com/go/deploy v1.27.3/go.mod h1:7LFIYYTSSdljYRqY3n+JSmIFdD4lv6aMD5xg0crB5iw= +cloud.google.com/go/deploy v1.32.0/go.mod h1:lUG7maG/NkoTXmQ8G1mtcVymnbizfDJh6ER7vljVa/U= cloud.google.com/go/dialogflow v1.68.2/go.mod h1:E0Ocrhf5/nANZzBju8RX8rONf0PuIvz2fVj3XkbAhiY= cloud.google.com/go/dialogflow v1.74.0/go.mod h1:jlKHmd3/KdvWWhGZjoCnWQAQNOMHOhDK6DQ430p3T1I= +cloud.google.com/go/dialogflow v1.82.0/go.mod h1:UtuiGOq9gAlTz9u4Vt+q1syMrx9ANQzTk+lC3WDdSOw= cloud.google.com/go/dlp v1.22.1/go.mod h1:Gc7tGo1UJJTBRt4OvNQhm8XEQ0i9VidAiGXBVtsftjM= cloud.google.com/go/dlp v1.28.0/go.mod h1:C3od1fIK8lf7Kr62aU1Uh0z4OL5Z8s3do3znAiEupAw= +cloud.google.com/go/dlp v1.34.0/go.mod h1:+haQd/n0QTv5BK7wZnCk2qctd5sfKL50jjh9E6N0d/Q= cloud.google.com/go/documentai v1.37.0/go.mod h1:qAf3ewuIUJgvSHQmmUWvM3Ogsr5A16U2WPHmiJldvLA= cloud.google.com/go/documentai v1.40.0/go.mod h1:oDTm0aoG8ldKucW/yzRrLbaTO0NvtgGAWm5KPAT5iNY= +cloud.google.com/go/documentai v1.48.0/go.mod h1:mGjfbNf0cqCHKgxMZZV7frbfoF9T2hKkU1h88QyOy3c= cloud.google.com/go/domains v0.10.6/go.mod h1:3xzG+hASKsVBA8dOPc4cIaoV3OdBHl1qgUpAvXK7pGY= cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ= +cloud.google.com/go/domains v0.15.0/go.mod h1:BjoSVNc+LVwoHMnE2fxTQNzGLSWWb6f3a8VAN6+VjVk= cloud.google.com/go/edgecontainer v1.4.3/go.mod h1:q9Ojw2ox0uhAvFisnfPRAXFTB1nfRIOIXVWzdXMZLcE= cloud.google.com/go/edgecontainer v1.4.4/go.mod h1:yyNVHsCKtsX/0mqFdbljQw0Uo660q2dlMPaiqYiC2Tg= +cloud.google.com/go/edgecontainer v1.9.0/go.mod h1:mZmgXuMGTGI6RUUTXsOZa+F2rFF21v0JPnuX7LQEqBE= cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= cloud.google.com/go/errorreporting v0.4.0/go.mod h1:dZGEhqzdHZSRxxWLVjC3Ue5CVaROzvP58D9rU6zbBfw= +cloud.google.com/go/errorreporting v0.9.0/go.mod h1:V7ojx7z76JITDZNGyDNkIIa9nNEkQzF6Yj+VHl2YF84= cloud.google.com/go/essentialcontacts v1.7.6/go.mod h1:/Ycn2egr4+XfmAfxpLYsJeJlVf9MVnq9V7OMQr9R4lA= cloud.google.com/go/essentialcontacts v1.7.7/go.mod h1:ytycWAEn/aKUMRKQPMVgMrAtphEMgjbzL8vFwM3tqXs= +cloud.google.com/go/essentialcontacts v1.12.0/go.mod h1:W8fTL17jP6vmsPHQaCT5rOjWGohEssuqDUroxnjST0A= cloud.google.com/go/eventarc v1.15.5/go.mod h1:vDCqGqyY7SRiickhEGt1Zhuj81Ya4F/NtwwL3OZNskg= cloud.google.com/go/eventarc v1.18.0/go.mod h1:/6SDoqh5+9QNUqCX4/oQcJVK16fG/snHBSXu7lrJtO8= +cloud.google.com/go/eventarc v1.23.0/go.mod h1:tIJL0hoWtZXVa5MjcAep/4xB+AXz4AbqQV14ogX5VwU= cloud.google.com/go/filestore v1.10.2/go.mod h1:w0Pr8uQeSRQfCPRsL0sYKW6NKyooRgixCkV9yyLykR4= cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= +cloud.google.com/go/filestore v1.15.0/go.mod h1:oD+PvCWu4HqfEdNv65yk2XaLIiP7h4AuAH9Ua5YBRTM= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/firestore v1.13.0 h1:/3S4RssUV4GO/kvgJZB+tayjhOfyAHs+KcpJgRVu/Qk= @@ -125,36 +176,51 @@ cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBp cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= +cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU= cloud.google.com/go/functions v1.19.6/go.mod h1:0G0RnIlbM4MJEycfbPZlCzSf2lPOjL7toLDwl+r0ZBw= cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA= +cloud.google.com/go/functions v1.24.0/go.mod h1:t40GeqBAQNuqKlHCxmV/pxhyYJnImLcvRa3GBv4tAy0= cloud.google.com/go/gkebackup v1.7.0/go.mod h1:oPHXUc6X6tg6Zf/7QmKOfXOFaVzBEgMWpLDb4LqngWA= cloud.google.com/go/gkebackup v1.8.1/go.mod h1:GAaAl+O5D9uISH5MnClUop2esQW4pDa2qe/95A4l7YQ= +cloud.google.com/go/gkebackup v1.13.0/go.mod h1:D2MDbHW4V/uKCmS9TnT8hNKX2tPkE/pWp9nSm0TQ9hY= cloud.google.com/go/gkeconnect v0.12.4/go.mod h1:bvpU9EbBpZnXGo3nqJ1pzbHWIfA9fYqgBMJ1VjxaZdk= cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o= +cloud.google.com/go/gkeconnect v0.16.0/go.mod h1:5iWSBQzMIRLwUHUWVhxxcNK45ZPE8ntyBgE0MkavlqQ= cloud.google.com/go/gkehub v0.15.6/go.mod h1:sRT0cOPAgI1jUJrS3gzwdYCJ1NEzVVwmnMKEwrS2QaM= cloud.google.com/go/gkehub v0.16.0/go.mod h1:ADp27Ucor8v81wY+x/5pOxTorxkPj/xswH3AUpN62GU= +cloud.google.com/go/gkehub v0.21.0/go.mod h1:xKePlMrI8LpKErzKMWdH/yQv+GDV60ypCNfTTdT+BN0= cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk= cloud.google.com/go/gkemulticloud v1.6.0/go.mod h1:bGpd4o/Z5Z/XFlaojkgdVisHRwb+fLJvUPzsmV0I9ok= +cloud.google.com/go/gkemulticloud v1.11.0/go.mod h1:OtfHtgqOgDrXfcdFw8eUkCUI154Q51vvdqZYZV4c4qM= cloud.google.com/go/gsuiteaddons v1.7.7/go.mod h1:zTGmmKG/GEBCONsvMOY2ckDiEsq3FN+lzWGUiXccF9o= cloud.google.com/go/gsuiteaddons v1.7.8/go.mod h1:DBKNHH4YXAdd/rd6zVvtOGAJNGo0ekOh+nIjTUDEJ5U= +cloud.google.com/go/gsuiteaddons v1.12.0/go.mod h1:rm/XT7wmwOFGn7jmWtVV65QmZCakzTbHLSojIC4Hskg= cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= cloud.google.com/go/iap v1.11.1/go.mod h1:qFipMJ4nOIv4yDHZxn31PiS8QxJJH2FlxgH9aFauejw= cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU= +cloud.google.com/go/iap v1.17.0/go.mod h1:b+r+yjrss2WmAEzNrQQjlEdD5E9B8c47mOF7XnqT+z0= cloud.google.com/go/ids v1.5.6/go.mod h1:y3SGLmEf9KiwKsH7OHvYYVNIJAtXybqsD2z8gppsziQ= cloud.google.com/go/ids v1.5.7/go.mod h1:N3ZQOIgIBwwOu2tzyhmh3JDT+kt8PcoKkn2BRT9Qe4A= +cloud.google.com/go/ids v1.10.0/go.mod h1:uCSFrXfCnRUKBl5PdE/ZqBNp1+vKSKPWpdYGa61WjpQ= cloud.google.com/go/iot v1.8.6/go.mod h1:MThnkiihNkMysWNeNje2Hp0GSOpEq2Wkb/DkBCVYa0U= cloud.google.com/go/iot v1.8.7/go.mod h1:HvVcypV8LPv1yTXSLCNK+YCtqGHhq+p0F3BXETfpN+U= +cloud.google.com/go/iot v1.13.0/go.mod h1:62W4n2fe/Ct66NWJEfCB5suZ3XsL5Atx+MxFjScr+9s= cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= cloud.google.com/go/language v1.14.5/go.mod h1:nl2cyAVjcBct1Hk73tzxuKebk0t2eULFCaruhetdZIA= cloud.google.com/go/language v1.14.6/go.mod h1:7y3J9OexQsfkWNGCxhT+7lb64pa60e12ZCoWDOHxJ1M= +cloud.google.com/go/language v1.18.0/go.mod h1:xSeiVB4UiA9wYmFy2GWjf1Mb1K3uR1Yi/80qoqTxH04= cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+UiDkPuJWAmhRi7Q= cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY= +cloud.google.com/go/lifesciences v0.15.0/go.mod h1:FwS+QkqPdVWl4SmKUCFozFvsTVWTLH13HCKcwR/MR9U= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= @@ -164,114 +230,166 @@ cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMI cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/longrunning v0.13.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= cloud.google.com/go/managedidentities v1.7.6/go.mod h1:pYCWPaI1AvR8Q027Vtp+SFSM/VOVgbjBF4rxp1/z5p4= cloud.google.com/go/managedidentities v1.7.7/go.mod h1:nwNlMxtBo2YJMvsKXRtAD1bL41qiCI9npS7cbqrsJUs= +cloud.google.com/go/managedidentities v1.12.0/go.mod h1:rm72jf/v//0NG73VQNZM1JlV2E95uhJymmSXlgi6hMA= cloud.google.com/go/maps v1.20.4/go.mod h1:Act0Ws4HffrECH+pL8YYy1scdSLegov7+0c6gvKqRzI= cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY= +cloud.google.com/go/maps v1.35.0/go.mod h1:HH1V8tduMn+b9oRMCdl3vok98uvHco/wElZXyJQ/9kU= cloud.google.com/go/mediatranslation v0.9.6/go.mod h1:WS3QmObhRtr2Xu5laJBQSsjnWFPPthsyetlOyT9fJvE= cloud.google.com/go/mediatranslation v0.9.7/go.mod h1:mz3v6PR7+Fd/1bYrRxNFGnd+p4wqdc/fyutqC5QHctw= +cloud.google.com/go/mediatranslation v0.13.0/go.mod h1:kjZrowuigFr+Bf1HM1TCtp1a3E3kfG1ovPK5VEuaNAQ= cloud.google.com/go/memcache v1.11.6/go.mod h1:ZM6xr1mw3F8TWO+In7eq9rKlJc3jlX2MDt4+4H+/+cc= cloud.google.com/go/memcache v1.11.7/go.mod h1:AU1jYlUqCihxapcJ1GGMtlMWDVhzjbfUWBXqsXa4rBg= +cloud.google.com/go/memcache v1.16.0/go.mod h1:y/rXhJiieCF742K958dY29fSfM+Y3wh2thRmWspU2Dg= cloud.google.com/go/metastore v1.14.7/go.mod h1:0dka99KQofeUgdfu+K/Jk1KeT9veWZlxuZdJpZPtuYU= cloud.google.com/go/metastore v1.14.8/go.mod h1:h1XI2LpD4ohJhQYn9TwXqKb5sVt6KSo47ft96SiFF1s= +cloud.google.com/go/metastore v1.19.0/go.mod h1:JGTjGdQ627m2ptDo86XsIKqzzZCk+GG41VEFD7ENsqs= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= cloud.google.com/go/networkconnectivity v1.17.1/go.mod h1:DTZCq8POTkHgAlOAAEDQF3cMEr/B9k1ZbpklqvHEBtg= cloud.google.com/go/networkconnectivity v1.20.0/go.mod h1:9MzGwD4ljiq+Z2Pg3ue27OEewCuHz7IUfw1fITrIdSw= +cloud.google.com/go/networkconnectivity v1.26.0/go.mod h1:Uhzfk7NbiY6RNqV9XFvPWRji58+MkTYsTRfQ3EPtrGg= cloud.google.com/go/networkmanagement v1.19.1/go.mod h1:icgk265dNnilxQzpr6rO9WuAuuCmUOqq9H6WBeM2Af4= cloud.google.com/go/networkmanagement v1.22.0/go.mod h1:RGR62aLOlm72C7DT/3yaMUK43oill6hj9wqktUQ8h6Q= +cloud.google.com/go/networkmanagement v1.28.0/go.mod h1:2YogSU3sD7LvtmWntUAuGARbFQmy3A0En3LrJr69jkU= cloud.google.com/go/networksecurity v0.10.6/go.mod h1:FTZvabFPvK2kR/MRIH3l/OoQ/i53eSix2KA1vhBMJec= cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM= +cloud.google.com/go/networksecurity v0.16.0/go.mod h1:LMn10eRVf4K85PMF33yRoKAra7VhCOetxFcLDMh9A74= cloud.google.com/go/notebooks v1.12.6/go.mod h1:3Z4TMEqAKP3pu6DI/U+aEXrNJw9hGZIVbp+l3zw8EuA= cloud.google.com/go/notebooks v1.12.7/go.mod h1:uR9pxAkKmlNloibMr9Q1t8WhIu4P2JeqJs7c064/0Mo= +cloud.google.com/go/notebooks v1.17.0/go.mod h1:NScGIhfQCqLRIlVaUVbm595F6dhqiTl5XS1KaKgitKM= cloud.google.com/go/optimization v1.7.6/go.mod h1:4MeQslrSJGv+FY4rg0hnZBR/tBX2awJ1gXYp6jZpsYY= cloud.google.com/go/optimization v1.7.7/go.mod h1:OY2IAlX23o52qwMAZ0w65wibKuV12a4x6IHDTCq6kcU= +cloud.google.com/go/optimization v1.11.0/go.mod h1:qCWskZMcynh0GBsUrCP6oPwwnUhbwg5UcXvVM9hzOD8= cloud.google.com/go/orchestration v1.11.9/go.mod h1:KKXK67ROQaPt7AxUS1V/iK0Gs8yabn3bzJ1cLHw4XBg= cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0= +cloud.google.com/go/orchestration v1.16.0/go.mod h1:H7MFVP8Z/dtml39nf43sWYPL/2o7J4tdSZAlJrBuqnQ= cloud.google.com/go/orgpolicy v1.15.0/go.mod h1:NTQLwgS8N5cJtdfK55tAnMGtvPSsy95JJhESwYHaJVs= cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= +cloud.google.com/go/orgpolicy v1.20.0/go.mod h1:9LHqEGx5P5dhansdKTNIEXpM+QbebAIOs66+HUID4aQ= cloud.google.com/go/osconfig v1.14.6/go.mod h1:LS39HDBH0IJDFgOUkhSZUHFQzmcWaCpYXLrc3A4CVzI= cloud.google.com/go/osconfig v1.15.1/go.mod h1:NegylQQl0+5m+I+4Ey/g3HGeQxKkncQ1q+Il4DZ8PME= +cloud.google.com/go/osconfig v1.21.0/go.mod h1:BofnHqjjvu6lZQv/hqo2+rLCUiY4O6A9UYwwvVrSBjk= cloud.google.com/go/oslogin v1.14.6/go.mod h1:xEvcRZTkMXHfNSKdZ8adxD6wvRzeyAq3cQX3F3kbMRw= cloud.google.com/go/oslogin v1.14.7/go.mod h1:NB6NqBHfDMwznePdBVX+ILllc1oPCdNSGp5u/WIyndY= +cloud.google.com/go/oslogin v1.18.0/go.mod h1:3Oa36T3781Mv+yCSVYlfasi7auHjfPFqvNOd1q92umc= cloud.google.com/go/phishingprotection v0.9.6/go.mod h1:VmuGg03DCI0wRp/FLSvNyjFj+J8V7+uITgHjCD/x4RQ= cloud.google.com/go/phishingprotection v0.9.7/go.mod h1:JTI4HNGyAbWolBoNOoCyCF0e3cqPNrYnlievHU49EwE= +cloud.google.com/go/phishingprotection v0.13.0/go.mod h1:2gyYqwNjePPEocXDkDve3EuJPaRqN/E7fp28K3arR0k= cloud.google.com/go/policytroubleshooter v1.11.6/go.mod h1:jdjYGIveoYolk38Dm2JjS5mPkn8IjVqPsDHccTMu3mY= cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk= +cloud.google.com/go/policytroubleshooter v1.15.0/go.mod h1:yNuROjN6h+2/TE2JOvBBJMjYIjC6j0UYHq8f2kVHlA4= cloud.google.com/go/privatecatalog v0.10.7/go.mod h1:Fo/PF/B6m4A9vUYt0nEF1xd0U6Kk19/Je3eZGrQ6l60= cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA= +cloud.google.com/go/privatecatalog v0.15.0/go.mod h1:av2b5Rv+oG5ORxUqGlCAYO9s4pXjgc6q2qO9nkTcqT8= cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY= cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= +cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w= cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= +cloud.google.com/go/pubsub/v2 v2.5.1/go.mod h1:Pd+qeabMX+576vQJhTN7TelE4k6kJh15dLU/ptOQ/UA= cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= cloud.google.com/go/recaptchaenterprise/v2 v2.20.4/go.mod h1:3H8nb8j8N7Ss2eJ+zr+/H7gyorfzcxiDEtVBDvDjwDQ= cloud.google.com/go/recaptchaenterprise/v2 v2.21.0/go.mod h1:HxQYqZC2/zl2CvKN7jJEv71vEdDi1GMGNUiZxnpiuVI= +cloud.google.com/go/recaptchaenterprise/v2 v2.26.0/go.mod h1:+ntF70/j7qBa6G/pwmYA0mkBcDeTCXV6WDqUL7GObfs= cloud.google.com/go/recommendationengine v0.9.6/go.mod h1:nZnjKJu1vvoxbmuRvLB5NwGuh6cDMMQdOLXTnkukUOE= cloud.google.com/go/recommendationengine v0.9.7/go.mod h1:snZ/FL147u86Jqpv1j95R+CyU5NvL/UzYiyDo6UByTM= +cloud.google.com/go/recommendationengine v0.14.0/go.mod h1:UP9cN46tDpZ/N57eDYIWeIRHjMOchtiIyjWjV0Dvr3k= cloud.google.com/go/recommender v1.13.5/go.mod h1:v7x/fzk38oC62TsN5Qkdpn0eoMBh610UgArJtDIgH/E= cloud.google.com/go/recommender v1.13.6/go.mod h1:y5/5womtdOaIM3xx+76vbsiA+8EBTIVfWnxHDFHBGJM= +cloud.google.com/go/recommender v1.18.0/go.mod h1:INRBLfBQJCrgPqjBVFht4OjaFq/WhB/c5V1sqBOdX8g= cloud.google.com/go/redis v1.18.2/go.mod h1:q6mPRhLiR2uLf584Lcl4tsiRn0xiFlu6fnJLwCORMtY= cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= +cloud.google.com/go/redis v1.23.0/go.mod h1:EUlUT24BAL6LsE1f/N9Bg3LhRCfH+LzwLGbst3KuZRw= cloud.google.com/go/resourcemanager v1.10.6/go.mod h1:VqMoDQ03W4yZmxzLPrB+RuAoVkHDS5tFUUQUhOtnRTg= cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs= +cloud.google.com/go/resourcemanager v1.15.0/go.mod h1:ve0VNxPoDU6XxDuEMCjkineb0YzXQXx3mOWwnNckGDE= cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= cloud.google.com/go/retail v1.20.0/go.mod h1:1CXWDZDJTOsK6lPjkv67gValP9+h1TMadTC9NpFFr9s= cloud.google.com/go/retail v1.26.0/go.mod h1:gMfh6s174Mvy1rK4g50J9TH5sRim8px+Krml25kdrqo= +cloud.google.com/go/retail v1.31.0/go.mod h1:sfq/cT+gfSLuURf/mdVAw5n0pav3hxSP1rT8RfL7Qxk= cloud.google.com/go/run v1.9.3/go.mod h1:Si9yDIkUGr5vsXE2QVSWFmAjJkv/O8s3tJ1eTxw3p1o= cloud.google.com/go/run v1.15.0/go.mod h1:rgFHMdAopLl++57vzeqA+a1o2x0/ILZnEacRD6nC0EA= +cloud.google.com/go/run v1.21.0/go.mod h1:Z5wHbyFirI8XU48EPs5XJf/qmVm1SXZEhuS8EvZOuQU= cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= cloud.google.com/go/scheduler v1.11.8/go.mod h1:bNKU7/f04eoM6iKQpwVLvFNBgGyJNS87RiFN73mIPik= +cloud.google.com/go/scheduler v1.16.0/go.mod h1:0hsZg0MZJADyke1lutI0FHAYJR8Dtm8oIivXkmpACkA= cloud.google.com/go/secretmanager v1.14.7/go.mod h1:uRuB4F6NTFbg0vLQ6HsT7PSsfbY7FqHbtJP1J94qxGc= cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= +cloud.google.com/go/secretmanager v1.20.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU= cloud.google.com/go/security v1.18.5/go.mod h1:D1wuUkDwGqTKD0Nv7d4Fn2Dc53POJSmO4tlg1K1iS7s= cloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w= +cloud.google.com/go/security v1.24.0/go.mod h1:XaB3p0SE7v2bBitsLBb1hM6R8/oI/k/IujpXFJalFK0= cloud.google.com/go/securitycenter v1.36.2/go.mod h1:80ocoXS4SNWxmpqeEPhttYrmlQzCPVGaPzL3wVcoJvE= cloud.google.com/go/securitycenter v1.38.1/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0= +cloud.google.com/go/securitycenter v1.44.0/go.mod h1:7BMMbSTAddVfiE+HrC8tKS6SuRkyK7FRPlkpAZBRV3U= cloud.google.com/go/servicedirectory v1.12.6/go.mod h1:OojC1KhOMDYC45oyTn3Mup08FY/S0Kj7I58dxUMMTpg= cloud.google.com/go/servicedirectory v1.12.7/go.mod h1:gOtN+qbuCMH6tj2dqlDY3qQL7w3V0+nkWaZElnJK8Ps= +cloud.google.com/go/servicedirectory v1.17.0/go.mod h1:CtgjXS1idj3s9Q6tB68021Rzk8Q6decV6+ldXC1BoBk= cloud.google.com/go/shell v1.8.6/go.mod h1:GNbTWf1QA/eEtYa+kWSr+ef/XTCDkUzRpV3JPw0LqSk= cloud.google.com/go/shell v1.8.7/go.mod h1:OTke7qc3laNEW5Jr5OV9VR3IwU5x5VqGOE6705zFex4= +cloud.google.com/go/shell v1.12.0/go.mod h1:TivWrVriy6xQ0wBjNJJridJgODZz8zXUEW2u48kynzY= cloud.google.com/go/spanner v1.81.0/go.mod h1:3yqzHZvK52zLw10mNLG8MefCEYp3iRFJryTLf5u+mJg= cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= +cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= cloud.google.com/go/speech v1.27.1/go.mod h1:efCfklHFL4Flxcdt9gpEMEJh9MupaBzw3QiSOVeJ6ck= cloud.google.com/go/speech v1.29.0/go.mod h1:wtUmIS/h0ZYU6cPA9klcyST3f6i2FdnvNDqENjrRDds= +cloud.google.com/go/speech v1.35.0/go.mod h1:shnf33sZbGnQQZyek1fdLOR5rRKV6D3jsNqpqyijvj8= cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= cloud.google.com/go/storagetransfer v1.12.4/go.mod h1:p1xLKvpt78aQFRJ8lZGYArgFuL4wljFzitPZoYjl/8A= cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= +cloud.google.com/go/storagetransfer v1.18.0/go.mod h1:AbGutEym/KNasoiDpSj/CYbigp5yhgosSgwlhGvQNs4= cloud.google.com/go/talent v1.8.3/go.mod h1:oD3/BilJpJX8/ad8ZUAxlXHCslTg2YBbafFH3ciZSLQ= cloud.google.com/go/talent v1.8.4/go.mod h1:3yukBXUTVFNyKcJpUExW/k5gqEy8qW6OCNj7WdN0MWo= +cloud.google.com/go/talent v1.13.0/go.mod h1:GSwli9V25WQdzeuJDJWH9TlQmA8lPFn7yKsxowdxW9Y= cloud.google.com/go/texttospeech v1.13.0/go.mod h1:g/tW/m0VJnulGncDrAoad6WdELMTes8eb77Idz+4HCo= cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0= +cloud.google.com/go/texttospeech v1.21.0/go.mod h1:p/UVJILAo/S5vsJaWZVdDRzNzA7wXIA+hTACvpMeOBk= cloud.google.com/go/tpu v1.8.3/go.mod h1:Do6Gq+/Jx6Xs3LcY2WhHyGwKDKVw++9jIJp+X+0rxRE= cloud.google.com/go/tpu v1.8.4/go.mod h1:ul0cyWSHr6jHGZYElZe6HvQn35VY93RAlwpDiSBRnPA= +cloud.google.com/go/tpu v1.13.0/go.mod h1:F5gT5BL22Dhsr05JLHdMjAjj+wcTn3Xtuu4jvq9yFug= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= cloud.google.com/go/translate v1.12.5/go.mod h1:o/v+QG/bdtBV1d1edmtau0PwTfActvxPk/gtqdSDBi4= cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI= +cloud.google.com/go/translate v1.17.0/go.mod h1:3mErnHTQBu9yeLiL35K0HBBuaM6Vk2fD/vyWFz790VU= cloud.google.com/go/video v1.23.5/go.mod h1:ZSpGFCpfTOTmb1IkmHNGC/9yI3TjIa/vkkOKBDo0Vpo= cloud.google.com/go/video v1.27.1/go.mod h1:xzfAC77B4vtnbi/TT3UUxEjCa/+Ehy5EA8w470ytOig= +cloud.google.com/go/video v1.32.0/go.mod h1:KxDL728ZzH+FJwtEb9XkiLTETW5bI37hTWbJiRYeXkk= cloud.google.com/go/videointelligence v1.12.6/go.mod h1:/l34WMndN5/bt04lHodxiYchLVuWPQjCU6SaiTswrIw= cloud.google.com/go/videointelligence v1.12.7/go.mod h1:XAk5hCMY+GihxJ55jNoMdwdXSNZnCl3wGs2+94gK7MA= +cloud.google.com/go/videointelligence v1.16.0/go.mod h1:mmX1JpIWzwozaigrdRNjikZc3aFLNHFKh+OFwAdfiW4= cloud.google.com/go/vision/v2 v2.9.5/go.mod h1:1SiNZPpypqZDbOzU052ZYRiyKjwOcyqgGgqQCI/nlx8= cloud.google.com/go/vision/v2 v2.9.6/go.mod h1:lJC+vP15D5znJvHQYjEoTKnpToX1L93BUlvBmzM0gyg= +cloud.google.com/go/vision/v2 v2.14.0/go.mod h1:ODlLCajJOq4t8thoi1uVvbnfIfix73HsYWhZuIveagQ= cloud.google.com/go/vmmigration v1.8.6/go.mod h1:uZ6/KXmekwK3JmC8PzBM/cKQmq404TTfWtThF6bbf0U= cloud.google.com/go/vmmigration v1.10.0/go.mod h1:LDztCWEb+RwS1bPg4Xzt0fcJS9kVrFxa3ejhH7OW9vg= +cloud.google.com/go/vmmigration v1.15.0/go.mod h1:MP6mQ21ru1usBeCbl805Ioz0Fy+yf3qK2kUkhZ69QQY= cloud.google.com/go/vmwareengine v1.3.5/go.mod h1:QuVu2/b/eo8zcIkxBYY5QSwiyEcAy6dInI7N+keI+Jg= cloud.google.com/go/vmwareengine v1.3.6/go.mod h1:ps0rb+Skgpt9ppHYC0o5DqtJ5ld2FyS8sAqtbHH8t9s= +cloud.google.com/go/vmwareengine v1.8.0/go.mod h1:e66l90IZhm1yQfYZv+YCWjSNSklQZCRmuEvKL8n3Ua0= cloud.google.com/go/vpcaccess v1.8.6/go.mod h1:61yymNplV1hAbo8+kBOFO7Vs+4ZHYI244rSFgmsHC6E= cloud.google.com/go/vpcaccess v1.8.7/go.mod h1:9RYw5bVvk4Z51Rc8vwXT63yjEiMD/l7XyEaDyrNHgmk= +cloud.google.com/go/vpcaccess v1.13.0/go.mod h1:4Uus6E/9FYUtIrwBE1wJ1RosKwb02H6kEd9puJ02TL8= cloud.google.com/go/webrisk v1.11.1/go.mod h1:+9SaepGg2lcp1p0pXuHyz3R2Yi2fHKKb4c1Q9y0qbtA= cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0= +cloud.google.com/go/webrisk v1.16.0/go.mod h1:VIQw8smiaMOlget/xOk6niTkNJTiQc5skEmCuAksxJc= cloud.google.com/go/websecurityscanner v1.7.6/go.mod h1:ucaaTO5JESFn5f2pjdX01wGbQ8D6h79KHrmO2uGZeiY= cloud.google.com/go/websecurityscanner v1.7.7/go.mod h1:ng/PzARaus3Bj4Os4LpUnyYHsbtJky1HbBDmz148v1o= +cloud.google.com/go/websecurityscanner v1.12.0/go.mod h1:cZSc9HqoFdccL1mqZtPIInOd4R8PBGwI20wdnrz6AO8= cloud.google.com/go/workflows v1.14.2/go.mod h1:5nqKjMD+MsJs41sJhdVrETgvD5cOK3hUcAs8ygqYvXQ= cloud.google.com/go/workflows v1.14.3/go.mod h1:CC9+YdVI2Kvp0L58WajHpEfKJxhrtRh3uQ0SYWcmAk4= +cloud.google.com/go/workflows v1.19.0/go.mod h1:TWsrDGgsJy7xAJ07byzHhKKehEWItJG3BivEHVhGH5g= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= @@ -295,16 +413,20 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad h1:EmNYJhPYy0pOFjCx2PrgtaBXmee0iUX9hLlxE1xHOJE= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= @@ -322,13 +444,12 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= @@ -440,6 +561,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/roadrunner-server/http/v5 v5.2.8 h1:cXUKHxUGlfEa4MlABUno3w6exok+RDkybSR065kGwUE= github.com/roadrunner-server/http/v5 v5.2.8/go.mod h1:5XopTtiPMbF03zEDkdd+mM3KRtgsbZ8o5uHMRS8hx98= @@ -461,6 +583,7 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -499,6 +622,7 @@ go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= @@ -517,6 +641,7 @@ go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0 go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.temporal.io/api v1.49.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/api v1.57.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.11/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -554,6 +679,7 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -563,6 +689,7 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= @@ -576,6 +703,7 @@ golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= @@ -629,6 +757,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go. google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= diff --git a/rpc.go b/rpc.go index e766477..8419de6 100644 --- a/rpc.go +++ b/rpc.go @@ -96,6 +96,10 @@ func (r *rpc) Observe(_ context.Context, req *connect.Request[metricsV1.ObserveR } switch c := col.(type) { + // prometheus.Histogram and prometheus.Summary have identical method sets + // (Metric + Collector + Observe(float64)), so scalar Summary instances + // also match this branch — type-switch picks the first matching interface + // in source order. case prometheus.Histogram: c.Observe(m.GetValue()) case *prometheus.HistogramVec: diff --git a/tests/metrics_api_test.go b/tests/metrics_api_test.go index c214c28..75906ce 100644 --- a/tests/metrics_api_test.go +++ b/tests/metrics_api_test.go @@ -177,6 +177,26 @@ func TestMetricsConnectAPI(t *testing.T) { })) require.NoError(t, err) require.True(t, obsResp.Msg.GetOk()) + + // Scalar Summary Observe — locks in the prometheus.Summary branch of + // the type-switch (buildPromCollector returns NewSummary when labels are + // empty, distinct from NewSummaryVec). + _, err = client.Declare(ctx, connect.NewRequest(&metricsV1.DeclareRequest{ + Collector: &metricsV1.NamedCollector{ + Name: "api_summary_scalar", + Collector: &metricsV1.Collector{ + Type: metricsV1.CollectorType_COLLECTOR_TYPE_SUMMARY, + Help: "API test scalar summary", + Objectives: []*metricsV1.Objective{{Quantile: 0.95, Error: 0.01}}, + }, + }, + })) + require.NoError(t, err) + scalarResp, err := client.Observe(ctx, connect.NewRequest(&metricsV1.ObserveRequest{ + Metric: &metricsV1.Metric{Name: "api_summary_scalar", Value: 1.23}, + })) + require.NoError(t, err) + require.True(t, scalarResp.Msg.GetOk()) } // TestMetricsHTTPApi exercises the metrics RPCs through plain HTTP/1.1 with