This patch fixes a variety of typos in documentation and comments, found by running `codespell`.
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
|
|
// standardized.
|
|
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)
|
|
}
|