mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
The openmetrics proposed standard says we should use the `application/openmetrics-text` content type when exporting the metrics. Currently we use `text/plain` for backwards compatibility with Prometheus, but the new content type is apparently supported since 2018, so it should be safe to update to match the current proposed standard.
228 lines
5.7 KiB
Go
228 lines
5.7 KiB
Go
// Package expvarom implements an OpenMetrics HTTP exporter for the variables
|
|
// from the expvar package.
|
|
//
|
|
// This is useful for small servers that want to support both packages with
|
|
// simple enough variables, without introducing any dependencies beyond the
|
|
// standard library.
|
|
//
|
|
// Some functions to add descriptions and map labels are exported for
|
|
// convenience, but their usage is optional.
|
|
//
|
|
// For more complex usage (like histograms, counters vs. gauges, etc.), use
|
|
// the OpenMetrics libraries directly.
|
|
//
|
|
// The exporter uses the text-based format, as documented in:
|
|
// https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
|
|
// https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md
|
|
//
|
|
// Note the adoption of that format as OpenMetrics' one isn't finalized yet,
|
|
// and it is possible that it will change in the future.
|
|
//
|
|
// Backwards compatibility is NOT guaranteed, until the format is fully
|
|
// standarized.
|
|
package expvarom
|
|
|
|
import (
|
|
"expvar"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type exportedVar struct {
|
|
Name string
|
|
Desc string
|
|
LabelName string
|
|
|
|
I *expvar.Int
|
|
F *expvar.Float
|
|
M *expvar.Map
|
|
}
|
|
|
|
var (
|
|
infoMu = sync.Mutex{}
|
|
descriptions = map[string]string{}
|
|
mapLabelNames = map[string]string{}
|
|
)
|
|
|
|
// MetricsHandler implements an http.HandlerFunc which serves the registered
|
|
// metrics, using the OpenMetrics text-based format.
|
|
func MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type",
|
|
"application/openmetrics-text; version=1.0.0; charset=utf-8")
|
|
|
|
vars := []exportedVar{}
|
|
ignored := []string{}
|
|
expvar.Do(func(kv expvar.KeyValue) {
|
|
evar := exportedVar{
|
|
Name: metricNameToOM(kv.Key),
|
|
}
|
|
switch value := kv.Value.(type) {
|
|
case *expvar.Int:
|
|
evar.I = value
|
|
case *expvar.Float:
|
|
evar.F = value
|
|
case *expvar.Map:
|
|
evar.M = value
|
|
default:
|
|
// Unsupported type, ignore this variable.
|
|
ignored = append(ignored, evar.Name)
|
|
return
|
|
}
|
|
|
|
infoMu.Lock()
|
|
evar.Desc = descriptions[kv.Key]
|
|
evar.LabelName = mapLabelNames[kv.Key]
|
|
infoMu.Unlock()
|
|
|
|
// OM maps need a label name, while expvar ones do not. If we weren't
|
|
// told what to use, use a generic "key".
|
|
if evar.LabelName == "" {
|
|
evar.LabelName = "key"
|
|
}
|
|
|
|
vars = append(vars, evar)
|
|
})
|
|
|
|
// Sort the variables for reproducibility and readability.
|
|
sort.Slice(vars, func(i, j int) bool {
|
|
return vars[i].Name < vars[j].Name
|
|
})
|
|
|
|
for _, v := range vars {
|
|
writeVar(w, &v)
|
|
}
|
|
|
|
fmt.Fprintf(w, "# Generated by expvarom\n")
|
|
fmt.Fprintf(w, "# EXPERIMENTAL - Format is not fully standard yet\n")
|
|
fmt.Fprintf(w, "# Ignored variables: %q\n", ignored)
|
|
fmt.Fprintf(w, "# EOF\n") // Mandated by the standard.
|
|
}
|
|
|
|
func writeVar(w io.Writer, v *exportedVar) {
|
|
if v.Desc != "" {
|
|
fmt.Fprintf(w, "# HELP %s %s\n", v.Name, v.Desc)
|
|
}
|
|
|
|
if v.I != nil {
|
|
fmt.Fprintf(w, "%s %d\n\n", v.Name, v.I.Value())
|
|
return
|
|
}
|
|
|
|
if v.F != nil {
|
|
fmt.Fprintf(w, "%s %g\n\n", v.Name, v.F.Value())
|
|
return
|
|
}
|
|
|
|
if v.M != nil {
|
|
count := 0
|
|
v.M.Do(func(kv expvar.KeyValue) {
|
|
vs := ""
|
|
switch value := kv.Value.(type) {
|
|
case *expvar.Int:
|
|
vs = strconv.FormatInt(value.Value(), 10)
|
|
case *expvar.Float:
|
|
vs = strconv.FormatFloat(value.Value(), 'g', -1, 64)
|
|
default:
|
|
// We only support Int and Float in maps.
|
|
return
|
|
}
|
|
|
|
labelValue := quoteLabelValue(kv.Key)
|
|
|
|
fmt.Fprintf(w, "%s{%s=%s} %s\n",
|
|
v.Name, v.LabelName, labelValue, vs)
|
|
count++
|
|
})
|
|
if count > 0 {
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// metricNameToOM converts an expvar metric name into an OpenMetrics-compliant
|
|
// metric name. The latter is more restrictive, as it must match the regexp
|
|
// "[a-zA-Z_:][a-zA-Z0-9_:]*", AND the ':' is not allowed for a direct
|
|
// exporter.
|
|
//
|
|
// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
|
|
func metricNameToOM(name string) string {
|
|
n := ""
|
|
for _, c := range name {
|
|
if (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
c == '_' {
|
|
n += string(c)
|
|
} else {
|
|
n += "_"
|
|
}
|
|
}
|
|
|
|
// If it begins with a number, prepend 'i' as a compromise.
|
|
if len(n) > 0 && n[0] >= '0' && n[0] <= '9' {
|
|
n = "i" + n
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// According to the spec, we only need to replace these 3 characters in label
|
|
// values.
|
|
var labelValueReplacer = strings.NewReplacer(
|
|
`\`, `\\`,
|
|
`"`, `\"`,
|
|
"\n", `\n`)
|
|
|
|
// quoteLabelValue takes an arbitrary string, and quotes it so it can be
|
|
// used as a label value. Output includes the wrapping `"`.
|
|
func quoteLabelValue(v string) string {
|
|
// The spec requires label values to be valid UTF8, with `\`, `"` and "\n"
|
|
// escaped. If it's invalid UTF8, hard-quote it first. This will result
|
|
// in uglier looking values, but they will be well formed.
|
|
if !utf8.ValidString(v) {
|
|
v = strconv.QuoteToASCII(v)
|
|
v = v[1 : len(v)-1]
|
|
}
|
|
|
|
return `"` + labelValueReplacer.Replace(v) + `"`
|
|
}
|
|
|
|
// NewInt registers a new expvar.Int variable, with the given description.
|
|
func NewInt(name, desc string) *expvar.Int {
|
|
infoMu.Lock()
|
|
descriptions[name] = desc
|
|
infoMu.Unlock()
|
|
return expvar.NewInt(name)
|
|
}
|
|
|
|
// NewFloat registers a new expvar.Float variable, with the given description.
|
|
func NewFloat(name, desc string) *expvar.Float {
|
|
infoMu.Lock()
|
|
descriptions[name] = desc
|
|
infoMu.Unlock()
|
|
return expvar.NewFloat(name)
|
|
}
|
|
|
|
// NewMap registers a new expvar.Map variable, with the given label
|
|
// name and description.
|
|
func NewMap(name, labelName, desc string) *expvar.Map {
|
|
// Prevent accidents when using the description as the label name.
|
|
if strings.Contains(labelName, " ") {
|
|
panic(fmt.Sprintf(
|
|
"label name has spaces, mix up with the description? %q",
|
|
labelName))
|
|
}
|
|
|
|
infoMu.Lock()
|
|
descriptions[name] = desc
|
|
mapLabelNames[name] = labelName
|
|
infoMu.Unlock()
|
|
return expvar.NewMap(name)
|
|
}
|