// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package metricsutil import ( "bytes" "encoding/json" "fmt" "net/http" "strings" "sync" "github.com/armon/go-metrics" "github.com/hashicorp/vault/sdk/logical" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/expfmt" ) const ( OpenMetricsMIMEType = "application/openmetrics-text" PrometheusSchemaMIMEType = "prometheus/telemetry" // ErrorContentType is the content type returned by an error response. ErrorContentType = "text/plain" ) const ( PrometheusMetricFormat = "prometheus" ) // PhysicalTableSizeName is a set of gauge metric keys for physical mount table sizes var PhysicalTableSizeName []string = []string{"core", "mount_table", "size"} // LogicalTableSizeName is a set of gauge metric keys for logical mount table sizes var LogicalTableSizeName []string = []string{"core", "mount_table", "num_entries"} type MetricsHelper struct { inMemSink *metrics.InmemSink PrometheusEnabled bool LoopMetrics GaugeMetrics } type GaugeMetrics struct { // Metrics is a map from keys concatenated by "." to the metric. // It is a map because although we do not care about distinguishing // these loop metrics during emission, we must distinguish them // when we update a metric. Metrics sync.Map } type GaugeMetric struct { Value float32 Labels []Label Key []string } func NewMetricsHelper(inMem *metrics.InmemSink, enablePrometheus bool) *MetricsHelper { return &MetricsHelper{inMem, enablePrometheus, GaugeMetrics{Metrics: sync.Map{}}} } func FormatFromRequest(req *logical.Request) string { acceptHeaders := req.Headers["Accept"] if len(acceptHeaders) > 0 { acceptHeader := acceptHeaders[0] if strings.HasPrefix(acceptHeader, OpenMetricsMIMEType) { return PrometheusMetricFormat } // Look for prometheus accept header for _, header := range acceptHeaders { if strings.Contains(header, PrometheusSchemaMIMEType) { return PrometheusMetricFormat } } } return "" } func (m *MetricsHelper) AddGaugeLoopMetric(key []string, val float32, labels []Label) { mapKey := m.CreateMetricsCacheKeyName(key, val, labels) m.LoopMetrics.Metrics.Store(mapKey, GaugeMetric{ Key: key, Value: val, Labels: labels, }) } func (m *MetricsHelper) CreateMetricsCacheKeyName(key []string, val float32, labels []Label) string { var keyJoin string = strings.Join(key, ".") labelJoinStr := "" for _, label := range labels { labelJoinStr = labelJoinStr + label.Name + "|" + label.Value + "||" } keyJoin = keyJoin + "." + labelJoinStr return keyJoin } func (m *MetricsHelper) ResponseForFormat(format string) *logical.Response { switch format { case PrometheusMetricFormat: return m.PrometheusResponse() case "": return m.GenericResponse() default: return &logical.Response{ Data: map[string]interface{}{ logical.HTTPContentType: ErrorContentType, logical.HTTPRawBody: fmt.Sprintf("metric response format %q unknown", format), logical.HTTPStatusCode: http.StatusBadRequest, }, } } } func (m *MetricsHelper) PrometheusResponse() *logical.Response { resp := &logical.Response{ Data: map[string]interface{}{ logical.HTTPContentType: ErrorContentType, logical.HTTPStatusCode: http.StatusBadRequest, }, } if !m.PrometheusEnabled { resp.Data[logical.HTTPRawBody] = "prometheus is not enabled" return resp } metricsFamilies, err := prometheus.DefaultGatherer.Gather() if err != nil && len(metricsFamilies) == 0 { resp.Data[logical.HTTPRawBody] = fmt.Sprintf("no prometheus metrics could be decoded: %s", err) return resp } // Initialize a byte buffer. buf := &bytes.Buffer{} defer buf.Reset() e := expfmt.NewEncoder(buf, expfmt.FmtText) for _, mf := range metricsFamilies { err := e.Encode(mf) if err != nil { resp.Data[logical.HTTPRawBody] = fmt.Sprintf("error during the encoding of metrics: %s", err) return resp } } resp.Data[logical.HTTPContentType] = string(expfmt.FmtText) resp.Data[logical.HTTPRawBody] = buf.Bytes() resp.Data[logical.HTTPStatusCode] = http.StatusOK return resp } func (m *MetricsHelper) GenericResponse() *logical.Response { resp := &logical.Response{ Data: map[string]interface{}{ logical.HTTPContentType: ErrorContentType, logical.HTTPStatusCode: http.StatusBadRequest, }, } summary, err := m.inMemSink.DisplayMetrics(nil, nil) if err != nil { resp.Data[logical.HTTPRawBody] = fmt.Sprintf("error while fetching the in-memory metrics: %s", err) return resp } content, err := json.Marshal(summary) if err != nil { resp.Data[logical.HTTPRawBody] = fmt.Sprintf("error while marshalling the in-memory metrics: %s", err) return resp } resp.Data[logical.HTTPContentType] = "application/json" resp.Data[logical.HTTPRawBody] = content resp.Data[logical.HTTPStatusCode] = http.StatusOK return resp }