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

Commit184fcf6

Browse files
committed
chore: instrument github oauth2 limits
1 parent50b78e3 commit184fcf6

File tree

4 files changed

+196
-3
lines changed

4 files changed

+196
-3
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/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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.header.Get("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+
returnlimits,false
66+
}
67+
returnlimits,true
68+
}
69+
70+
typeheaderParserstruct {
71+
errorsmap[string]error
72+
header http.Header
73+
}
74+
75+
func (p*headerParser)int(keystring)int {
76+
ifp.errors==nil {
77+
p.errors=make(map[string]error)
78+
}
79+
80+
v:=p.header.Get(key)
81+
ifv=="" {
82+
p.errors[key]=fmt.Errorf("missing header %q",key)
83+
}
84+
85+
i,err:=strconv.Atoi(v)
86+
iferr!=nil {
87+
p.errors[key]=err
88+
}
89+
returni
90+
}

‎coderd/promoauth/oauth2.go‎

Lines changed: 99 additions & 1 deletion
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"
@@ -51,6 +52,16 @@ type Factory struct {
5152
// metrics is the reusable metrics for all oauth2 providers.
5253
typemetricsstruct {
5354
externalRequestCount*prometheus.CounterVec
55+
56+
// if the oauth supports it, rate limit metrics.
57+
// rateLimit is the defined limit per interval
58+
rateLimit*prometheus.GaugeVec
59+
rateLimitRemaining*prometheus.GaugeVec
60+
rateLimitUsed*prometheus.GaugeVec
61+
// rateLimitReset is the time in seconds the rate limit resets.
62+
rateLimitReset*prometheus.GaugeVec
63+
// rateLimitResetIn is the time in seconds until the rate limit resets.
64+
rateLimitResetIn*prometheus.GaugeVec
5465
}
5566

5667
funcNewFactory(registry prometheus.Registerer)*Factory {
@@ -68,25 +79,107 @@ func NewFactory(registry prometheus.Registerer) *Factory {
6879
"source",
6980
"status_code",
7081
}),
82+
rateLimit:factory.NewGaugeVec(prometheus.GaugeOpts{
83+
Namespace:"coderd",
84+
Subsystem:"oauth2",
85+
Name:"external_requests_rate_limit_total",
86+
Help:"The total number of allowed requests per interval.",
87+
}, []string{
88+
"name",
89+
// Resource allows different rate limits for the same oauth2 provider.
90+
// Some IDPs have different buckets for different rate limits.
91+
"resource",
92+
}),
93+
rateLimitRemaining:factory.NewGaugeVec(prometheus.GaugeOpts{
94+
Namespace:"coderd",
95+
Subsystem:"oauth2",
96+
Name:"external_requests_rate_limit_remaining",
97+
Help:"The remaining number of allowed requests in this interval.",
98+
}, []string{
99+
"name",
100+
"resource",
101+
}),
102+
rateLimitUsed:factory.NewGaugeVec(prometheus.GaugeOpts{
103+
Namespace:"coderd",
104+
Subsystem:"oauth2",
105+
Name:"external_requests_rate_limit_used",
106+
Help:"The number of requests made in this interval.",
107+
}, []string{
108+
"name",
109+
"resource",
110+
}),
111+
rateLimitReset:factory.NewGaugeVec(prometheus.GaugeOpts{
112+
Namespace:"coderd",
113+
Subsystem:"oauth2",
114+
Name:"external_requests_rate_limit_next_reset_unix",
115+
Help:"Unix timestamp of the next interval",
116+
}, []string{
117+
"name",
118+
"resource",
119+
}),
120+
rateLimitResetIn:factory.NewGaugeVec(prometheus.GaugeOpts{
121+
Namespace:"coderd",
122+
Subsystem:"oauth2",
123+
Name:"external_requests_rate_limit_reset_in_seconds",
124+
Help:"Seconds until the next interval",
125+
}, []string{
126+
"name",
127+
"resource",
128+
}),
71129
},
72130
}
73131
}
74132

75-
func (f*Factory)New(namestring,underOAuth2Config)*Config {
133+
func (f*Factory)New(namestring,underOAuth2Config,opts...func(cfg*Config))*Config {
76134
return&Config{
77135
name:name,
78136
underlying:under,
79137
metrics:f.metrics,
80138
}
81139
}
82140

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

92185
func (c*Config)Do(ctx context.Context,sourceOauth2Source,req*http.Request) (*http.Response,error) {
@@ -169,5 +262,10 @@ func (i *instrumentedTripper) RoundTrip(r *http.Request) (*http.Response, error)
169262
"source":string(i.source),
170263
"status_code":fmt.Sprintf("%d",statusCode),
171264
}).Inc()
265+
266+
// Handle any extra interceptors.
267+
for_,interceptor:=rangei.c.interceptors {
268+
interceptor(resp,err)
269+
}
172270
returnresp,err
173271
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp