177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
// 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
|
|
}
|