feat(listener): add gha_job_queue_duration_seconds metric

Restore the job queue duration histogram by capturing queue time from
JobAvailable messages, since GitHub does not populate QueueTime on
JobStarted payloads.

Depends on actions/scaleset adding RecordJobAvailable to MetricsRecorder.
This commit is contained in:
Evan Alferez 2026-05-25 13:42:25 +09:00
parent 30879de182
commit 60d1981a5b
5 changed files with 225 additions and 33 deletions

View File

@ -5,6 +5,7 @@ import (
"log/slog"
"net/http"
"strings"
"sync"
"time"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
@ -44,6 +45,7 @@ const (
MetricIdleRunners = "gha_idle_runners"
MetricStartedJobsTotal = "gha_started_jobs_total"
MetricCompletedJobsTotal = "gha_completed_jobs_total"
MetricJobQueueDurationSeconds = "gha_job_queue_duration_seconds"
MetricJobStartupDurationSeconds = "gha_job_startup_duration_seconds"
MetricJobExecutionDurationSeconds = "gha_job_execution_duration_seconds"
)
@ -70,6 +72,7 @@ var metricsHelp = metricsHelpRegistry{
MetricIdleRunners: "Number of registered runners not running a job.",
},
histograms: map[string]string{
MetricJobQueueDurationSeconds: "Time spent waiting for workflow jobs to get assigned to the scale set after queueing (in seconds).",
MetricJobStartupDurationSeconds: "Time spent waiting for workflow job to get started on the runner owned by the scale set (in seconds).",
MetricJobExecutionDurationSeconds: "Time spent executing workflow jobs by the scale set (in seconds).",
},
@ -102,6 +105,7 @@ func (e *exporter) startedJobLabels(msg *scaleset.JobStarted) prometheus.Labels
type Recorder interface {
RecordStatic(min, max int)
RecordStatistics(stats *scaleset.RunnerScaleSetStatistic)
RecordJobAvailable(msg *scaleset.JobAvailable)
RecordJobStarted(msg *scaleset.JobStarted)
RecordJobCompleted(msg *scaleset.JobCompleted)
RecordDesiredRunners(count int)
@ -124,6 +128,9 @@ type exporter struct {
scaleSetLabels prometheus.Labels
*metrics
srv *http.Server
// JobStarted.QueueTime is unset on the wire, so queue time is captured on JobAvailable.
queuedAt sync.Map // map[int64]time.Time keyed by RunnerRequestID
}
type metrics struct {
@ -256,6 +263,16 @@ var defaultMetrics = v1alpha1.MetricsConfig{
},
},
Histograms: map[string]*v1alpha1.HistogramMetric{
MetricJobQueueDurationSeconds: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyJobName,
labelKeyEventName,
},
Buckets: defaultRuntimeBuckets,
},
MetricJobStartupDurationSeconds: {
Labels: []string{
labelKeyEnterprise,
@ -484,10 +501,26 @@ func (e *exporter) RecordStatistics(stats *scaleset.RunnerScaleSetStatistic) {
e.setGauge(MetricIdleRunners, e.scaleSetLabels, float64(stats.TotalIdleRunners))
}
func (e *exporter) RecordJobAvailable(msg *scaleset.JobAvailable) {
queuedAt := msg.QueueTime
if queuedAt.IsZero() {
queuedAt = time.Now()
}
e.queuedAt.Store(msg.RunnerRequestID, queuedAt)
}
func (e *exporter) RecordJobStarted(msg *scaleset.JobStarted) {
l := e.startedJobLabels(msg)
e.incCounter(MetricStartedJobsTotal, l)
if v, ok := e.queuedAt.LoadAndDelete(msg.RunnerRequestID); ok {
if queuedAt, ok := v.(time.Time); ok && !queuedAt.IsZero() && !msg.ScaleSetAssignTime.IsZero() {
if d := msg.ScaleSetAssignTime.Sub(queuedAt).Seconds(); d >= 0 {
e.observeHistogram(MetricJobQueueDurationSeconds, l, d)
}
}
}
startupDuration := msg.RunnerAssignTime.Unix() - msg.ScaleSetAssignTime.Unix()
e.observeHistogram(MetricJobStartupDurationSeconds, l, float64(startupDuration))
}
@ -510,6 +543,7 @@ type discard struct{}
func (*discard) RecordStatic(int, int) {}
func (*discard) RecordStatistics(*scaleset.RunnerScaleSetStatistic) {}
func (*discard) RecordJobAvailable(*scaleset.JobAvailable) {}
func (*discard) RecordJobStarted(*scaleset.JobStarted) {}
func (*discard) RecordJobCompleted(*scaleset.JobCompleted) {}
func (*discard) RecordDesiredRunners(int) {}

View File

@ -3,8 +3,10 @@ package metrics
import (
"log/slog"
"testing"
"time"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/scaleset"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -69,6 +71,10 @@ func TestInstallMetrics(t *testing.T) {
Buckets: []float64{0.1, 1},
},
// histogram metric should be registered with default runtime buckets
MetricJobQueueDurationSeconds: {
Labels: []string{labelKeyRepository},
},
// histogram metric should be registered with default runtime buckets
MetricJobStartupDurationSeconds: {
Labels: []string{labelKeyRepository},
},
@ -79,7 +85,7 @@ func TestInstallMetrics(t *testing.T) {
got := installMetrics(metricsConfig, reg, discardLogger)
assert.Len(t, got.counters, 1)
assert.Len(t, got.gauges, 1)
assert.Len(t, got.histograms, 2)
assert.Len(t, got.histograms, 3)
assert.Equal(t, got.counters[MetricStartedJobsTotal].config, metricsConfig.Counters[MetricStartedJobsTotal])
assert.Equal(t, got.gauges[MetricAssignedJobs].config, metricsConfig.Gauges[MetricAssignedJobs])
@ -265,3 +271,73 @@ func TestExporterConfigDefaults(t *testing.T) {
assert.Equal(t, want, config)
}
func TestJobQueueDurationMetric(t *testing.T) {
metricsConfig := v1alpha1.MetricsConfig{
Counters: map[string]*v1alpha1.CounterMetric{
MetricStartedJobsTotal: {
Labels: []string{labelKeyRepository},
},
},
Histograms: map[string]*v1alpha1.HistogramMetric{
MetricJobQueueDurationSeconds: {
Labels: []string{labelKeyRepository},
Buckets: []float64{1, 5, 10},
},
MetricJobStartupDurationSeconds: {
Labels: []string{labelKeyRepository},
Buckets: []float64{1, 5, 10},
},
},
}
reg := prometheus.NewRegistry()
installed := installMetrics(metricsConfig, reg, discardLogger)
exporter := &exporter{
scaleSetLabels: prometheus.Labels{
labelKeyRepository: "repo",
},
metrics: installed,
}
queueTime := time.Unix(100, 0)
scaleSetAssignTime := queueTime.Add(30 * time.Second)
runnerAssignTime := scaleSetAssignTime.Add(10 * time.Second)
exporter.RecordJobAvailable(&scaleset.JobAvailable{
JobMessageBase: scaleset.JobMessageBase{
RunnerRequestID: 42,
RepositoryName: "repo",
QueueTime: queueTime,
},
})
exporter.RecordJobStarted(&scaleset.JobStarted{
JobMessageBase: scaleset.JobMessageBase{
RunnerRequestID: 42,
RepositoryName: "repo",
ScaleSetAssignTime: scaleSetAssignTime,
RunnerAssignTime: runnerAssignTime,
},
})
_, ok := exporter.queuedAt.Load(int64(42))
assert.False(t, ok, "queue time entry should be removed after job started")
metricFamilies, err := reg.Gather()
require.NoError(t, err)
var queueDurationSum float64
var queueDurationCount uint64
for _, mf := range metricFamilies {
if mf.GetName() != "gha_job_queue_duration_seconds" {
continue
}
for _, m := range mf.GetMetric() {
queueDurationSum = m.GetHistogram().GetSampleSum()
queueDurationCount = m.GetHistogram().GetSampleCount()
}
}
assert.Equal(t, uint64(1), queueDurationCount)
assert.Equal(t, float64(30), queueDurationSum)
}

View File

@ -118,6 +118,46 @@ func (_c *MockRecorder_RecordJobCompleted_Call) RunAndReturn(run func(msg *scale
return _c
}
// RecordJobAvailable provides a mock function for the type MockRecorder
func (_mock *MockRecorder) RecordJobAvailable(msg *scaleset.JobAvailable) {
_mock.Called(msg)
return
}
// MockRecorder_RecordJobAvailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecordJobAvailable'
type MockRecorder_RecordJobAvailable_Call struct {
*mock.Call
}
// RecordJobAvailable is a helper method to define mock.On call
// - msg *scaleset.JobAvailable
func (_e *MockRecorder_Expecter) RecordJobAvailable(msg interface{}) *MockRecorder_RecordJobAvailable_Call {
return &MockRecorder_RecordJobAvailable_Call{Call: _e.mock.On("RecordJobAvailable", msg)}
}
func (_c *MockRecorder_RecordJobAvailable_Call) Run(run func(msg *scaleset.JobAvailable)) *MockRecorder_RecordJobAvailable_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *scaleset.JobAvailable
if args[0] != nil {
arg0 = args[0].(*scaleset.JobAvailable)
}
run(
arg0,
)
})
return _c
}
func (_c *MockRecorder_RecordJobAvailable_Call) Return() *MockRecorder_RecordJobAvailable_Call {
_c.Call.Return()
return _c
}
func (_c *MockRecorder_RecordJobAvailable_Call) RunAndReturn(run func(msg *scaleset.JobAvailable)) *MockRecorder_RecordJobAvailable_Call {
_c.Run(run)
return _c
}
// RecordJobStarted provides a mock function for the type MockRecorder
func (_mock *MockRecorder) RecordJobStarted(msg *scaleset.JobStarted) {
_mock.Called(msg)
@ -402,6 +442,46 @@ func (_c *MockServerExporter_RecordJobCompleted_Call) RunAndReturn(run func(msg
return _c
}
// RecordJobAvailable provides a mock function for the type MockServerExporter
func (_mock *MockServerExporter) RecordJobAvailable(msg *scaleset.JobAvailable) {
_mock.Called(msg)
return
}
// MockServerExporter_RecordJobAvailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecordJobAvailable'
type MockServerExporter_RecordJobAvailable_Call struct {
*mock.Call
}
// RecordJobAvailable is a helper method to define mock.On call
// - msg *scaleset.JobAvailable
func (_e *MockServerExporter_Expecter) RecordJobAvailable(msg interface{}) *MockServerExporter_RecordJobAvailable_Call {
return &MockServerExporter_RecordJobAvailable_Call{Call: _e.mock.On("RecordJobAvailable", msg)}
}
func (_c *MockServerExporter_RecordJobAvailable_Call) Run(run func(msg *scaleset.JobAvailable)) *MockServerExporter_RecordJobAvailable_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *scaleset.JobAvailable
if args[0] != nil {
arg0 = args[0].(*scaleset.JobAvailable)
}
run(
arg0,
)
})
return _c
}
func (_c *MockServerExporter_RecordJobAvailable_Call) Return() *MockServerExporter_RecordJobAvailable_Call {
_c.Call.Return()
return _c
}
func (_c *MockServerExporter_RecordJobAvailable_Call) RunAndReturn(run func(msg *scaleset.JobAvailable)) *MockServerExporter_RecordJobAvailable_Call {
_c.Run(run)
return _c
}
// RecordJobStarted provides a mock function for the type MockServerExporter
func (_mock *MockServerExporter) RecordJobStarted(msg *scaleset.JobStarted) {
_mock.Called(msg)

21
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/teambition/rrule-go v1.8.2
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.52.0
golang.org/x/net v0.54.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
gomodules.xyz/jsonpatch/v2 v2.5.0
@ -115,7 +115,7 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/gonvenience/bunt v1.4.3 // indirect
github.com/gonvenience/idem v0.0.3 // indirect
@ -152,7 +152,7 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-zglob v0.0.6 // indirect
@ -190,14 +190,14 @@ require (
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
@ -213,3 +213,6 @@ require (
tool github.com/vektra/mockery/v3
replace github.com/gregjones/httpcache => github.com/actions-runner-controller/httpcache v0.2.0
// TODO: remove after actions/scaleset merges RecordJobAvailable hook
replace github.com/actions/scaleset => github.com/evanclan/scaleset v0.4.1-0.20260525043720-fe3f8bd76c98

45
go.sum
View File

@ -25,10 +25,6 @@ github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5Qx
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/actions-runner-controller/httpcache v0.2.0 h1:hCNvYuVPJ2xxYBymqBvH0hSiQpqz4PHF/LbU3XghGNI=
github.com/actions-runner-controller/httpcache v0.2.0/go.mod h1:JLu9/2M/btPz1Zu/vTZ71XzukQHn2YeISPmJoM5exBI=
github.com/actions/scaleset v0.3.0 h1:y5/ClYLJXFuGCikzILOOPhaCShAcL6K0mnUtjDKFxVw=
github.com/actions/scaleset v0.3.0/go.mod h1:2L2I6rggFWV+zprDet6y7y7Vkm3HPudaup78eSc79Uo=
github.com/actions/scaleset v0.4.0 h1:691GC2AkHb3ZGjfNvatboYoRS7CLr3+4VcZk/6w9IbM=
github.com/actions/scaleset v0.4.0/go.mod h1:2L2I6rggFWV+zprDet6y7y7Vkm3HPudaup78eSc79Uo=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
@ -128,12 +124,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanclan/scaleset v0.4.1-0.20260525043720-fe3f8bd76c98 h1:lmq3GYz4xt/cNvvzUGGyaxEZwmGr7y0pOxT8Z9aHIB4=
github.com/evanclan/scaleset v0.4.1-0.20260525043720-fe3f8bd76c98/go.mod h1:+Ylz7IYPnOTJd8dZmMziJ7J9HEfZhdoH7iliEWSb/Ms=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -193,8 +191,8 @@ github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI6
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -319,8 +317,9 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk=
github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -450,20 +449,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -484,21 +483,21 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=