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  }