mirror of https://github.com/minio/minio.git
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:
parent
f944a42886
commit
e8c54c3d6c
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>`
|
||||||
|
|
Loading…
Reference in New Issue