* add response body size metric to InstrumentedTransport, add InstrumentedTransport to RetryableHTTPClient * add doc to responseSizeCounterReadCloser, remove unused field from test * record response using ContentLength
125 lines
3.3 KiB
Go
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()
|
|
}
|