aboutsummaryrefslogtreecommitdiff
path: root/microprom/microprom.go
blob: 4055c8b15bdc620cefb86223cba1bf70a276034e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0

// Package microprom is a minimal alternative implementation of [promhttp],
// intended for very specific situations where the design choices of [prometheus/client_golang]
// cause scaling problems:
//   - metric families with very high cardinality,
//   - that may have lots of label dimensions,
//   - and which do not need to be held in memory, but can instead easily be generated at scrape time (e.g. from a database query).
//
// In this specific circumstance, the internal structure of [prometheus/client_golang]
// leads to abnormally high memory fragmentation and a spiky memory usage pattern overall.
// Implementing the same metrics endpoint with microprom will lead to a
// more stable memory consumption with less intense spikes during scrapes,
// at the cost of slightly more CPU time cost and GC pressure.
//
// A microprom handler produces output in the [Prometheus exposition format], matching the output of promhttp exactly;
// thus it can be scraped by Prometheus or any other OpenTelemetry-compatible metrics collector.
// However, because of the highly specialized focus on high-cardinality database metrics,
// significant parts of the OTLP Stream Model (e.g. summaries, histograms, exemplars) are not implemented.
// The only supported metric types are gauges, counters and info metrics.
//
// # How to use
//
// To get started with microprom, declare your metric families in a [Metadata] instance,
// and then call [Metadata.Handler] to obtain a handler for your "GET /metrics" endpoint.
//
// [promhttp]: https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/promhttp
// [prometheus/client_golang]: https://pkg.go.dev/github.com/prometheus/client_golang
// [Prometheus exposition format]: https://prometheus.io/docs/instrumenting/exposition_formats/
package microprom

import (
	"fmt"
	"regexp"
)

// MetricFamilyInfo appears in type [HandlerInfo].
type MetricFamilyInfo struct {
	Type MetricType
	Help string
}

var (
	labelNameRx        = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
	metricFamilyNameRx = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
)

func (i MetricFamilyInfo) validate(name MetricFamilyName) error {
	if !metricFamilyNameRx.MatchString(string(name)) {
		return fmt.Errorf("in family %q: invalid family name (does not match /%s/)", name, metricFamilyNameRx.String())
	}
	if i.Type >= MetricType(len(metricTypeSuffixes)) {
		return fmt.Errorf("in family %q: invalid value for microprom.MetricType: %d", name, i.Type)
	}
	return nil
}

// MetricFamilyName is the name of a metric family.
//
// Per the [OpenMetrics 1.0] spec, metric family names must match the following regular expression:
//
//	^[a-zA-Z_:][a-zA-Z0-9_:]*$
//
// Package microprom does not implement escaping at the moment;
// metric family names not matching this pattern are invalid and will cause a panic.
// This restriction may be lifted in a future version.
//
// [OpenMetrics 1.0]: https://prometheus.io/docs/specs/om/open_metrics_spec/
type MetricFamilyName string

// MetricType is a enum. It appears in type [MetricFamilyInfo].
//
// As documented on the individual values below,
// the choice of metric type determines how [MetricSet.Add] derives the metric name.
type MetricType uint

const (
	// MetricTypeGauge is used for metrics that are current measurements,
	// where the absolute value is of interest to a user.
	//
	// For this metric type, the metric name is the same as the metric family name.
	MetricTypeGauge MetricType = iota

	// MetricTypeCounter is used for counting discrete events,
	// where the rate of increase over time is of interest to a user.
	//
	// For this metric type, the metric name is formed by appending "_total" to the metric family name.
	MetricTypeCounter

	// MetricTypeInfo is used for info metrics,
	// which only expose textual information in their labels.
	//
	// For this metric type, the metric name is formed by appending "_info" to the metric family name.
	MetricTypeInfo
)

var (
	metricTypeNames    = []string{"gauge", "counter", "info"}
	metricTypeSuffixes = []string{"", "_total", "_info"}
)

// MetricSet holds a set of metrics.
type MetricSet struct {
	syntax  Syntax
	metrics map[MetricFamilyName][]metric
}

type metric struct {
	labels Labels
	value  float64
}

// NewMetricSet constructs an initially empty [MetricSet] that accepts metrics for the given metric families.
func NewMetricSet(syntax Syntax, families map[MetricFamilyName]MetricFamilyInfo) *MetricSet {
	if syntax > SyntaxOpenMetricsV1 {
		panic(fmt.Sprintf("unknown value for Syntax: %d", syntax))
	}
	m := make(map[MetricFamilyName][]metric, len(families))
	for name, family := range families {
		err := family.validate(name)
		if err != nil {
			// this is fine to panic because it will only blow up in case of gross API misuse
			panic(err.Error())
		}
		m[name] = nil
	}
	return &MetricSet{syntax, m}
}

// Add adds a metric to the MetricSet.
//
// The name must be of a metric family that was declared during [NewMetricSet], otherwise Add will panic.
// The metric name will be derived according to the rules documented on the respective [MetricType].
func (ms *MetricSet) Add(name MetricFamilyName, labels Labels, value float64) {
	_, ok := ms.metrics[name]
	if !ok {
		panic("no such family: " + string(name))
	}
	ms.metrics[name] = append(ms.metrics[name], metric{labels, value})
}

// Syntax is an enum, defining which exposition format will be used by [MetricSet].
//
//   - SyntaxPrometheusLegacy corresponds to the [Prometheus Text Format].
//   - SyntaxOpenMetricsV1 corresponds to the [OpenMetrics 1.0] text format
//   - Additional formats may be added in the future (e.g. OpenMetrics 2.0, once it is stabilized).
//
// [Prometheus Text Format]: https://prometheus.io/docs/instrumenting/exposition_formats/
// [OpenMetrics 1.0]: https://prometheus.io/docs/specs/om/open_metrics_spec/
type Syntax uint

const (
	// SyntaxPrometheusLegacy corresponds to the Prometheus text format (currently version 0.0.4).
	SyntaxPrometheusLegacy Syntax = iota
	// SyntaxOpenMetricsV1 corresponds to the OpenMetrics 1.0 text format.
	SyntaxOpenMetricsV1
)