Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit3f9da67

Browse files
authored
chore: instrument github oauth2 limits (#11532)
* chore: instrument github oauth2 limitsRate limit information for github oauth2 providers instrumented in prometheus
1 parent50b78e3 commit3f9da67

File tree

6 files changed

+421
-10
lines changed

6 files changed

+421
-10
lines changed

‎cli/server.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1802,7 +1802,7 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
18021802
}
18031803

18041804
return&coderd.GithubOAuth2Config{
1805-
OAuth2Config:instrument.New("github-login",&oauth2.Config{
1805+
OAuth2Config:instrument.NewGithub("github-login",&oauth2.Config{
18061806
ClientID:clientID,
18071807
ClientSecret:clientSecret,
18081808
Endpoint:endpoint,

‎coderd/coderdtest/oidctest/idp.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ type FakeIDP struct {
8585
// to test something like PKI auth vs a client_secret.
8686
hookAuthenticateClientfunc(t testing.TB,req*http.Request) (url.Values,error)
8787
servebool
88+
// optional middlewares
89+
middlewares chi.Middlewares
8890
}
8991

9092
funcStatusError(codeint,errerror)error {
@@ -115,6 +117,12 @@ func WithAuthorizedRedirectURL(hook func(redirectURL string) error) func(*FakeID
115117
}
116118
}
117119

120+
funcWithMiddlewares(mws...func(http.Handler) http.Handler)func(*FakeIDP) {
121+
returnfunc(f*FakeIDP) {
122+
f.middlewares=append(f.middlewares,mws...)
123+
}
124+
}
125+
118126
// WithRefresh is called when a refresh token is used. The email is
119127
// the email of the user that is being refreshed assuming the claims are correct.
120128
funcWithRefresh(hookfunc(emailstring)error)func(*FakeIDP) {
@@ -570,6 +578,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
570578
t.Helper()
571579

572580
mux:=chi.NewMux()
581+
mux.Use(f.middlewares...)
573582
// This endpoint is required to initialize the OIDC provider.
574583
// It is used to get the OIDC configuration.
575584
mux.Get("/.well-known/openid-configuration",func(rw http.ResponseWriter,r*http.Request) {

‎coderd/externalauth/externalauth.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,13 @@ func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAut
464464
oauthConfig=&exchangeWithClientSecret{oc}
465465
}
466466

467+
instrumented:=instrument.New(entry.ID,oauthConfig)
468+
ifstrings.EqualFold(entry.Type,string(codersdk.EnhancedExternalAuthProviderGitHub)) {
469+
instrumented=instrument.NewGithub(entry.ID,oauthConfig)
470+
}
471+
467472
cfg:=&Config{
468-
InstrumentedOAuth2Config:instrument.New(entry.ID,oauthConfig),
473+
InstrumentedOAuth2Config:instrumented,
469474
ID:entry.ID,
470475
Regex:regex,
471476
Type:entry.Type,

‎coderd/promoauth/github.go‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package promoauth
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
"time"
8+
)
9+
10+
typerateLimitsstruct {
11+
Limitint
12+
Remainingint
13+
Usedint
14+
Reset time.Time
15+
Resourcestring
16+
}
17+
18+
// githubRateLimits checks the returned response headers and
19+
funcgithubRateLimits(resp*http.Response,errerror) (rateLimits,bool) {
20+
iferr!=nil||resp==nil {
21+
returnrateLimits{},false
22+
}
23+
24+
p:=headerParser{header:resp.Header}
25+
// See
26+
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit
27+
limits:=rateLimits{
28+
Limit:p.int("x-ratelimit-limit"),
29+
Remaining:p.int("x-ratelimit-remaining"),
30+
Used:p.int("x-ratelimit-used"),
31+
Resource:p.string("x-ratelimit-resource"),
32+
}
33+
34+
iflimits.Limit==0&&
35+
limits.Remaining==0&&
36+
limits.Used==0 {
37+
// For some requests, github has no rate limit. In which case,
38+
// it returns all 0s. We can just omit these.
39+
returnlimits,false
40+
}
41+
42+
// Reset is when the rate limit "used" will be reset to 0.
43+
// If it's unix 0, then we do not know when it will reset.
44+
// Change it to a zero time as that is easier to handle in golang.
45+
unix:=p.int("x-ratelimit-reset")
46+
resetAt:=time.Unix(int64(unix),0)
47+
ifunix==0 {
48+
resetAt= time.Time{}
49+
}
50+
limits.Reset=resetAt
51+
52+
// Unauthorized requests have their own rate limit, so we should
53+
// track them separately.
54+
ifresp.StatusCode==http.StatusUnauthorized {
55+
limits.Resource+="-unauthorized"
56+
}
57+
58+
// A 401 or 429 means too many requests. This might mess up the
59+
// "resource" string because we could hit the unauthorized limit,
60+
// and we do not want that to override the authorized one.
61+
// However, in testing, it seems a 401 is always a 401, even if
62+
// the limit is hit.
63+
64+
iflen(p.errors)>0 {
65+
// If we are missing any headers, then do not try and guess
66+
// what the rate limits are.
67+
returnlimits,false
68+
}
69+
returnlimits,true
70+
}
71+
72+
typeheaderParserstruct {
73+
errorsmap[string]error
74+
header http.Header
75+
}
76+
77+
func (p*headerParser)string(keystring)string {
78+
ifp.errors==nil {
79+
p.errors=make(map[string]error)
80+
}
81+
82+
v:=p.header.Get(key)
83+
ifv=="" {
84+
p.errors[key]=fmt.Errorf("missing header %q",key)
85+
}
86+
returnv
87+
}
88+
89+
func (p*headerParser)int(keystring)int {
90+
v:=p.string(key)
91+
ifv=="" {
92+
return-1
93+
}
94+
95+
i,err:=strconv.Atoi(v)
96+
iferr!=nil {
97+
p.errors[key]=err
98+
}
99+
returni
100+
}

‎coderd/promoauth/oauth2.go‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"time"
78

89
"github.com/prometheus/client_golang/prometheus"
910
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -46,11 +47,25 @@ var _ OAuth2Config = (*Config)(nil)
4647
// Primarily to avoid any prometheus errors registering duplicate metrics.
4748
typeFactorystruct {
4849
metrics*metrics
50+
// optional replace now func
51+
Nowfunc() time.Time
4952
}
5053

5154
// metrics is the reusable metrics for all oauth2 providers.
5255
typemetricsstruct {
5356
externalRequestCount*prometheus.CounterVec
57+
58+
// if the oauth supports it, rate limit metrics.
59+
// rateLimit is the defined limit per interval
60+
rateLimit*prometheus.GaugeVec
61+
rateLimitRemaining*prometheus.GaugeVec
62+
rateLimitUsed*prometheus.GaugeVec
63+
// rateLimitReset is unix time of the next interval (when the rate limit resets).
64+
rateLimitReset*prometheus.GaugeVec
65+
// rateLimitResetIn is the time in seconds until the rate limit resets.
66+
// This is included because it is sometimes more helpful to know the limit
67+
// will reset in 600seconds, rather than at 1704000000 unix time.
68+
rateLimitResetIn*prometheus.GaugeVec
5469
}
5570

5671
funcNewFactory(registry prometheus.Registerer)*Factory {
@@ -68,6 +83,53 @@ func NewFactory(registry prometheus.Registerer) *Factory {
6883
"source",
6984
"status_code",
7085
}),
86+
rateLimit:factory.NewGaugeVec(prometheus.GaugeOpts{
87+
Namespace:"coderd",
88+
Subsystem:"oauth2",
89+
Name:"external_requests_rate_limit_total",
90+
Help:"The total number of allowed requests per interval.",
91+
}, []string{
92+
"name",
93+
// Resource allows different rate limits for the same oauth2 provider.
94+
// Some IDPs have different buckets for different rate limits.
95+
"resource",
96+
}),
97+
rateLimitRemaining:factory.NewGaugeVec(prometheus.GaugeOpts{
98+
Namespace:"coderd",
99+
Subsystem:"oauth2",
100+
Name:"external_requests_rate_limit_remaining",
101+
Help:"The remaining number of allowed requests in this interval.",
102+
}, []string{
103+
"name",
104+
"resource",
105+
}),
106+
rateLimitUsed:factory.NewGaugeVec(prometheus.GaugeOpts{
107+
Namespace:"coderd",
108+
Subsystem:"oauth2",
109+
Name:"external_requests_rate_limit_used",
110+
Help:"The number of requests made in this interval.",
111+
}, []string{
112+
"name",
113+
"resource",
114+
}),
115+
rateLimitReset:factory.NewGaugeVec(prometheus.GaugeOpts{
116+
Namespace:"coderd",
117+
Subsystem:"oauth2",
118+
Name:"external_requests_rate_limit_next_reset_unix",
119+
Help:"Unix timestamp for when the next interval starts",
120+
}, []string{
121+
"name",
122+
"resource",
123+
}),
124+
rateLimitResetIn:factory.NewGaugeVec(prometheus.GaugeOpts{
125+
Namespace:"coderd",
126+
Subsystem:"oauth2",
127+
Name:"external_requests_rate_limit_reset_in_seconds",
128+
Help:"Seconds until the next interval",
129+
}, []string{
130+
"name",
131+
"resource",
132+
}),
71133
},
72134
}
73135
}
@@ -80,13 +142,53 @@ func (f *Factory) New(name string, under OAuth2Config) *Config {
80142
}
81143
}
82144

145+
// NewGithub returns a new instrumented oauth2 config for github. It tracks
146+
// rate limits as well as just the external request counts.
147+
//
148+
//nolint:bodyclose
149+
func (f*Factory)NewGithub(namestring,underOAuth2Config)*Config {
150+
cfg:=f.New(name,under)
151+
cfg.interceptors=append(cfg.interceptors,func(resp*http.Response,errerror) {
152+
limits,ok:=githubRateLimits(resp,err)
153+
if!ok {
154+
return
155+
}
156+
labels:= prometheus.Labels{
157+
"name":cfg.name,
158+
"resource":limits.Resource,
159+
}
160+
// Default to -1 for "do not know"
161+
resetIn:=float64(-1)
162+
if!limits.Reset.IsZero() {
163+
now:=time.Now()
164+
iff.Now!=nil {
165+
now=f.Now()
166+
}
167+
resetIn=limits.Reset.Sub(now).Seconds()
168+
ifresetIn<0 {
169+
// If it just reset, just make it 0.
170+
resetIn=0
171+
}
172+
}
173+
174+
f.metrics.rateLimit.With(labels).Set(float64(limits.Limit))
175+
f.metrics.rateLimitRemaining.With(labels).Set(float64(limits.Remaining))
176+
f.metrics.rateLimitUsed.With(labels).Set(float64(limits.Used))
177+
f.metrics.rateLimitReset.With(labels).Set(float64(limits.Reset.Unix()))
178+
f.metrics.rateLimitResetIn.With(labels).Set(resetIn)
179+
})
180+
returncfg
181+
}
182+
83183
typeConfigstruct {
84184
// Name is a human friendly name to identify the oauth2 provider. This should be
85185
// deterministic from restart to restart, as it is going to be used as a label in
86186
// prometheus metrics.
87187
namestring
88188
underlyingOAuth2Config
89189
metrics*metrics
190+
// interceptors are called after every request made by the oauth2 client.
191+
interceptors []func(resp*http.Response,errerror)
90192
}
91193

92194
func (c*Config)Do(ctx context.Context,sourceOauth2Source,req*http.Request) (*http.Response,error) {
@@ -169,5 +271,10 @@ func (i *instrumentedTripper) RoundTrip(r *http.Request) (*http.Response, error)
169271
"source":string(i.source),
170272
"status_code":fmt.Sprintf("%d",statusCode),
171273
}).Inc()
274+
275+
// Handle any extra interceptors.
276+
for_,interceptor:=rangei.c.interceptors {
277+
interceptor(resp,err)
278+
}
172279
returnresp,err
173280
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp