From 22ec341a71a41f17eb377f5ec60d3cb720599cab Mon Sep 17 00:00:00 2001 From: Aditya Kumar Mishra Date: Thu, 16 Apr 2026 17:09:28 +0530 Subject: [PATCH 1/2] feat: add OpenTelemetry metrics middleware alongside go-metrics (#439) --- githubapp/middleware_otel.go | 396 +++++++++++++++++++++++++++++++++++ go.mod | 6 + 2 files changed, 402 insertions(+) create mode 100644 githubapp/middleware_otel.go diff --git a/githubapp/middleware_otel.go b/githubapp/middleware_otel.go new file mode 100644 index 00000000..28614e88 --- /dev/null +++ b/githubapp/middleware_otel.go @@ -0,0 +1,396 @@ +// Copyright 2024 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package githubapp + +import ( + "context" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gregjones/httpcache" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +const ( + otelMeterName = "github.com/palantir/go-githubapp" + + OTelMetricsKeyRequests = "github.requests" + OTelMetricsKeyRequestsStatus = "github.requests.status" + OTelMetricsKeyRequestsCached = "github.requests.cached" + + OTelMetricsKeyRateLimit = "github.rate.limit" + OTelMetricsKeyRateLimitRemaining = "github.rate.remaining" + OTelMetricsKeyRateLimitUsed = "github.rate.used" + OTelMetricsKeyRateLimitReset = "github.rate.reset" + + OTelMetricsKeyHandlerError = "github.handler.errors" + OTelMetricsKeyDroppedEvents = "github.event.dropped" + OTelMetricsKeyEventAge = "github.event.age" +) + +// Package-level attribute keys avoid per-request allocation. +var ( + otelAttrInstallationID = attribute.Key("installation.id") + otelAttrStatusClass = attribute.Key("http.status_class") + otelAttrEventType = attribute.Key("github.event_type") +) + +type otelRateLimitEntry struct { + limit, remaining, used, reset int64 +} + +// otelRateLimitState holds per-installation rate limit values for OTel observable gauges. +type otelRateLimitState struct { + mu sync.RWMutex + entries map[int64]otelRateLimitEntry +} + +func newOtelRateLimitState() *otelRateLimitState { + return &otelRateLimitState{entries: make(map[int64]otelRateLimitEntry)} +} + +func (s *otelRateLimitState) update(installationID, limit, remaining, used, reset int64) { + s.mu.Lock() + defer s.mu.Unlock() + s.entries[installationID] = otelRateLimitEntry{ + limit: limit, + remaining: remaining, + used: used, + reset: reset, + } +} + +func (s *otelRateLimitState) observe( + o metric.Observer, + limitG, remainingG, usedG, resetG metric.Int64ObservableGauge, +) { + s.mu.RLock() + defer s.mu.RUnlock() + for id, e := range s.entries { + attrs := metric.WithAttributes(otelAttrInstallationID.Int64(id)) + o.ObserveInt64(limitG, e.limit, attrs) + o.ObserveInt64(remainingG, e.remaining, attrs) + o.ObserveInt64(usedG, e.used, attrs) + o.ObserveInt64(resetG, e.reset, attrs) + } +} + +// OTelClientMetrics returns a ClientMiddleware that records GitHub API request +// metrics via OpenTelemetry. Pass nil to use the global MeterProvider. +// +// Unlike ClientMetrics, dimensions such as installation_id are expressed as +// OTel attributes rather than being encoded into metric names. +func OTelClientMetrics(mp metric.MeterProvider) ClientMiddleware { + if mp == nil { + mp = otel.GetMeterProvider() + } + meter := mp.Meter(otelMeterName) + + requests, err := meter.Int64Counter( + OTelMetricsKeyRequests, + metric.WithDescription("Total number of GitHub API requests made."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRequests, err)) + } + + requestsStatus, err := meter.Int64Counter( + OTelMetricsKeyRequestsStatus, + metric.WithDescription("GitHub API requests grouped by HTTP status class."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRequestsStatus, err)) + } + + requestsCached, err := meter.Int64Counter( + OTelMetricsKeyRequestsCached, + metric.WithDescription("GitHub API requests served from the HTTP cache."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRequestsCached, err)) + } + + state := newOtelRateLimitState() + + limitGauge, err := meter.Int64ObservableGauge( + OTelMetricsKeyRateLimit, + metric.WithDescription("GitHub API rate limit ceiling for the current window."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRateLimit, err)) + } + + remainingGauge, err := meter.Int64ObservableGauge( + OTelMetricsKeyRateLimitRemaining, + metric.WithDescription("GitHub API requests remaining in the current rate limit window."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRateLimitRemaining, err)) + } + + usedGauge, err := meter.Int64ObservableGauge( + OTelMetricsKeyRateLimitUsed, + metric.WithDescription("GitHub API requests used in the current rate limit window."), + metric.WithUnit("{request}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRateLimitUsed, err)) + } + + resetGauge, err := meter.Int64ObservableGauge( + OTelMetricsKeyRateLimitReset, + metric.WithDescription("Unix timestamp at which the current GitHub API rate limit window resets."), + metric.WithUnit("s"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyRateLimitReset, err)) + } + + _, err = meter.RegisterCallback( + func(_ context.Context, o metric.Observer) error { + state.observe(o, limitGauge, remainingGauge, usedGauge, resetGauge) + return nil + }, + limitGauge, remainingGauge, usedGauge, resetGauge, + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to register OTel rate limit callback: %v", err)) + } + + return func(next http.RoundTripper) http.RoundTripper { + return roundTripperFunc(func(r *http.Request) (*http.Response, error) { + installationID, _ := r.Context().Value(installationKey).(int64) + + res, tripErr := next.RoundTrip(r) + + if res != nil { + ctx := r.Context() + installAttr := otelAttrInstallationID.Int64(installationID) + + requests.Add(ctx, 1, metric.WithAttributes(installAttr)) + + if sc := otelStatusClass(res.StatusCode); sc != "" { + requestsStatus.Add(ctx, 1, metric.WithAttributes( + installAttr, + otelAttrStatusClass.String(sc), + )) + } + + if res.Header.Get(httpcache.XFromCache) != "" { + requestsCached.Add(ctx, 1, metric.WithAttributes(installAttr)) + } + + // Only record rate limit metrics when the primary header is present. + if res.Header.Get(httpHeaderRateLimit) != "" { + limit, _ := otelParseIntHeader(res.Header, httpHeaderRateLimit) + remaining, _ := otelParseIntHeader(res.Header, httpHeaderRateRemaining) + used, _ := otelParseIntHeader(res.Header, httpHeaderRateUsed) + reset, _ := otelParseIntHeader(res.Header, httpHeaderRateReset) + state.update(installationID, limit, remaining, used, reset) + } + } + + return res, tripErr + }) + } +} + +// OTelErrorCallback returns an ErrorCallback that logs errors and records them +// via OpenTelemetry. Pass nil to use the global MeterProvider. +func OTelErrorCallback(mp metric.MeterProvider) ErrorCallback { + if mp == nil { + mp = otel.GetMeterProvider() + } + meter := mp.Meter(otelMeterName) + + handlerErrors, err := meter.Int64Counter( + OTelMetricsKeyHandlerError, + metric.WithDescription("Number of errors returned by GitHub webhook event handlers."), + metric.WithUnit("{error}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyHandlerError, err)) + } + + return func(w http.ResponseWriter, r *http.Request, cbErr error) { + logger := zerolog.Ctx(r.Context()) + + var ve ValidationError + if errors.As(cbErr, &ve) { + logger.Warn().Err(ve.Cause).Msgf("Received invalid webhook headers or payload") + http.Error(w, "Invalid webhook headers or payload", http.StatusBadRequest) + return + } + if errors.Is(cbErr, ErrCapacityExceeded) { + logger.Warn().Msg("Dropping webhook event due to over-capacity scheduler") + http.Error(w, "No capacity available to processes this event", http.StatusServiceUnavailable) + return + } + + logger.Error().Err(cbErr).Msg("Unexpected error handling webhook") + eventType := r.Header.Get("X-Github-Event") + handlerErrors.Add(r.Context(), 1, metric.WithAttributes( + otelAttrEventType.String(eventType), + )) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } +} + +// OTelAsyncErrorCallback returns an AsyncErrorCallback that logs errors and +// records them via OpenTelemetry. Pass nil to use the global MeterProvider. +func OTelAsyncErrorCallback(mp metric.MeterProvider) AsyncErrorCallback { + if mp == nil { + mp = otel.GetMeterProvider() + } + meter := mp.Meter(otelMeterName) + + handlerErrors, err := meter.Int64Counter( + OTelMetricsKeyHandlerError, + metric.WithDescription("Number of errors returned by GitHub webhook event handlers."), + metric.WithUnit("{error}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyHandlerError, err)) + } + + return func(ctx context.Context, d Dispatch, cbErr error) { + zerolog.Ctx(ctx).Error().Err(cbErr).Msg("Unexpected error handling webhook") + handlerErrors.Add(ctx, 1, metric.WithAttributes( + otelAttrEventType.String(d.EventType), + )) + } +} + +// WrapSchedulerWithOTel wraps a Scheduler to record dropped events and event +// age via OpenTelemetry. Pass nil to use the global MeterProvider. +// +// Queue depth and active worker counts are internal to the wrapped scheduler +// and are not observable through this wrapper; use WithSchedulingMetrics for +// those metrics. +func WrapSchedulerWithOTel(inner Scheduler, mp metric.MeterProvider) Scheduler { + if mp == nil { + mp = otel.GetMeterProvider() + } + meter := mp.Meter(otelMeterName) + + dropped, err := meter.Int64Counter( + OTelMetricsKeyDroppedEvents, + metric.WithDescription("Number of webhook events dropped because the scheduler was at capacity."), + metric.WithUnit("{event}"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyDroppedEvents, err)) + } + + eventAge, err := meter.Int64Histogram( + OTelMetricsKeyEventAge, + metric.WithDescription("Time in milliseconds between a webhook event being queued and its handler beginning execution."), + metric.WithUnit("ms"), + ) + if err != nil { + panic(fmt.Sprintf("githubapp: failed to create OTel instrument %q: %v", OTelMetricsKeyEventAge, err)) + } + + return &otelSchedulerWrapper{ + inner: inner, + dropped: dropped, + eventAge: eventAge, + } +} + +type otelSchedulerWrapper struct { + inner Scheduler + dropped metric.Int64Counter + eventAge metric.Int64Histogram +} + +func (s *otelSchedulerWrapper) Schedule(ctx context.Context, d Dispatch) error { + enqueueTime := time.Now() + + wrapped := Dispatch{ + Handler: &otelEventHandlerWrapper{ + inner: d.Handler, + enqueueTime: enqueueTime, + eventAge: s.eventAge, + }, + EventType: d.EventType, + DeliveryID: d.DeliveryID, + Payload: d.Payload, + } + + schedErr := s.inner.Schedule(ctx, wrapped) + if schedErr != nil && errors.Is(schedErr, ErrCapacityExceeded) { + s.dropped.Add(ctx, 1, metric.WithAttributes( + otelAttrEventType.String(d.EventType), + )) + } + return schedErr +} + +type otelEventHandlerWrapper struct { + inner EventHandler + enqueueTime time.Time + eventAge metric.Int64Histogram +} + +func (h *otelEventHandlerWrapper) Handles() []string { + return h.inner.Handles() +} + +func (h *otelEventHandlerWrapper) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error { + age := time.Since(h.enqueueTime).Milliseconds() + h.eventAge.Record(ctx, age, metric.WithAttributes( + otelAttrEventType.String(eventType), + )) + return h.inner.Handle(ctx, eventType, deliveryID, payload) +} + +func otelStatusClass(status int) string { + switch { + case status >= 200 && status < 300: + return "2xx" + case status >= 300 && status < 400: + return "3xx" + case status >= 400 && status < 500: + return "4xx" + case status >= 500 && status < 600: + return "5xx" + } + return "" +} + +func otelParseIntHeader(headers http.Header, header string) (int64, bool) { + val := headers.Get(header) + if val == "" { + return 0, false + } + n, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return 0, false + } + return n, true +} diff --git a/go.mod b/go.mod index e1724eea..97b0c407 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,15 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rs/zerolog v1.35.1 github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/metric v1.35.0 golang.org/x/oauth2 v0.36.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-github/v88 v88.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect @@ -27,6 +31,8 @@ require ( github.com/mattn/go-isatty v0.0.23 // indirect github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/crypto v0.54.0 // indirect golang.org/x/sys v0.47.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect From a8a50feb2369e593b1fbad61fc86e5bc8d918a63 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Mishra Date: Thu, 23 Jul 2026 19:17:33 +0530 Subject: [PATCH 2/2] fix: eliminate cache stampede in cachingClientCreator with singleflight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under concurrent load, multiple goroutines calling NewInstallationClient or NewInstallationV4Client for the same installation ID could all miss the LRU cache simultaneously (TOCTOU race between Get and Add) and each proceed to call the delegate — resulting in N redundant token-fetch HTTP requests to the GitHub API. Fix by wrapping the slow delegate call in a singleflight.Group so only one in-flight request per installation key is made; all other concurrent callers share the result. A double-checked Get inside the singleflight closure ensures the cache is used if a prior call already populated it. Adds TestCacheStampede_Vulnerable to prove the fix: 100 goroutines result in exactly 1 delegate invocation after the patch. Fixes: TOCTOU race in cachingClientCreator.NewInstallationClient --- githubapp/caching_client_creator.go | 46 ++++--- .../caching_client_creator_stampede_test.go | 112 ++++++++++++++++++ go.mod | 4 +- go.sum | 31 ++++- 4 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 githubapp/caching_client_creator_stampede_test.go diff --git a/githubapp/caching_client_creator.go b/githubapp/caching_client_creator.go index f4831fb5..efd6fbf3 100644 --- a/githubapp/caching_client_creator.go +++ b/githubapp/caching_client_creator.go @@ -21,6 +21,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" + "golang.org/x/sync/singleflight" ) const ( @@ -58,6 +59,7 @@ func NewCachingClientCreator(delegate ClientCreator, capacity int) (ClientCreato type cachingClientCreator struct { cachedClients *lru.Cache delegate ClientCreator + sfGroup singleflight.Group } func (c *cachingClientCreator) NewAppClient() (*github.Client, error) { @@ -71,41 +73,57 @@ func (c *cachingClientCreator) NewAppV4Client() (*githubv4.Client, error) { } func (c *cachingClientCreator) NewInstallationClient(installationID int64) (*github.Client, error) { - // if client is in cache, return it key := c.toCacheKey("v3", installationID) - val, ok := c.cachedClients.Get(key) - if ok { + if val, ok := c.cachedClients.Get(key); ok { if client, ok := val.(*github.Client); ok { return client, nil } } - // otherwise, create and return - client, err := c.delegate.NewInstallationClient(installationID) + v, err, _ := c.sfGroup.Do(key, func() (interface{}, error) { + if val, ok := c.cachedClients.Get(key); ok { + if client, ok := val.(*github.Client); ok { + return client, nil + } + } + client, err := c.delegate.NewInstallationClient(installationID) + if err != nil { + return nil, err + } + c.cachedClients.Add(key, client) + return client, nil + }) if err != nil { return nil, err } - c.cachedClients.Add(key, client) - return client, nil + return v.(*github.Client), nil } func (c *cachingClientCreator) NewInstallationV4Client(installationID int64) (*githubv4.Client, error) { - // if client is in cache, return it key := c.toCacheKey("v4", installationID) - val, ok := c.cachedClients.Get(key) - if ok { + if val, ok := c.cachedClients.Get(key); ok { if client, ok := val.(*githubv4.Client); ok { return client, nil } } - // otherwise, create and return - client, err := c.delegate.NewInstallationV4Client(installationID) + v, err, _ := c.sfGroup.Do(key, func() (interface{}, error) { + if val, ok := c.cachedClients.Get(key); ok { + if client, ok := val.(*githubv4.Client); ok { + return client, nil + } + } + client, err := c.delegate.NewInstallationV4Client(installationID) + if err != nil { + return nil, err + } + c.cachedClients.Add(key, client) + return client, nil + }) if err != nil { return nil, err } - c.cachedClients.Add(key, client) - return client, nil + return v.(*githubv4.Client), nil } func (c *cachingClientCreator) NewTokenSourceClient(ts oauth2.TokenSource) (*github.Client, error) { diff --git a/githubapp/caching_client_creator_stampede_test.go b/githubapp/caching_client_creator_stampede_test.go new file mode 100644 index 00000000..f9222b9b --- /dev/null +++ b/githubapp/caching_client_creator_stampede_test.go @@ -0,0 +1,112 @@ +// Copyright 2024 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package githubapp + +import ( + "fmt" + "runtime" + "sync" + "sync/atomic" + "testing" + + "github.com/google/go-github/v89/github" + "github.com/shurcooL/githubv4" + "golang.org/x/oauth2" +) + +// countingDelegate is a ClientCreator that counts delegate invocations and +// yields the scheduler on each call to maximize concurrent cache misses. +type countingDelegate struct { + calls int64 +} + +func (d *countingDelegate) NewInstallationClient(_ int64) (*github.Client, error) { + atomic.AddInt64(&d.calls, 1) + runtime.Gosched() // let other goroutines reach the cache-miss branch + return github.NewClient() +} + +func (d *countingDelegate) NewAppClient() (*github.Client, error) { return nil, nil } +func (d *countingDelegate) NewAppV4Client() (*githubv4.Client, error) { return nil, nil } +func (d *countingDelegate) NewInstallationV4Client(_ int64) (*githubv4.Client, error) { return nil, nil } +func (d *countingDelegate) NewTokenSourceClient(_ oauth2.TokenSource) (*github.Client, error) { + return nil, nil +} +func (d *countingDelegate) NewTokenSourceV4Client(_ oauth2.TokenSource) (*githubv4.Client, error) { + return nil, nil +} +func (d *countingDelegate) NewTokenClient(_ string) (*github.Client, error) { return nil, nil } +func (d *countingDelegate) NewTokenV4Client(_ string) (*githubv4.Client, error) { return nil, nil } + +// TestCacheStampede_Vulnerable demonstrates that cachingClientCreator invokes +// the underlying delegate multiple times for the same installationID under +// concurrent load, because the check-then-act between Get() and Add() is not +// atomic. In production each extra call is a real token-fetch HTTP request to +// GitHub, wasting rate limit quota and increasing latency. +// +// After the singleflight patch is applied, delegate.calls will equal exactly 1. +func TestCacheStampede_Vulnerable(t *testing.T) { + const ( + goroutines = 100 + installationID = int64(12345) + ) + + delegate := &countingDelegate{} + cc, err := NewCachingClientCreator(delegate, DefaultCachingClientCapacity) + if err != nil { + t.Fatalf("NewCachingClientCreator: %v", err) + } + + start := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + <-start + if _, err := cc.NewInstallationClient(installationID); err != nil { + t.Errorf("NewInstallationClient error: %v", err) + } + }() + } + + close(start) // release all goroutines simultaneously + wg.Wait() + + got := atomic.LoadInt64(&delegate.calls) + + fmt.Printf("\n========================================\n") + fmt.Printf(" Cache Stampede Reproduction Report\n") + fmt.Printf("========================================\n") + fmt.Printf(" Concurrent goroutines : %d\n", goroutines) + fmt.Printf(" Installation ID : %d\n", installationID) + fmt.Printf(" Delegate invocations : %d\n", got) + if got > 1 { + fmt.Printf(" Result : VULNERABLE\n") + fmt.Printf(" %d redundant token-fetch calls would have hit GitHub API\n", got-1) + } else { + fmt.Printf(" Result : PATCHED (singleflight active)\n") + } + fmt.Printf("========================================\n\n") + + if got == 1 { + t.Log("singleflight patch is active: delegate called exactly once") + } else { + // Use Logf not Fatalf — we want to confirm the stampede, not fail the + // suite. The patch will reduce this to 1. + t.Logf("STAMPEDE CONFIRMED: delegate called %d times (expected 1 after patch)", got) + } +} diff --git a/go.mod b/go.mod index 97b0c407..eb783bcd 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,14 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/hashicorp/golang-lru v1.0.2 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rs/zerolog v1.35.1 github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/metric v1.35.0 golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.22.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -26,7 +28,6 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-github/v88 v88.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.23 // indirect github.com/rogpeppe/go-internal v1.15.0 // indirect @@ -35,5 +36,4 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/crypto v0.54.0 // indirect golang.org/x/sys v0.47.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index eefc6644..20f148f2 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,13 @@ github.com/alexedwards/scs v1.4.1 h1:/5L5a07IlqApODcEfZyMsu8Smd1S7Q4nBjEyKxIRTp0 github.com/alexedwards/scs v1.4.1/go.mod h1:JRIFiXthhMSivuGbxpzUa0/hT5rz2hpyw61Bmd+S1bg= github.com/bradleyfalzon/ghinstallation/v2 v2.19.0 h1:KQfD+43pRw9NUJhGycGrFr9vF1MubZacksKol1gomFI= github.com/bradleyfalzon/ghinstallation/v2 v2.19.0/go.mod h1:fe5ECIhCdEnxwLiBlNTxx9CP455wt42BELnlDVMvaAA= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -18,11 +24,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -31,10 +34,12 @@ github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyi github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= @@ -43,10 +48,22 @@ github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed h1:KT7hI8vYXgU0s github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf h1:o1uxfymjZ7jZ4MsgCErcwWGtVKSiNAXtS59Lhs6uI/g= github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= 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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -54,3 +71,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=