github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/metrics-v3-handler.go (about)

     1  // Copyright (c) 2015-2024 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/http"
    24  	"slices"
    25  	"strings"
    26  
    27  	"github.com/minio/minio/internal/logger"
    28  	"github.com/minio/minio/internal/mcontext"
    29  	"github.com/minio/mux"
    30  	"github.com/prometheus/client_golang/prometheus"
    31  	"github.com/prometheus/client_golang/prometheus/promhttp"
    32  )
    33  
    34  type promLogger struct{}
    35  
    36  func (p promLogger) Println(v ...interface{}) {
    37  	s := make([]string, 0, len(v))
    38  	for _, val := range v {
    39  		s = append(s, fmt.Sprintf("%v", val))
    40  	}
    41  	err := fmt.Errorf("metrics handler error: %v", strings.Join(s, " "))
    42  	logger.LogIf(GlobalContext, err)
    43  }
    44  
    45  type metricsV3Server struct {
    46  	registry *prometheus.Registry
    47  	opts     promhttp.HandlerOpts
    48  	authFn   func(http.Handler) http.Handler
    49  
    50  	metricsData *metricsV3Collection
    51  }
    52  
    53  func newMetricsV3Server(authType prometheusAuthType) *metricsV3Server {
    54  	registry := prometheus.NewRegistry()
    55  	authFn := AuthMiddleware
    56  	if authType == prometheusPublic {
    57  		authFn = NoAuthMiddleware
    58  	}
    59  
    60  	metricGroups := newMetricGroups(registry)
    61  
    62  	return &metricsV3Server{
    63  		registry: registry,
    64  		opts: promhttp.HandlerOpts{
    65  			ErrorLog:            promLogger{},
    66  			ErrorHandling:       promhttp.HTTPErrorOnError,
    67  			Registry:            registry,
    68  			MaxRequestsInFlight: 2,
    69  		},
    70  		authFn: authFn,
    71  
    72  		metricsData: metricGroups,
    73  	}
    74  }
    75  
    76  // metricDisplay - contains info on a metric for display purposes.
    77  type metricDisplay struct {
    78  	Name   string   `json:"name"`
    79  	Help   string   `json:"help"`
    80  	Type   string   `json:"type"`
    81  	Labels []string `json:"labels"`
    82  }
    83  
    84  func (md metricDisplay) String() string {
    85  	return fmt.Sprintf("Name: %s\nType: %s\nHelp: %s\nLabels: {%s}\n", md.Name, md.Type, md.Help, strings.Join(md.Labels, ","))
    86  }
    87  
    88  func (md metricDisplay) TableRow() string {
    89  	labels := strings.Join(md.Labels, ",")
    90  	if labels == "" {
    91  		labels = ""
    92  	} else {
    93  		labels = "`" + labels + "`"
    94  	}
    95  	return fmt.Sprintf("| `%s` | `%s` | %s | %s |\n", md.Name, md.Type, md.Help, labels)
    96  }
    97  
    98  // listMetrics - returns a handler that lists all the metrics that could be
    99  // returned for the requested path.
   100  //
   101  // FIXME: It currently only lists `minio_` prefixed metrics.
   102  func (h *metricsV3Server) listMetrics(path string) http.Handler {
   103  	// First collect all matching MetricsGroup's
   104  	matchingMG := make(map[collectorPath]*MetricsGroup)
   105  	for _, collPath := range h.metricsData.collectorPaths {
   106  		if collPath.isDescendantOf(path) {
   107  			if v, ok := h.metricsData.mgMap[collPath]; ok {
   108  				matchingMG[collPath] = v
   109  			} else {
   110  				matchingMG[collPath] = h.metricsData.bucketMGMap[collPath]
   111  			}
   112  		}
   113  	}
   114  
   115  	if len(matchingMG) == 0 {
   116  		return nil
   117  	}
   118  
   119  	var metrics []metricDisplay
   120  	for _, collectorPath := range h.metricsData.collectorPaths {
   121  		if mg, ok := matchingMG[collectorPath]; ok {
   122  			var commonLabels []string
   123  			for k := range mg.ExtraLabels {
   124  				commonLabels = append(commonLabels, k)
   125  			}
   126  			for _, d := range mg.Descriptors {
   127  				labels := slices.Clone(d.VariableLabels)
   128  				labels = append(labels, commonLabels...)
   129  				metric := metricDisplay{
   130  					Name:   mg.MetricFQN(d.Name),
   131  					Help:   d.Help,
   132  					Type:   d.Type.String(),
   133  					Labels: labels,
   134  				}
   135  				metrics = append(metrics, metric)
   136  			}
   137  		}
   138  	}
   139  
   140  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   141  		contentType := r.Header.Get("Content-Type")
   142  		if contentType == "application/json" {
   143  			w.Header().Set("Content-Type", "application/json")
   144  			jsonEncoder := json.NewEncoder(w)
   145  			jsonEncoder.Encode(metrics)
   146  			return
   147  		}
   148  
   149  		// If not JSON, return plain text. We format it as a markdown table for
   150  		// readability.
   151  		w.Header().Set("Content-Type", "text/plain")
   152  		var b strings.Builder
   153  		b.WriteString("| Name | Type | Help | Labels |\n")
   154  		b.WriteString("| ---- | ---- | ---- | ------ |\n")
   155  		for _, metric := range metrics {
   156  			b.WriteString(metric.TableRow())
   157  		}
   158  		w.Write([]byte(b.String()))
   159  	})
   160  }
   161  
   162  func (h *metricsV3Server) handle(path string, isListingRequest bool, buckets []string) http.Handler {
   163  	var notFoundHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   164  		http.Error(w, "Metrics Resource Not found", http.StatusNotFound)
   165  	})
   166  
   167  	// Require that metrics path has at least component.
   168  	if path == "/" {
   169  		return notFoundHandler
   170  	}
   171  
   172  	if isListingRequest {
   173  		handler := h.listMetrics(path)
   174  		if handler == nil {
   175  			return notFoundHandler
   176  		}
   177  		return handler
   178  	}
   179  
   180  	// In each of the following cases, we check if the collect path is a
   181  	// descendant of `path`, and if so, we add the corresponding gatherer to
   182  	// the list of gatherers. This way, /api/a will return all metrics returned
   183  	// by /api/a/b and /api/a/c (and any other matching descendant collector
   184  	// paths).
   185  
   186  	var gatherers []prometheus.Gatherer
   187  	for _, collectorPath := range h.metricsData.collectorPaths {
   188  		if collectorPath.isDescendantOf(path) {
   189  			gatherer := h.metricsData.mgGatherers[collectorPath]
   190  
   191  			// For Bucket metrics we need to set the buckets argument inside the
   192  			// metric group, so that it will affect collection. If no buckets
   193  			// are provided, we will not return bucket metrics.
   194  			if bmg, ok := h.metricsData.bucketMGMap[collectorPath]; ok {
   195  				if len(buckets) == 0 {
   196  					continue
   197  				}
   198  				unLocker := bmg.LockAndSetBuckets(buckets)
   199  				defer unLocker()
   200  			}
   201  			gatherers = append(gatherers, gatherer)
   202  		}
   203  	}
   204  
   205  	if len(gatherers) == 0 {
   206  		return notFoundHandler
   207  	}
   208  
   209  	return promhttp.HandlerFor(prometheus.Gatherers(gatherers), h.opts)
   210  }
   211  
   212  // ServeHTTP - implements http.Handler interface.
   213  //
   214  // When the `list` query parameter is provided (its value is ignored), the
   215  // server lists all metrics that could be returned for the requested path.
   216  //
   217  // The (repeatable) `buckets` query parameter is a list of bucket names (or it
   218  // could be a comma separated value) to return metrics with a bucket label.
   219  // Bucket metrics will be returned only for the provided buckets. If no buckets
   220  // parameter is provided, no bucket metrics are returned.
   221  func (h *metricsV3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   222  	pathComponents := mux.Vars(r)["pathComps"]
   223  	isListingRequest := r.Form.Has("list")
   224  
   225  	// Parse optional buckets query parameter.
   226  	bucketsParam := r.Form["buckets"]
   227  	buckets := make([]string, 0, len(bucketsParam))
   228  	for _, bp := range bucketsParam {
   229  		bp = strings.TrimSpace(bp)
   230  		if bp == "" {
   231  			continue
   232  		}
   233  		splits := strings.Split(bp, ",")
   234  		for _, split := range splits {
   235  			buckets = append(buckets, strings.TrimSpace(split))
   236  		}
   237  	}
   238  
   239  	innerHandler := h.handle(pathComponents, isListingRequest, buckets)
   240  
   241  	// Add tracing to the prom. handler
   242  	tracedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   243  		tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   244  		if ok {
   245  			tc.FuncName = "handler.MetricsV3"
   246  			tc.ResponseRecorder.LogErrBody = true
   247  		}
   248  
   249  		innerHandler.ServeHTTP(w, r)
   250  	})
   251  
   252  	// Add authentication
   253  	h.authFn(tracedHandler).ServeHTTP(w, r)
   254  }