Skip to content

Commit beeb99a

Browse files
authored
Merge pull request #60 from chainguard-dev/create-pull-request/patch
Export mono/public/terraform-infra-common: refs/heads/main
2 parents 28b1c62 + 3640fcd commit beeb99a

2 files changed

Lines changed: 128 additions & 9 deletions

File tree

pkg/httpmetrics/transport.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ const (
3232
OriginalTraceHeader string = "original-traceparent"
3333
)
3434

35+
// contextKey is an unexported type for context keys in this package, preventing
36+
// collisions with keys defined in other packages.
37+
type contextKey string
38+
39+
const (
40+
githubAppIDKey contextKey = "github_app_id"
41+
githubInstallationIDKey contextKey = "github_installation_id"
42+
)
43+
44+
// WithGitHubAppID returns a copy of ctx with the GitHub App ID attached.
45+
// The transport reads this value to label rate limit metrics with the app
46+
// that made the request, enabling per-app visibility into quota consumption.
47+
func WithGitHubAppID(ctx context.Context, appID int64) context.Context {
48+
return context.WithValue(ctx, githubAppIDKey, strconv.FormatInt(appID, 10))
49+
}
50+
51+
// WithGitHubInstallationID returns a copy of ctx with the GitHub installation
52+
// ID attached. The transport reads this value to label rate limit metrics with
53+
// the installation that made the request. GitHub enforces rate limits per
54+
// installation, so this label identifies which (app, org) pair is consuming quota.
55+
func WithGitHubInstallationID(ctx context.Context, installationID int64) context.Context {
56+
return context.WithValue(ctx, githubInstallationIDKey, strconv.FormatInt(installationID, 10))
57+
}
58+
3559
var (
3660
mReqCount = promauto.NewCounterVec(
3761
prometheus.CounterOpts{
@@ -283,50 +307,50 @@ var (
283307
Name: "github_rate_limit_remaining",
284308
Help: "The number of requests remaining in the current rate limit window",
285309
},
286-
[]string{"resource", "organization"},
310+
[]string{"resource", "organization", "app_id", "installation_id"},
287311
)
288312
mGitHubRateLimit = promauto.NewGaugeVec(
289313
prometheus.GaugeOpts{
290314
Name: "github_rate_limit",
291315
Help: "The number of requests allowed during the rate limit window",
292316
},
293-
[]string{"resource", "organization"},
317+
[]string{"resource", "organization", "app_id", "installation_id"},
294318
)
295319
mGitHubRateLimitReset = promauto.NewGaugeVec(
296320
prometheus.GaugeOpts{
297321
Name: "github_rate_limit_reset",
298322
Help: "The timestamp at which the current rate limit window resets",
299323
},
300-
[]string{"resource", "organization"},
324+
[]string{"resource", "organization", "app_id", "installation_id"},
301325
)
302326
mGitHubRateLimitUsed = promauto.NewGaugeVec(
303327
prometheus.GaugeOpts{
304328
Name: "github_rate_limit_used",
305329
Help: "The fraction of the rate limit window used",
306330
},
307-
[]string{"resource", "organization"},
331+
[]string{"resource", "organization", "app_id", "installation_id"},
308332
)
309333
mGitHubRateLimitTimeToReset = promauto.NewGaugeVec(
310334
prometheus.GaugeOpts{
311335
Name: "github_rate_limit_time_to_reset",
312336
Help: "The number of minutes until the current rate limit window resets",
313337
},
314-
[]string{"resource", "organization"},
338+
[]string{"resource", "organization", "app_id", "installation_id"},
315339
)
316340
mGitHubRateLimitErrors = promauto.NewCounterVec(
317341
prometheus.CounterOpts{
318342
Name: "github_rate_limit_errors_total",
319343
Help: "GitHub API requests rejected due to rate limiting (403/429 with rate limit headers)",
320344
},
321-
[]string{"resource", "organization", "service_name", "code", "rate_limit_type"},
345+
[]string{"resource", "organization", "app_id", "installation_id", "service_name", "code", "rate_limit_type"},
322346
)
323347
mGitHubRetryAfterSeconds = promauto.NewHistogramVec(
324348
prometheus.HistogramOpts{
325349
Name: "github_retry_after_seconds",
326350
Help: "Retry-After values from GitHub rate limit responses",
327351
Buckets: []float64{1, 5, 15, 30, 60, 120, 300, 900, 1800, 3600},
328352
},
329-
[]string{"organization", "service_name", "rate_limit_type"},
353+
[]string{"organization", "app_id", "installation_id", "service_name", "rate_limit_type"},
330354
)
331355
)
332356

@@ -368,6 +392,13 @@ func instrumentGitHubRateLimits(next http.RoundTripper) promhttp.RoundTripperFun
368392
// Extract organization from the request URL
369393
organization := extractOrgFromGitHubURL(r.URL.Path)
370394

395+
// Read caller-supplied app/installation IDs from the request context.
396+
// These are set by callers via WithGitHubAppID and WithGitHubInstallationID.
397+
// Empty string when not set — callers that do not set these values produce
398+
// time series with app_id="" and installation_id="".
399+
appID, _ := r.Context().Value(githubAppIDKey).(string)
400+
installationID, _ := r.Context().Value(githubInstallationIDKey).(string)
401+
371402
val := func(key string) float64 {
372403
val := resp.Header.Get(key)
373404
if val == "" {
@@ -380,8 +411,10 @@ func instrumentGitHubRateLimits(next http.RoundTripper) promhttp.RoundTripperFun
380411
return float64(i)
381412
}
382413
labels := prometheus.Labels{
383-
"resource": resource,
384-
"organization": organization,
414+
"resource": resource,
415+
"organization": organization,
416+
"app_id": appID,
417+
"installation_id": installationID,
385418
}
386419

387420
remaining := val("X-RateLimit-Remaining")
@@ -446,6 +479,8 @@ func instrumentGitHubRateLimits(next http.RoundTripper) promhttp.RoundTripperFun
446479
mGitHubRateLimitErrors.With(prometheus.Labels{
447480
"resource": resource,
448481
"organization": organization,
482+
"app_id": appID,
483+
"installation_id": installationID,
449484
"service_name": env.KnativeServiceName,
450485
"code": strconv.Itoa(resp.StatusCode),
451486
"rate_limit_type": rateLimitType,
@@ -456,6 +491,8 @@ func instrumentGitHubRateLimits(next http.RoundTripper) promhttp.RoundTripperFun
456491
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
457492
mGitHubRetryAfterSeconds.With(prometheus.Labels{
458493
"organization": organization,
494+
"app_id": appID,
495+
"installation_id": installationID,
459496
"service_name": env.KnativeServiceName,
460497
"rate_limit_type": rateLimitType,
461498
}).Observe(float64(seconds))

pkg/httpmetrics/transport_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
66
package httpmetrics
77

88
import (
9+
"context"
910
"errors"
1011
"fmt"
1112
"net/http"
@@ -123,6 +124,87 @@ func TestTransport_SkipBucketize(t *testing.T) {
123124
}
124125
}
125126

127+
func TestGitHubRateLimitContextLabels(t *testing.T) {
128+
// Verify that WithGitHubAppID / WithGitHubInstallationID values are
129+
// propagated to the rate limit gauge labels.
130+
stub := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
131+
return &http.Response{
132+
StatusCode: http.StatusOK,
133+
Header: http.Header{
134+
"X-Ratelimit-Resource": []string{"core"},
135+
"X-Ratelimit-Remaining": []string{"4500"},
136+
"X-Ratelimit-Limit": []string{"5000"},
137+
"X-Ratelimit-Reset": []string{"9999999999"},
138+
},
139+
Body: http.NoBody,
140+
}, nil
141+
})
142+
143+
transport := instrumentGitHubRateLimits(stub)
144+
145+
ctx := context.Background()
146+
ctx = WithGitHubAppID(ctx, 42)
147+
ctx = WithGitHubInstallationID(ctx, 1234)
148+
149+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/org/repo/contents/file", nil)
150+
if err != nil {
151+
t.Fatal(err)
152+
}
153+
if _, err := transport.RoundTrip(req); err != nil {
154+
t.Fatal(err)
155+
}
156+
157+
labels := prometheus.Labels{
158+
"resource": "core",
159+
"organization": "org",
160+
"app_id": "42",
161+
"installation_id": "1234",
162+
}
163+
if got := testutil.ToFloat64(mGitHubRateLimitRemaining.With(labels)); got != 4500 {
164+
t.Errorf("github_rate_limit_remaining: got %v, want 4500", got)
165+
}
166+
if got := testutil.ToFloat64(mGitHubRateLimit.With(labels)); got != 5000 {
167+
t.Errorf("github_rate_limit: got %v, want 5000", got)
168+
}
169+
}
170+
171+
func TestGitHubRateLimitContextLabels_NoContext(t *testing.T) {
172+
// Callers that do not set context values get empty-string labels — verifies
173+
// backward compatibility with existing consumers of the transport.
174+
stub := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
175+
return &http.Response{
176+
StatusCode: http.StatusOK,
177+
Header: http.Header{
178+
"X-Ratelimit-Resource": []string{"core"},
179+
"X-Ratelimit-Remaining": []string{"3000"},
180+
"X-Ratelimit-Limit": []string{"5000"},
181+
"X-Ratelimit-Reset": []string{"9999999999"},
182+
},
183+
Body: http.NoBody,
184+
}, nil
185+
})
186+
187+
transport := instrumentGitHubRateLimits(stub)
188+
189+
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api.github.com/repos/org2/repo/contents/file", nil)
190+
if err != nil {
191+
t.Fatal(err)
192+
}
193+
if _, err := transport.RoundTrip(req); err != nil {
194+
t.Fatal(err)
195+
}
196+
197+
labels := prometheus.Labels{
198+
"resource": "core",
199+
"organization": "org2",
200+
"app_id": "",
201+
"installation_id": "",
202+
}
203+
if got := testutil.ToFloat64(mGitHubRateLimitRemaining.With(labels)); got != 3000 {
204+
t.Errorf("github_rate_limit_remaining: got %v, want 3000", got)
205+
}
206+
}
207+
126208
func TestDockerHubRateLimitParsing(t *testing.T) {
127209
// Exercise the actual instrumentDockerHubRateLimit round tripper with a
128210
// test server that returns Docker Hub rate limit headers. Before the fix,

0 commit comments

Comments
 (0)