github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/services/github_metrics.go (about) 1 package services 2 3 import ( 4 "net/http" 5 "strconv" 6 "time" 7 8 "github.com/prometheus/client_golang/prometheus" 9 log "github.com/sirupsen/logrus" 10 "sigs.k8s.io/controller-runtime/pkg/metrics" 11 ) 12 13 // Doc for the GitHub API rate limit headers: 14 // 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 15 16 // Metric names as constants 17 const ( 18 githubAPIRequestTotalMetricName = "argocd_github_api_requests_total" 19 githubAPIRequestDurationMetricName = "argocd_github_api_request_duration_seconds" 20 githubAPIRateLimitRemainingMetricName = "argocd_github_api_rate_limit_remaining" 21 githubAPIRateLimitLimitMetricName = "argocd_github_api_rate_limit_limit" 22 githubAPIRateLimitResetMetricName = "argocd_github_api_rate_limit_reset_seconds" 23 githubAPIRateLimitUsedMetricName = "argocd_github_api_rate_limit_used" 24 ) 25 26 // GitHubMetrics groups all metric vectors for easier injection and registration 27 type GitHubMetrics struct { 28 RequestTotal *prometheus.CounterVec 29 RequestDuration *prometheus.HistogramVec 30 RateLimitRemaining *prometheus.GaugeVec 31 RateLimitLimit *prometheus.GaugeVec 32 RateLimitReset *prometheus.GaugeVec 33 RateLimitUsed *prometheus.GaugeVec 34 } 35 36 // Factory for a new set of GitHub metrics (for tests or custom registries) 37 func NewGitHubMetrics() *GitHubMetrics { 38 return &GitHubMetrics{ 39 RequestTotal: NewGitHubAPIRequestTotal(), 40 RequestDuration: NewGitHubAPIRequestDuration(), 41 RateLimitRemaining: NewGitHubAPIRateLimitRemaining(), 42 RateLimitLimit: NewGitHubAPIRateLimitLimit(), 43 RateLimitReset: NewGitHubAPIRateLimitReset(), 44 RateLimitUsed: NewGitHubAPIRateLimitUsed(), 45 } 46 } 47 48 // Factory functions for each metric vector 49 func NewGitHubAPIRequestTotal() *prometheus.CounterVec { 50 return prometheus.NewCounterVec( 51 prometheus.CounterOpts{ 52 Name: githubAPIRequestTotalMetricName, 53 Help: "Total number of GitHub API requests", 54 }, 55 []string{"method", "endpoint", "status", "appset_namespace", "appset_name"}, 56 ) 57 } 58 59 func NewGitHubAPIRequestDuration() *prometheus.HistogramVec { 60 return prometheus.NewHistogramVec( 61 prometheus.HistogramOpts{ 62 Name: githubAPIRequestDurationMetricName, 63 Help: "GitHub API request duration in seconds", 64 Buckets: prometheus.DefBuckets, 65 }, 66 []string{"method", "endpoint", "appset_namespace", "appset_name"}, 67 ) 68 } 69 70 func NewGitHubAPIRateLimitRemaining() *prometheus.GaugeVec { 71 return prometheus.NewGaugeVec( 72 prometheus.GaugeOpts{ 73 Name: githubAPIRateLimitRemainingMetricName, 74 Help: "The number of requests remaining in the current rate limit window", 75 }, 76 []string{"endpoint", "appset_namespace", "appset_name", "resource"}, 77 ) 78 } 79 80 func NewGitHubAPIRateLimitLimit() *prometheus.GaugeVec { 81 return prometheus.NewGaugeVec( 82 prometheus.GaugeOpts{ 83 Name: githubAPIRateLimitLimitMetricName, 84 Help: "The maximum number of requests that you can make per hour", 85 }, 86 []string{"endpoint", "appset_namespace", "appset_name", "resource"}, 87 ) 88 } 89 90 func NewGitHubAPIRateLimitReset() *prometheus.GaugeVec { 91 return prometheus.NewGaugeVec( 92 prometheus.GaugeOpts{ 93 Name: githubAPIRateLimitResetMetricName, 94 Help: "The time left till the current rate limit window resets, in seconds", 95 }, 96 []string{"endpoint", "appset_namespace", "appset_name", "resource"}, 97 ) 98 } 99 100 func NewGitHubAPIRateLimitUsed() *prometheus.GaugeVec { 101 return prometheus.NewGaugeVec( 102 prometheus.GaugeOpts{ 103 Name: githubAPIRateLimitUsedMetricName, 104 Help: "The number of requests used in the current rate limit window", 105 }, 106 []string{"endpoint", "appset_namespace", "appset_name", "resource"}, 107 ) 108 } 109 110 // Global metrics (registered with the default registry) 111 var globalGitHubMetrics = NewGitHubMetrics() 112 113 func init() { 114 log.Debug("Registering GitHub API AppSet metrics") 115 metrics.Registry.MustRegister(globalGitHubMetrics.RequestTotal) 116 metrics.Registry.MustRegister(globalGitHubMetrics.RequestDuration) 117 metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitRemaining) 118 metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitLimit) 119 metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitReset) 120 metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitUsed) 121 } 122 123 type MetricsContext struct { 124 AppSetNamespace string 125 AppSetName string 126 } 127 128 // GitHubMetricsTransport is a custom http.RoundTripper that collects GitHub API metrics 129 type GitHubMetricsTransport struct { 130 transport http.RoundTripper 131 metricsContext *MetricsContext 132 metrics *GitHubMetrics 133 } 134 135 // RoundTrip implements http.RoundTripper interface and collects metrics along with debug logging 136 func (t *GitHubMetricsTransport) RoundTrip(req *http.Request) (*http.Response, error) { 137 endpoint := req.URL.Path 138 method := req.Method 139 140 appsetNamespace := "unknown" 141 appsetName := "unknown" 142 143 if t.metricsContext != nil { 144 appsetNamespace = t.metricsContext.AppSetNamespace 145 appsetName = t.metricsContext.AppSetName 146 } 147 148 log.WithFields(log.Fields{ 149 "method": method, 150 "endpoint": endpoint, 151 "applicationset": map[string]string{"name": appsetName, "namespace": appsetNamespace}, 152 }).Debugf("Invoking GitHub API") 153 154 startTime := time.Now() 155 resp, err := t.transport.RoundTrip(req) 156 duration := time.Since(startTime) 157 158 // Record metrics 159 t.metrics.RequestDuration.WithLabelValues(method, endpoint, appsetNamespace, appsetName).Observe(duration.Seconds()) 160 161 status := "0" 162 if resp != nil { 163 status = strconv.Itoa(resp.StatusCode) 164 } 165 t.metrics.RequestTotal.WithLabelValues(method, endpoint, status, appsetNamespace, appsetName).Inc() 166 167 if resp != nil { 168 resetHumanReadableTime := "" 169 remainingInt := 0 170 limitInt := 0 171 usedInt := 0 172 resource := resp.Header.Get("X-RateLimit-Resource") 173 174 // Record rate limit metrics if available 175 if resetTime := resp.Header.Get("X-RateLimit-Reset"); resetTime != "" { 176 if resetUnix, err := strconv.ParseInt(resetTime, 10, 64); err == nil { 177 // Calculate seconds until reset (reset timestamp - current time) 178 secondsUntilReset := resetUnix - time.Now().Unix() 179 t.metrics.RateLimitReset.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(secondsUntilReset)) 180 resetHumanReadableTime = time.Unix(resetUnix, 0).Local().Format("2006-01-02 15:04:05 MST") 181 } 182 } 183 if remaining := resp.Header.Get("X-RateLimit-Remaining"); remaining != "" { 184 if remainingInt, err = strconv.Atoi(remaining); err == nil { 185 t.metrics.RateLimitRemaining.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(remainingInt)) 186 } 187 } 188 if limit := resp.Header.Get("X-RateLimit-Limit"); limit != "" { 189 if limitInt, err = strconv.Atoi(limit); err == nil { 190 t.metrics.RateLimitLimit.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(limitInt)) 191 } 192 } 193 if used := resp.Header.Get("X-RateLimit-Used"); used != "" { 194 if usedInt, err = strconv.Atoi(used); err == nil { 195 t.metrics.RateLimitUsed.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(usedInt)) 196 } 197 } 198 199 log.WithFields(log.Fields{ 200 "endpoint": endpoint, 201 "reset": resetHumanReadableTime, 202 "remaining": remainingInt, 203 "limit": limitInt, 204 "used": usedInt, 205 "resource": resource, 206 "applicationset": map[string]string{"name": appsetName, "namespace": appsetNamespace}, 207 }).Debugf("GitHub API rate limit info") 208 } 209 210 return resp, err 211 } 212 213 // Full constructor (for tests and advanced use) 214 func NewGitHubMetricsTransport( 215 transport http.RoundTripper, 216 metricsContext *MetricsContext, 217 metrics *GitHubMetrics, 218 ) *GitHubMetricsTransport { 219 return &GitHubMetricsTransport{ 220 transport: transport, 221 metricsContext: metricsContext, 222 metrics: metrics, 223 } 224 } 225 226 // Default constructor 227 func NewDefaultGitHubMetricsTransport(transport http.RoundTripper, metricsContext *MetricsContext) *GitHubMetricsTransport { 228 return NewGitHubMetricsTransport( 229 transport, 230 metricsContext, 231 globalGitHubMetrics, 232 ) 233 } 234 235 // NewGitHubMetricsClient wraps an http.Client with metrics middleware 236 func NewGitHubMetricsClient(metricsContext *MetricsContext) *http.Client { 237 log.Debug("Creating new GitHub metrics client") 238 return &http.Client{ 239 Transport: NewDefaultGitHubMetricsTransport(http.DefaultTransport, metricsContext), 240 } 241 }