From b65a26fbc52afed98dec79124ea8487d7c4343fb Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sat, 25 Jan 2025 21:59:53 +0100 Subject: [PATCH] Logging Middleware: Add GitHub API Rate Limiting information --- githubapp/middleware_logging.go | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/githubapp/middleware_logging.go b/githubapp/middleware_logging.go index 8e4dfec46..422c8dcd5 100644 --- a/githubapp/middleware_logging.go +++ b/githubapp/middleware_logging.go @@ -19,12 +19,21 @@ import ( "io" "net/http" "regexp" + "strconv" "time" "github.com/gregjones/httpcache" "github.com/rs/zerolog" ) +const ( + headerRateLimit = "X-Ratelimit-Limit" + headerRateRemaining = "X-Ratelimit-Remaining" + headerRateUsed = "X-Ratelimit-Used" + headerRateReset = "X-Ratelimit-Reset" + headerRateResource = "X-Ratelimit-Resource" +) + // ClientLogging creates client middleware that logs request and response // information at the given level. If the request fails without creating a // response, it is logged with a status code of -1. The middleware uses a @@ -83,6 +92,10 @@ func ClientLogging(lvl zerolog.Level, opts ...ClientLoggingOption) ClientMiddlew Int64("size", -1) } + if options.LogRateLimitInformation { + addRateLimitInformationToLog(evt, res) + } + evt.Msg("github_request") return res, err }) @@ -95,6 +108,9 @@ type ClientLoggingOption func(*clientLoggingOptions) type clientLoggingOptions struct { RequestBodyPatterns []*regexp.Regexp ResponseBodyPatterns []*regexp.Regexp + + // Output control + LogRateLimitInformation bool } // LogRequestBody enables request body logging for requests to paths matching @@ -117,6 +133,22 @@ func LogResponseBody(patterns ...string) ClientLoggingOption { } } +// EnableRateLimitInformation enables logging of rate limit information like +// the number of requests remaining in the current rate limit window. +func EnableRateLimitInformation() ClientLoggingOption { + return func(opts *clientLoggingOptions) { + opts.LogRateLimitInformation = true + } +} + +// DisableRateLimitInformation disables logging of rate limit information like +// the number of requests remaining in the current rate limit window. +func DisableRateLimitInformation() ClientLoggingOption { + return func(opts *clientLoggingOptions) { + opts.LogRateLimitInformation = false + } +} + func mirrorRequestBody(r *http.Request) (*http.Request, []byte, error) { switch { case r.Body == nil || r.Body == http.NoBody: @@ -174,3 +206,26 @@ func requestMatches(r *http.Request, pats []*regexp.Regexp) bool { func closeBody(b io.ReadCloser) { _ = b.Close() // per http.Transport impl, ignoring close errors is fine } + +func addRateLimitInformationToLog(evt *zerolog.Event, res *http.Response) { + if limitHeader := res.Header.Get(headerRateLimit); limitHeader != "" { + limit, _ := strconv.Atoi(limitHeader) + evt.Int("ratelimit-limit", limit) + } + if remainingHeader := res.Header.Get(headerRateRemaining); remainingHeader != "" { + remaining, _ := strconv.Atoi(remainingHeader) + evt.Int("ratelimit-remaining", remaining) + } + if usedHeader := res.Header.Get(headerRateUsed); usedHeader != "" { + used, _ := strconv.Atoi(usedHeader) + evt.Int("ratelimit-used", used) + } + if resetHeader := res.Header.Get(headerRateReset); resetHeader != "" { + if v, _ := strconv.ParseInt(resetHeader, 10, 64); v != 0 { + evt.Time("ratelimit-reset", time.Unix(v, 0)) + } + } + if resourceHeader := res.Header.Get(headerRateResource); resourceHeader != "" { + evt.Str("ratelimit-resource", resourceHeader) + } +}