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
}