Skip to content

Commit dbfad5b

Browse files
authored
add GitHub API rate limits metrics (#169)
Signed-off-by: Jason Hall <[email protected]>
1 parent 41af8cb commit dbfad5b

File tree

5 files changed

+201
-5
lines changed

5 files changed

+201
-5
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
## Requirements
3+
4+
No requirements.
5+
6+
## Providers
7+
8+
No providers.
9+
10+
## Modules
11+
12+
| Name | Source | Version |
13+
|------|--------|---------|
14+
| <a name="module_collapsible"></a> [collapsible](#module\_collapsible) | ../collapsible | n/a |
15+
| <a name="module_limit"></a> [limit](#module\_limit) | ../../widgets/xy | n/a |
16+
| <a name="module_remaining"></a> [remaining](#module\_remaining) | ../../widgets/xy | n/a |
17+
| <a name="module_reset"></a> [reset](#module\_reset) | ../../widgets/xy | n/a |
18+
| <a name="module_width"></a> [width](#module\_width) | ../width | n/a |
19+
20+
## Resources
21+
22+
No resources.
23+
24+
## Inputs
25+
26+
| Name | Description | Type | Default | Required |
27+
|------|-------------|------|---------|:--------:|
28+
| <a name="input_cloudrun_name"></a> [cloudrun\_name](#input\_cloudrun\_name) | n/a | `string` | n/a | yes |
29+
| <a name="input_collapsed"></a> [collapsed](#input\_collapsed) | n/a | `bool` | `false` | no |
30+
| <a name="input_filter"></a> [filter](#input\_filter) | n/a | `list(string)` | n/a | yes |
31+
| <a name="input_title"></a> [title](#input\_title) | n/a | `string` | n/a | yes |
32+
33+
## Outputs
34+
35+
| Name | Description |
36+
|------|-------------|
37+
| <a name="output_section"></a> [section](#output\_section) | n/a |
38+
<!-- END_TF_DOCS -->
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
variable "title" { type = string }
2+
variable "filter" { type = list(string) }
3+
variable "cloudrun_name" { type = string }
4+
variable "collapsed" { default = false }
5+
6+
module "width" { source = "../width" }
7+
8+
module "remaining" {
9+
source = "../../widgets/xy"
10+
title = "GitHub API Rate Limit Remaining"
11+
filter = concat(var.filter, [
12+
"metric.type=\"prometheus.googleapis.com/github_rate_limit_remaining/gauge\"",
13+
"metric.label.service_name=\"${var.cloudrun_name}\"",
14+
])
15+
group_by_fields = ["resource.label.\"resource\""]
16+
primary_align = "ALIGN_MEAN"
17+
primary_reduce = "REDUCE_SUM"
18+
plot_type = "STACKED_AREA"
19+
}
20+
21+
module "limit" {
22+
source = "../../widgets/xy"
23+
title = "GitHub API Rate Limit"
24+
filter = concat(var.filter, [
25+
"metric.type=\"prometheus.googleapis.com/github_rate_limit/gauge\"",
26+
"metric.label.service_name=\"${var.cloudrun_name}\"",
27+
])
28+
group_by_fields = ["resource.label.\"resource\""]
29+
primary_align = "ALIGN_DELTA"
30+
primary_reduce = "REDUCE_MEAN"
31+
}
32+
33+
module "reset" {
34+
source = "../../widgets/xy"
35+
title = "Next GitHub API reset"
36+
filter = concat(var.filter, [
37+
"metric.type=\"prometheus.googleapis.com/github_rate_limit_remaining_reset/gauge\"",
38+
"metric.label.service_name=\"${var.cloudrun_name}\"",
39+
])
40+
group_by_fields = ["resource.label.\"resource\""]
41+
primary_align = "ALIGN_DELTA"
42+
primary_reduce = "REDUCE_MEAN"
43+
}
44+
45+
locals {
46+
columns = 3
47+
unit = module.width.size / local.columns
48+
49+
// https://www.terraform.io/language/functions/range
50+
// N columns, unit width each ([0, unit, 2 * unit, ...])
51+
col = range(0, local.columns * local.unit, local.unit)
52+
53+
tiles = [{
54+
55+
yPos = local.unit,
56+
xPos = local.col[0],
57+
height = local.unit,
58+
width = local.unit,
59+
widget = module.remaining.widget,
60+
},
61+
{
62+
yPos = local.unit,
63+
xPos = local.col[1],
64+
height = local.unit,
65+
width = local.unit,
66+
widget = module.limit.widget,
67+
},
68+
{
69+
yPos = local.unit,
70+
xPos = local.col[2],
71+
height = local.unit,
72+
width = local.unit,
73+
widget = module.reset.widget,
74+
},
75+
]
76+
}
77+
78+
module "collapsible" {
79+
source = "../collapsible"
80+
81+
title = var.title
82+
tiles = local.tiles
83+
collapsed = var.collapsed
84+
}
85+
86+
output "section" {
87+
value = module.collapsible.section
88+
}

modules/regional-go-service/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ No requirements.
9494
| Name | Description | Type | Default | Required |
9595
|------|-------------|------|---------|:--------:|
9696
| <a name="input_containers"></a> [containers](#input\_containers) | The containers to run in the service. Each container will be run in each region. | <pre>map(object({<br> source = object({<br> base_image = optional(string, "cgr.dev/chainguard/static:latest-glibc")<br> working_dir = string<br> importpath = string<br> })<br> args = optional(list(string), [])<br> ports = optional(list(object({<br> name = optional(string, "http1")<br> container_port = number<br> })), [])<br> resources = optional(<br> object(<br> {<br> limits = optional(object(<br> {<br> cpu = string<br> memory = string<br> }<br> ), null)<br> cpu_idle = optional(bool, true)<br> }<br> ),<br> {<br> cpu_idle = true<br> }<br> )<br> env = optional(list(object({<br> name = string<br> value = optional(string)<br> value_source = optional(object({<br> secret_key_ref = object({<br> secret = string<br> version = string<br> })<br> }), null)<br> })), [])<br> regional-env = optional(list(object({<br> name = string<br> value = map(string)<br> })), [])<br> volume_mounts = optional(list(object({<br> name = string<br> mount_path = string<br> })), [])<br> }))</pre> | n/a | yes |
97-
| <a name="input_egress"></a> [egress](#input\_egress) | The egress mode for the service. Must be one of ALL\_TRAFFIC, or PRIVATE\_RANGES\_ONLY. Egress traffic is routed through the regional VPC network from var.regions. | `string` | `"ALL_TRAFFIC"` | no |
97+
| <a name="input_egress"></a> [egress](#input\_egress) | Which type of egress traffic to send through the VPC.<br><br>- ALL\_TRAFFIC sends all traffic through regional VPC network<br>- PRIVATE\_RANGES\_ONLY sends only traffic to private IP addresses through regional VPC network | `string` | `"ALL_TRAFFIC"` | no |
9898
| <a name="input_execution_environment"></a> [execution\_environment](#input\_execution\_environment) | The execution environment for the service | `string` | `"EXECUTION_ENVIRONMENT_GEN1"` | no |
99-
| <a name="input_ingress"></a> [ingress](#input\_ingress) | The ingress mode for the service. Must be one of INGRESS\_TRAFFIC\_ALL, INGRESS\_TRAFFIC\_INTERNAL\_LOAD\_BALANCER, or INGRESS\_TRAFFIC\_INTERNAL\_ONLY. | `string` | `"INGRESS_TRAFFIC_INTERNAL_ONLY"` | no |
99+
| <a name="input_ingress"></a> [ingress](#input\_ingress) | Which type of ingress traffic to accept for the service.<br><br>- INGRESS\_TRAFFIC\_ALL accepts all traffic, enabling the public .run.app URL for the service<br>- INGRESS\_TRAFFIC\_INTERNAL\_LOAD\_BALANCER accepts traffic only from a load balancer<br>- INGRESS\_TRAFFIC\_INTERNAL\_ONLY accepts internal traffic only | `string` | `"INGRESS_TRAFFIC_INTERNAL_ONLY"` | no |
100100
| <a name="input_name"></a> [name](#input\_name) | n/a | `string` | n/a | yes |
101101
| <a name="input_notification_channels"></a> [notification\_channels](#input\_notification\_channels) | List of notification channels to alert. | `list(string)` | n/a | yes |
102102
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes |

modules/regional-go-service/variables.tf

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,24 @@ variable "regions" {
1616

1717
variable "ingress" {
1818
type = string
19-
description = "The ingress mode for the service. Must be one of INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER, or INGRESS_TRAFFIC_INTERNAL_ONLY."
19+
description = <<EOD
20+
Which type of ingress traffic to accept for the service.
21+
22+
- INGRESS_TRAFFIC_ALL accepts all traffic, enabling the public .run.app URL for the service
23+
- INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER accepts traffic only from a load balancer
24+
- INGRESS_TRAFFIC_INTERNAL_ONLY accepts internal traffic only
25+
EOD
2026
default = "INGRESS_TRAFFIC_INTERNAL_ONLY"
2127
}
2228

2329
variable "egress" {
2430
type = string
25-
description = "The egress mode for the service. Must be one of ALL_TRAFFIC, or PRIVATE_RANGES_ONLY. Egress traffic is routed through the regional VPC network from var.regions."
31+
description = <<EOD
32+
Which type of egress traffic to send through the VPC.
33+
34+
- ALL_TRAFFIC sends all traffic through regional VPC network
35+
- PRIVATE_RANGES_ONLY sends only traffic to private IP addresses through regional VPC network
36+
EOD
2637
default = "ALL_TRAFFIC"
2738
}
2839

pkg/httpmetrics/transport.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"log/slog"
66
"math"
77
"net/http"
8+
"strconv"
89
"strings"
910
"time"
1011

@@ -56,7 +57,8 @@ func WrapTransport(t http.RoundTripper) http.RoundTripper {
5657
return instrumentRoundTripperCounter(
5758
instrumentRoundTripperInFlight(
5859
instrumentRoundTripperDuration(
59-
otelhttp.NewTransport(t))))
60+
instrumentGitHubRateLimits(
61+
otelhttp.NewTransport(t)))))
6062
}
6163

6264
func mapErrorToLabel(err error) string {
@@ -161,3 +163,60 @@ func bucketize(host string) string {
161163
}
162164
return "other"
163165
}
166+
167+
var (
168+
mGitHubRateLimitRemaining = promauto.NewGaugeVec(
169+
prometheus.GaugeOpts{
170+
Name: "github_rate_limit_remaining",
171+
Help: "The number of requests remaining in the current rate limit window",
172+
},
173+
[]string{"resource"},
174+
)
175+
mGitHubRateLimit = promauto.NewGaugeVec(
176+
prometheus.GaugeOpts{
177+
Name: "github_rate_limit",
178+
Help: "The number of requests allowed during the rate limit window",
179+
},
180+
[]string{"resource"},
181+
)
182+
mGitHubRateLimitReset = promauto.NewGaugeVec(
183+
prometheus.GaugeOpts{
184+
Name: "github_rate_limit_reset",
185+
Help: "The timestamp at which the current rate limit window resets",
186+
},
187+
[]string{"resource"},
188+
)
189+
)
190+
191+
// instrumentGitHubRateLimits is a promhttp.RoundTripperFunc that records GitHub rate limit metrics.
192+
// See https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28
193+
func instrumentGitHubRateLimits(next http.RoundTripper) promhttp.RoundTripperFunc {
194+
return func(r *http.Request) (*http.Response, error) {
195+
resp, err := next.RoundTrip(r)
196+
if err != nil {
197+
return resp, err
198+
}
199+
if r.URL.Host == "api.github.com" {
200+
resource := resp.Header.Get("X-RateLimit-Resource")
201+
if resource == "" {
202+
resource = "unknown"
203+
}
204+
205+
set := func(key string, v *prometheus.GaugeVec) {
206+
val := resp.Header.Get(key)
207+
if val == "" {
208+
return
209+
}
210+
i, err := strconv.Atoi(val)
211+
if err != nil {
212+
return
213+
}
214+
v.With(prometheus.Labels{"resource": resource}).Set(float64(i))
215+
}
216+
set("X-RateLimit-Remaining", mGitHubRateLimitRemaining)
217+
set("X-RateLimit-Limit", mGitHubRateLimit)
218+
set("X-RateLimit-Reset", mGitHubRateLimitReset)
219+
}
220+
return resp, err
221+
}
222+
}

0 commit comments

Comments
 (0)