| // 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 |
| } |