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() }