4
4
"context"
5
5
"fmt"
6
6
"net/http"
7
+ "time"
7
8
8
9
"github.com/prometheus/client_golang/prometheus"
9
10
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -46,11 +47,25 @@ var _ OAuth2Config = (*Config)(nil)
46
47
// Primarily to avoid any prometheus errors registering duplicate metrics.
47
48
type Factory struct {
48
49
metrics * metrics
50
+ // optional replace now func
51
+ Now func () time.Time
49
52
}
50
53
51
54
// metrics is the reusable metrics for all oauth2 providers.
52
55
type metrics struct {
53
56
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
54
69
}
55
70
56
71
func NewFactory (registry prometheus.Registerer ) * Factory {
@@ -68,6 +83,53 @@ func NewFactory(registry prometheus.Registerer) *Factory {
68
83
"source" ,
69
84
"status_code" ,
70
85
}),
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
+ }),
71
133
},
72
134
}
73
135
}
@@ -80,13 +142,53 @@ func (f *Factory) New(name string, under OAuth2Config) *Config {
80
142
}
81
143
}
82
144
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 (name string , under OAuth2Config ) * Config {
150
+ cfg := f .New (name , under )
151
+ cfg .interceptors = append (cfg .interceptors , func (resp * http.Response , err error ) {
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
+ if f .Now != nil {
165
+ now = f .Now ()
166
+ }
167
+ resetIn = limits .Reset .Sub (now ).Seconds ()
168
+ if resetIn < 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
+ return cfg
181
+ }
182
+
83
183
type Config struct {
84
184
// Name is a human friendly name to identify the oauth2 provider. This should be
85
185
// deterministic from restart to restart, as it is going to be used as a label in
86
186
// prometheus metrics.
87
187
name string
88
188
underlying OAuth2Config
89
189
metrics * metrics
190
+ // interceptors are called after every request made by the oauth2 client.
191
+ interceptors []func (resp * http.Response , err error )
90
192
}
91
193
92
194
func (c * Config ) Do (ctx context.Context , source Oauth2Source , req * http.Request ) (* http.Response , error ) {
@@ -169,5 +271,10 @@ func (i *instrumentedTripper) RoundTrip(r *http.Request) (*http.Response, error)
169
271
"source" : string (i .source ),
170
272
"status_code" : fmt .Sprintf ("%d" , statusCode ),
171
273
}).Inc ()
274
+
275
+ // Handle any extra interceptors.
276
+ for _ , interceptor := range i .c .interceptors {
277
+ interceptor (resp , err )
278
+ }
172
279
return resp , err
173
280
}
0 commit comments