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 }