add validation test for v3 metrics for all its endpoints (#20094)

add unit test for v3 metrics for all its exposed endpoints

Bonus:
  - support OpenMetrics encoding
  - adds boot time for prometheus
  - continueOnError is better to serve as
    much metrics as possible.
This commit is contained in:
Harshavardhana 2024-07-15 09:28:02 -07:00 committed by GitHub
parent f944a42886
commit e8c54c3d6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 28 deletions

View File

@ -38,7 +38,8 @@ const (
// Standard env prometheus auth type // Standard env prometheus auth type
const ( const (
EnvPrometheusAuthType = "MINIO_PROMETHEUS_AUTH_TYPE" EnvPrometheusAuthType = "MINIO_PROMETHEUS_AUTH_TYPE"
EnvPrometheusOpenMetrics = "MINIO_PROMETHEUS_OPEN_METRICS"
) )
type prometheusAuthType string type prometheusAuthType string
@ -58,14 +59,15 @@ func registerMetricsRouter(router *mux.Router) {
if authType == prometheusPublic { if authType == prometheusPublic {
auth = NoAuthMiddleware auth = NoAuthMiddleware
} }
metricsRouter.Handle(prometheusMetricsPathLegacy, auth(metricsHandler())) metricsRouter.Handle(prometheusMetricsPathLegacy, auth(metricsHandler()))
metricsRouter.Handle(prometheusMetricsV2ClusterPath, auth(metricsServerHandler())) metricsRouter.Handle(prometheusMetricsV2ClusterPath, auth(metricsServerHandler()))
metricsRouter.Handle(prometheusMetricsV2BucketPath, auth(metricsBucketHandler())) metricsRouter.Handle(prometheusMetricsV2BucketPath, auth(metricsBucketHandler()))
metricsRouter.Handle(prometheusMetricsV2NodePath, auth(metricsNodeHandler())) metricsRouter.Handle(prometheusMetricsV2NodePath, auth(metricsNodeHandler()))
metricsRouter.Handle(prometheusMetricsV2ResourcePath, auth(metricsResourceHandler())) metricsRouter.Handle(prometheusMetricsV2ResourcePath, auth(metricsResourceHandler()))
// Metrics v3! // Metrics v3
metricsV3Server := newMetricsV3Server(authType) metricsV3Server := newMetricsV3Server(auth)
// Register metrics v3 handler. It also accepts an optional query // Register metrics v3 handler. It also accepts an optional query
// parameter `?list` - see handler for details. // parameter `?list` - see handler for details.

View File

@ -23,9 +23,12 @@ import (
"net/http" "net/http"
"slices" "slices"
"strings" "strings"
"sync"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/mcontext" "github.com/minio/minio/internal/mcontext"
"github.com/minio/mux" "github.com/minio/mux"
"github.com/minio/pkg/v3/env"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
@ -33,41 +36,39 @@ import (
type promLogger struct{} type promLogger struct{}
func (p promLogger) Println(v ...interface{}) { func (p promLogger) Println(v ...interface{}) {
s := make([]string, 0, len(v)) metricsLogIf(GlobalContext, fmt.Errorf("metrics handler error: %v", v))
for _, val := range v {
s = append(s, fmt.Sprintf("%v", val))
}
err := fmt.Errorf("metrics handler error: %v", strings.Join(s, " "))
metricsLogIf(GlobalContext, err)
} }
type metricsV3Server struct { type metricsV3Server struct {
registry *prometheus.Registry registry *prometheus.Registry
opts promhttp.HandlerOpts opts promhttp.HandlerOpts
authFn func(http.Handler) http.Handler auth func(http.Handler) http.Handler
metricsData *metricsV3Collection metricsData *metricsV3Collection
} }
func newMetricsV3Server(authType prometheusAuthType) *metricsV3Server { var (
globalMetricsV3CollectorPaths []collectorPath
globalMetricsV3Once sync.Once
)
func newMetricsV3Server(auth func(h http.Handler) http.Handler) *metricsV3Server {
registry := prometheus.NewRegistry() registry := prometheus.NewRegistry()
authFn := AuthMiddleware
if authType == prometheusPublic {
authFn = NoAuthMiddleware
}
metricGroups := newMetricGroups(registry) metricGroups := newMetricGroups(registry)
globalMetricsV3Once.Do(func() {
globalMetricsV3CollectorPaths = metricGroups.collectorPaths
})
return &metricsV3Server{ return &metricsV3Server{
registry: registry, registry: registry,
opts: promhttp.HandlerOpts{ opts: promhttp.HandlerOpts{
ErrorLog: promLogger{}, ErrorLog: promLogger{},
ErrorHandling: promhttp.HTTPErrorOnError, ErrorHandling: promhttp.ContinueOnError,
Registry: registry, Registry: registry,
MaxRequestsInFlight: 2, MaxRequestsInFlight: 2,
EnableOpenMetrics: env.Get(EnvPrometheusOpenMetrics, config.EnableOff) == config.EnableOn,
ProcessStartTime: globalBootTime,
}, },
authFn: authFn, auth: auth,
metricsData: metricGroups, metricsData: metricGroups,
} }
} }
@ -221,7 +222,7 @@ func (h *metricsV3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathComponents := mux.Vars(r)["pathComps"] pathComponents := mux.Vars(r)["pathComps"]
isListingRequest := r.Form.Has("list") isListingRequest := r.Form.Has("list")
buckets := []string{} var buckets []string
if strings.HasPrefix(pathComponents, "/bucket/") { if strings.HasPrefix(pathComponents, "/bucket/") {
// bucket specific metrics, extract the bucket name from the path. // bucket specific metrics, extract the bucket name from the path.
// it's the last part of the path. e.g. /bucket/api/<bucket-name> // it's the last part of the path. e.g. /bucket/api/<bucket-name>
@ -246,5 +247,5 @@ func (h *metricsV3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}) })
// Add authentication // Add authentication
h.authFn(tracedHandler).ServeHTTP(w, r) h.auth(tracedHandler).ServeHTTP(w, r)
} }

View File

@ -35,8 +35,8 @@ type collectorPath string
// converted to snake-case (by replaced '/' and '-' with '_') and prefixed with // converted to snake-case (by replaced '/' and '-' with '_') and prefixed with
// `minio_`. // `minio_`.
func (cp collectorPath) metricPrefix() string { func (cp collectorPath) metricPrefix() string {
s := strings.TrimPrefix(string(cp), "/") s := strings.TrimPrefix(string(cp), SlashSeparator)
s = strings.ReplaceAll(s, "/", "_") s = strings.ReplaceAll(s, SlashSeparator, "_")
s = strings.ReplaceAll(s, "-", "_") s = strings.ReplaceAll(s, "-", "_")
return "minio_" + s return "minio_" + s
} }
@ -56,8 +56,8 @@ func (cp collectorPath) isDescendantOf(arg string) bool {
if len(arg) >= len(descendant) { if len(arg) >= len(descendant) {
return false return false
} }
if !strings.HasSuffix(arg, "/") { if !strings.HasSuffix(arg, SlashSeparator) {
arg += "/" arg += SlashSeparator
} }
return strings.HasPrefix(descendant, arg) return strings.HasPrefix(descendant, arg)
} }
@ -72,10 +72,11 @@ const (
GaugeMT GaugeMT
// HistogramMT - represents a histogram metric. // HistogramMT - represents a histogram metric.
HistogramMT HistogramMT
// rangeL - represents a range label.
rangeL = "range"
) )
// rangeL - represents a range label.
const rangeL = "range"
func (mt MetricType) String() string { func (mt MetricType) String() string {
switch mt { switch mt {
case CounterMT: case CounterMT:

View File

@ -35,6 +35,7 @@ import (
"time" "time"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/pkg/v3/policy" "github.com/minio/pkg/v3/policy"
@ -122,6 +123,9 @@ func runAllTests(suite *TestSuiteCommon, c *check) {
suite.TestObjectMultipartListError(c) suite.TestObjectMultipartListError(c)
suite.TestObjectValidMD5(c) suite.TestObjectValidMD5(c)
suite.TestObjectMultipart(c) suite.TestObjectMultipart(c)
suite.TestMetricsV3Handler(c)
suite.TestBucketSQSNotificationWebHook(c)
suite.TestBucketSQSNotificationAMQP(c)
suite.TearDownSuite(c) suite.TearDownSuite(c)
} }
@ -189,6 +193,36 @@ func (s *TestSuiteCommon) TearDownSuite(c *check) {
s.testServer.Stop() s.testServer.Stop()
} }
const (
defaultPrometheusJWTExpiry = 100 * 365 * 24 * time.Hour
)
func (s *TestSuiteCommon) TestMetricsV3Handler(c *check) {
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: time.Now().UTC().Add(defaultPrometheusJWTExpiry).Unix(),
Subject: s.accessKey,
Issuer: "prometheus",
})
token, err := jwt.SignedString([]byte(s.secretKey))
c.Assert(err, nil)
for _, cpath := range globalMetricsV3CollectorPaths {
request, err := newTestSignedRequest(http.MethodGet, s.endPoint+minioReservedBucketPath+metricsV3Path+string(cpath),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
request.Header.Set("Authorization", "Bearer "+token)
// execute the request.
response, err := s.client.Do(request)
c.Assert(err, nil)
// assert the http response status code.
c.Assert(response.StatusCode, http.StatusOK)
}
}
func (s *TestSuiteCommon) TestBucketSQSNotificationWebHook(c *check) { func (s *TestSuiteCommon) TestBucketSQSNotificationWebHook(c *check) {
// Sample bucket notification. // Sample bucket notification.
bucketNotificationBuf := `<NotificationConfiguration><QueueConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Queue>arn:minio:sqs:us-east-1:444455556666:webhook</Queue></QueueConfiguration></NotificationConfiguration>` bucketNotificationBuf := `<NotificationConfiguration><QueueConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Queue>arn:minio:sqs:us-east-1:444455556666:webhook</Queue></QueueConfiguration></NotificationConfiguration>`