Files
trufflehog/pkg/common/http_metrics.go
Mustansir 1935692d46 Add Metrics to RetryableHTTPClient (#4545)
* add response body size metric to InstrumentedTransport, add InstrumentedTransport to RetryableHTTPClient

* add doc to responseSizeCounterReadCloser, remove unused field from test

* record response using ContentLength
2025-12-04 11:43:51 +05:00

125 lines
3.3 KiB
Go

package common
import (
"net/url"
"strconv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "requests_total",
Help: "Total number of HTTP requests made, labeled by URL.",
},
[]string{"url"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "request_duration_seconds",
Help: "HTTP request latency in seconds, labeled by URL.",
Buckets: prometheus.DefBuckets,
},
[]string{"url"},
)
httpNon200ResponsesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "non_200_responses_total",
Help: "Total number of non-200 HTTP responses, labeled by URL and status code.",
},
[]string{"url", "status_code"},
)
httpResponseBodySizeBytes = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: "http_client",
Name: "response_body_size_bytes",
Help: "Size of HTTP response bodies in bytes, labeled by URL.",
Buckets: prometheus.ExponentialBuckets(100, 10, 5), // [100B, 1KB, 10KB, 100KB, 1MB]
},
[]string{"url"},
)
)
// sanitizeURL sanitizes a URL to avoid high cardinality metrics.
// It keeps only the host and path, removing query parameters, fragments, and user info.
func sanitizeURL(rawURL string) string {
if rawURL == "" {
return "unknown"
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "invalid_url"
}
// Build sanitized URL with just scheme, host, and path
sanitized := &url.URL{
Scheme: parsedURL.Scheme,
Host: parsedURL.Host,
Path: parsedURL.Path,
}
// If host is empty, try to extract from the raw URL
if sanitized.Host == "" {
// For relative URLs or malformed URLs, just use a placeholder
return "relative_or_invalid"
}
// Normalize path
if sanitized.Path == "" {
sanitized.Path = "/"
}
// Limit path length to avoid extremely long paths creating high cardinality
if len(sanitized.Path) > 100 {
sanitized.Path = sanitized.Path[:100] + "..."
}
result := sanitized.String()
// Final fallback to avoid empty strings
if result == "" {
return "unknown"
}
return result
}
// recordHTTPRequest records metrics for an HTTP request.
func recordHTTPRequest(sanitizedURL string) {
httpRequestsTotal.WithLabelValues(sanitizedURL).Inc()
}
// recordHTTPResponse records metrics for an HTTP response.
func recordHTTPResponse(sanitizedURL string, statusCode int, durationSeconds float64, contentLength int64) {
// Record latency
httpRequestDuration.WithLabelValues(sanitizedURL).Observe(durationSeconds)
// Record non-200 responses
if statusCode != 200 {
httpNon200ResponsesTotal.WithLabelValues(sanitizedURL, strconv.Itoa(statusCode)).Inc()
}
// Record response body size if known
if contentLength >= 0 {
httpResponseBodySizeBytes.WithLabelValues(sanitizedURL).Observe(float64(contentLength))
}
}
// recordNetworkError records metrics for failed HTTP response
func recordNetworkError(sanitizedURL string) {
httpNon200ResponsesTotal.WithLabelValues(sanitizedURL, "network_error").Inc()
}