Browse code

vendor: github.com/prometheus/client_golang v1.22.0

full diff: https://github.com/prometheus/client_golang/compare/v1.20.5...v1.22.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/06/12 20:11:25
Showing 38 changed files
... ...
@@ -88,7 +88,7 @@ require (
88 88
 	github.com/opencontainers/selinux v1.12.0
89 89
 	github.com/pelletier/go-toml v1.9.5
90 90
 	github.com/pkg/errors v0.9.1
91
-	github.com/prometheus/client_golang v1.20.5
91
+	github.com/prometheus/client_golang v1.22.0
92 92
 	github.com/rootless-containers/rootlesskit/v2 v2.3.4
93 93
 	github.com/sirupsen/logrus v1.9.3
94 94
 	github.com/spf13/cobra v1.9.1
... ...
@@ -198,7 +198,7 @@ require (
198 198
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
199 199
 	github.com/pmezard/go-difflib v1.0.0 // indirect
200 200
 	github.com/prometheus/client_model v0.6.1 // indirect
201
-	github.com/prometheus/common v0.55.0 // indirect
201
+	github.com/prometheus/common v0.62.0 // indirect
202 202
 	github.com/prometheus/procfs v0.15.1 // indirect
203 203
 	github.com/sasha-s/go-deadlock v0.3.5 // indirect
204 204
 	github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
... ...
@@ -485,8 +485,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
485 485
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
486 486
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
487 487
 github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
488
-github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
489
-github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
488
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
489
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
490 490
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
491 491
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
492 492
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
... ...
@@ -496,8 +496,8 @@ github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQy
496 496
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
497 497
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
498 498
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
499
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
500
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
499
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
500
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
501 501
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
502 502
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
503 503
 github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
504 504
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+// Copyright 2025 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+package prometheus
14
+
15
+// CollectorFunc is a convenient way to implement a Prometheus Collector
16
+// without interface boilerplate.
17
+// This implementation is based on DescribeByCollect method.
18
+// familiarize yourself to it before using.
19
+type CollectorFunc func(chan<- Metric)
20
+
21
+// Collect calls the defined CollectorFunc function with the provided Metrics channel
22
+func (f CollectorFunc) Collect(ch chan<- Metric) {
23
+	f(ch)
24
+}
25
+
26
+// Describe sends the descriptor information using DescribeByCollect
27
+func (f CollectorFunc) Describe(ch chan<- *Desc) {
28
+	DescribeByCollect(f, ch)
29
+}
... ...
@@ -189,12 +189,15 @@ func (d *Desc) String() string {
189 189
 			fmt.Sprintf("%s=%q", lp.GetName(), lp.GetValue()),
190 190
 		)
191 191
 	}
192
-	vlStrings := make([]string, 0, len(d.variableLabels.names))
193
-	for _, vl := range d.variableLabels.names {
194
-		if fn, ok := d.variableLabels.labelConstraints[vl]; ok && fn != nil {
195
-			vlStrings = append(vlStrings, fmt.Sprintf("c(%s)", vl))
196
-		} else {
197
-			vlStrings = append(vlStrings, vl)
192
+	vlStrings := []string{}
193
+	if d.variableLabels != nil {
194
+		vlStrings = make([]string, 0, len(d.variableLabels.names))
195
+		for _, vl := range d.variableLabels.names {
196
+			if fn, ok := d.variableLabels.labelConstraints[vl]; ok && fn != nil {
197
+				vlStrings = append(vlStrings, fmt.Sprintf("c(%s)", vl))
198
+			} else {
199
+				vlStrings = append(vlStrings, vl)
200
+			}
198 201
 		}
199 202
 	}
200 203
 	return fmt.Sprintf(
... ...
@@ -288,7 +288,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
288 288
 }
289 289
 
290 290
 func attachOriginalName(desc, origName string) string {
291
-	return fmt.Sprintf("%s Sourced from %s", desc, origName)
291
+	return fmt.Sprintf("%s Sourced from %s.", desc, origName)
292 292
 }
293 293
 
294 294
 // Describe returns all descriptions of the collector.
... ...
@@ -14,6 +14,7 @@
14 14
 package prometheus
15 15
 
16 16
 import (
17
+	"errors"
17 18
 	"fmt"
18 19
 	"math"
19 20
 	"runtime"
... ...
@@ -28,6 +29,11 @@ import (
28 28
 	"google.golang.org/protobuf/types/known/timestamppb"
29 29
 )
30 30
 
31
+const (
32
+	nativeHistogramSchemaMaximum = 8
33
+	nativeHistogramSchemaMinimum = -4
34
+)
35
+
31 36
 // nativeHistogramBounds for the frac of observed values. Only relevant for
32 37
 // schema > 0. The position in the slice is the schema. (0 is never used, just
33 38
 // here for convenience of using the schema directly as the index.)
... ...
@@ -330,11 +336,11 @@ func ExponentialBuckets(start, factor float64, count int) []float64 {
330 330
 // used for the Buckets field of HistogramOpts.
331 331
 //
332 332
 // The function panics if 'count' is 0 or negative, if 'min' is 0 or negative.
333
-func ExponentialBucketsRange(min, max float64, count int) []float64 {
333
+func ExponentialBucketsRange(minBucket, maxBucket float64, count int) []float64 {
334 334
 	if count < 1 {
335 335
 		panic("ExponentialBucketsRange count needs a positive count")
336 336
 	}
337
-	if min <= 0 {
337
+	if minBucket <= 0 {
338 338
 		panic("ExponentialBucketsRange min needs to be greater than 0")
339 339
 	}
340 340
 
... ...
@@ -342,12 +348,12 @@ func ExponentialBucketsRange(min, max float64, count int) []float64 {
342 342
 	// max = min*growthFactor^(bucketCount-1)
343 343
 
344 344
 	// We know max/min and highest bucket. Solve for growthFactor.
345
-	growthFactor := math.Pow(max/min, 1.0/float64(count-1))
345
+	growthFactor := math.Pow(maxBucket/minBucket, 1.0/float64(count-1))
346 346
 
347 347
 	// Now that we know growthFactor, solve for each bucket.
348 348
 	buckets := make([]float64, count)
349 349
 	for i := 1; i <= count; i++ {
350
-		buckets[i-1] = min * math.Pow(growthFactor, float64(i-1))
350
+		buckets[i-1] = minBucket * math.Pow(growthFactor, float64(i-1))
351 351
 	}
352 352
 	return buckets
353 353
 }
... ...
@@ -858,15 +864,35 @@ func (h *histogram) Write(out *dto.Metric) error {
858 858
 // findBucket returns the index of the bucket for the provided value, or
859 859
 // len(h.upperBounds) for the +Inf bucket.
860 860
 func (h *histogram) findBucket(v float64) int {
861
-	// TODO(beorn7): For small numbers of buckets (<30), a linear search is
862
-	// slightly faster than the binary search. If we really care, we could
863
-	// switch from one search strategy to the other depending on the number
864
-	// of buckets.
865
-	//
866
-	// Microbenchmarks (BenchmarkHistogramNoLabels):
867
-	// 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op
868
-	// 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op
869
-	// 300 buckets: 154 ns/op linear - binary 61.6 ns/op
861
+	n := len(h.upperBounds)
862
+	if n == 0 {
863
+		return 0
864
+	}
865
+
866
+	// Early exit: if v is less than or equal to the first upper bound, return 0
867
+	if v <= h.upperBounds[0] {
868
+		return 0
869
+	}
870
+
871
+	// Early exit: if v is greater than the last upper bound, return len(h.upperBounds)
872
+	if v > h.upperBounds[n-1] {
873
+		return n
874
+	}
875
+
876
+	// For small arrays, use simple linear search
877
+	// "magic number" 35 is result of tests on couple different (AWS and baremetal) servers
878
+	// see more details here: https://github.com/prometheus/client_golang/pull/1662
879
+	if n < 35 {
880
+		for i, bound := range h.upperBounds {
881
+			if v <= bound {
882
+				return i
883
+			}
884
+		}
885
+		// If v is greater than all upper bounds, return len(h.upperBounds)
886
+		return n
887
+	}
888
+
889
+	// For larger arrays, use stdlib's binary search
870 890
 	return sort.SearchFloat64s(h.upperBounds, v)
871 891
 }
872 892
 
... ...
@@ -1440,9 +1466,9 @@ func pickSchema(bucketFactor float64) int32 {
1440 1440
 	floor := math.Floor(math.Log2(math.Log2(bucketFactor)))
1441 1441
 	switch {
1442 1442
 	case floor <= -8:
1443
-		return 8
1443
+		return nativeHistogramSchemaMaximum
1444 1444
 	case floor >= 4:
1445
-		return -4
1445
+		return nativeHistogramSchemaMinimum
1446 1446
 	default:
1447 1447
 		return -int32(floor)
1448 1448
 	}
... ...
@@ -1835,3 +1861,196 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1835 1835
 		n.exemplars = append(n.exemplars[:nIdx], append([]*dto.Exemplar{e}, append(n.exemplars[nIdx:rIdx], n.exemplars[rIdx+1:]...)...)...)
1836 1836
 	}
1837 1837
 }
1838
+
1839
+type constNativeHistogram struct {
1840
+	desc *Desc
1841
+	dto.Histogram
1842
+	labelPairs []*dto.LabelPair
1843
+}
1844
+
1845
+func validateCount(sum float64, count uint64, negativeBuckets, positiveBuckets map[int]int64, zeroBucket uint64) error {
1846
+	var bucketPopulationSum int64
1847
+	for _, v := range positiveBuckets {
1848
+		bucketPopulationSum += v
1849
+	}
1850
+	for _, v := range negativeBuckets {
1851
+		bucketPopulationSum += v
1852
+	}
1853
+	bucketPopulationSum += int64(zeroBucket)
1854
+
1855
+	// If the sum of observations is NaN, the number of observations must be greater or equal to the sum of all bucket counts.
1856
+	// Otherwise, the number of observations must be equal to the sum of all bucket counts .
1857
+
1858
+	if math.IsNaN(sum) && bucketPopulationSum > int64(count) ||
1859
+		!math.IsNaN(sum) && bucketPopulationSum != int64(count) {
1860
+		return errors.New("the sum of all bucket populations exceeds the count of observations")
1861
+	}
1862
+	return nil
1863
+}
1864
+
1865
+// NewConstNativeHistogram returns a metric representing a Prometheus native histogram with
1866
+// fixed values for the count, sum, and positive/negative/zero bucket counts. As those parameters
1867
+// cannot be changed, the returned value does not implement the Histogram
1868
+// interface (but only the Metric interface). Users of this package will not
1869
+// have much use for it in regular operations. However, when implementing custom
1870
+// OpenTelemetry Collectors, it is useful as a throw-away metric that is generated on the fly
1871
+// to send it to Prometheus in the Collect method.
1872
+//
1873
+// zeroBucket counts all (positive and negative)
1874
+// observations in the zero bucket (with an absolute value less or equal
1875
+// the current threshold).
1876
+// positiveBuckets and negativeBuckets are separate maps for negative and positive
1877
+// observations. The map's value is an int64, counting observations in
1878
+// that bucket. The map's key is the
1879
+// index of the bucket according to the used
1880
+// Schema. Index 0 is for an upper bound of 1 in positive buckets and for a lower bound of -1 in negative buckets.
1881
+// NewConstNativeHistogram returns an error if
1882
+//   - the length of labelValues is not consistent with the variable labels in Desc or if Desc is invalid.
1883
+//   - the schema passed is not between 8 and -4
1884
+//   - the sum of counts in all buckets including the zero bucket does not equal the count if sum is not NaN (or exceeds the count if sum is NaN)
1885
+//
1886
+// See https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#exponential-histograms for more details about the conversion from OTel to Prometheus.
1887
+func NewConstNativeHistogram(
1888
+	desc *Desc,
1889
+	count uint64,
1890
+	sum float64,
1891
+	positiveBuckets, negativeBuckets map[int]int64,
1892
+	zeroBucket uint64,
1893
+	schema int32,
1894
+	zeroThreshold float64,
1895
+	createdTimestamp time.Time,
1896
+	labelValues ...string,
1897
+) (Metric, error) {
1898
+	if desc.err != nil {
1899
+		return nil, desc.err
1900
+	}
1901
+	if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
1902
+		return nil, err
1903
+	}
1904
+	if schema > nativeHistogramSchemaMaximum || schema < nativeHistogramSchemaMinimum {
1905
+		return nil, errors.New("invalid native histogram schema")
1906
+	}
1907
+	if err := validateCount(sum, count, negativeBuckets, positiveBuckets, zeroBucket); err != nil {
1908
+		return nil, err
1909
+	}
1910
+
1911
+	NegativeSpan, NegativeDelta := makeBucketsFromMap(negativeBuckets)
1912
+	PositiveSpan, PositiveDelta := makeBucketsFromMap(positiveBuckets)
1913
+	ret := &constNativeHistogram{
1914
+		desc: desc,
1915
+		Histogram: dto.Histogram{
1916
+			CreatedTimestamp: timestamppb.New(createdTimestamp),
1917
+			Schema:           &schema,
1918
+			ZeroThreshold:    &zeroThreshold,
1919
+			SampleCount:      &count,
1920
+			SampleSum:        &sum,
1921
+
1922
+			NegativeSpan:  NegativeSpan,
1923
+			NegativeDelta: NegativeDelta,
1924
+
1925
+			PositiveSpan:  PositiveSpan,
1926
+			PositiveDelta: PositiveDelta,
1927
+
1928
+			ZeroCount: proto.Uint64(zeroBucket),
1929
+		},
1930
+		labelPairs: MakeLabelPairs(desc, labelValues),
1931
+	}
1932
+	if *ret.ZeroThreshold == 0 && *ret.ZeroCount == 0 && len(ret.PositiveSpan) == 0 && len(ret.NegativeSpan) == 0 {
1933
+		ret.PositiveSpan = []*dto.BucketSpan{{
1934
+			Offset: proto.Int32(0),
1935
+			Length: proto.Uint32(0),
1936
+		}}
1937
+	}
1938
+	return ret, nil
1939
+}
1940
+
1941
+// MustNewConstNativeHistogram is a version of NewConstNativeHistogram that panics where
1942
+// NewConstNativeHistogram would have returned an error.
1943
+func MustNewConstNativeHistogram(
1944
+	desc *Desc,
1945
+	count uint64,
1946
+	sum float64,
1947
+	positiveBuckets, negativeBuckets map[int]int64,
1948
+	zeroBucket uint64,
1949
+	nativeHistogramSchema int32,
1950
+	nativeHistogramZeroThreshold float64,
1951
+	createdTimestamp time.Time,
1952
+	labelValues ...string,
1953
+) Metric {
1954
+	nativehistogram, err := NewConstNativeHistogram(desc,
1955
+		count,
1956
+		sum,
1957
+		positiveBuckets,
1958
+		negativeBuckets,
1959
+		zeroBucket,
1960
+		nativeHistogramSchema,
1961
+		nativeHistogramZeroThreshold,
1962
+		createdTimestamp,
1963
+		labelValues...)
1964
+	if err != nil {
1965
+		panic(err)
1966
+	}
1967
+	return nativehistogram
1968
+}
1969
+
1970
+func (h *constNativeHistogram) Desc() *Desc {
1971
+	return h.desc
1972
+}
1973
+
1974
+func (h *constNativeHistogram) Write(out *dto.Metric) error {
1975
+	out.Histogram = &h.Histogram
1976
+	out.Label = h.labelPairs
1977
+	return nil
1978
+}
1979
+
1980
+func makeBucketsFromMap(buckets map[int]int64) ([]*dto.BucketSpan, []int64) {
1981
+	if len(buckets) == 0 {
1982
+		return nil, nil
1983
+	}
1984
+	var ii []int
1985
+	for k := range buckets {
1986
+		ii = append(ii, k)
1987
+	}
1988
+	sort.Ints(ii)
1989
+
1990
+	var (
1991
+		spans     []*dto.BucketSpan
1992
+		deltas    []int64
1993
+		prevCount int64
1994
+		nextI     int
1995
+	)
1996
+
1997
+	appendDelta := func(count int64) {
1998
+		*spans[len(spans)-1].Length++
1999
+		deltas = append(deltas, count-prevCount)
2000
+		prevCount = count
2001
+	}
2002
+
2003
+	for n, i := range ii {
2004
+		count := buckets[i]
2005
+		// Multiple spans with only small gaps in between are probably
2006
+		// encoded more efficiently as one larger span with a few empty
2007
+		// buckets. Needs some research to find the sweet spot. For now,
2008
+		// we assume that gaps of one or two buckets should not create
2009
+		// a new span.
2010
+		iDelta := int32(i - nextI)
2011
+		if n == 0 || iDelta > 2 {
2012
+			// We have to create a new span, either because we are
2013
+			// at the very beginning, or because we have found a gap
2014
+			// of more than two buckets.
2015
+			spans = append(spans, &dto.BucketSpan{
2016
+				Offset: proto.Int32(iDelta),
2017
+				Length: proto.Uint32(0),
2018
+			})
2019
+		} else {
2020
+			// We have found a small gap (or no gap at all).
2021
+			// Insert empty buckets as needed.
2022
+			for j := int32(0); j < iDelta; j++ {
2023
+				appendDelta(0)
2024
+			}
2025
+		}
2026
+		appendDelta(count)
2027
+		nextI = i + 1
2028
+	}
2029
+	return spans, deltas
2030
+}
... ...
@@ -22,17 +22,18 @@ import (
22 22
 	"bytes"
23 23
 	"fmt"
24 24
 	"io"
25
+	"strconv"
25 26
 	"strings"
26 27
 )
27 28
 
28
-func min(a, b int) int {
29
+func minInt(a, b int) int {
29 30
 	if a < b {
30 31
 		return a
31 32
 	}
32 33
 	return b
33 34
 }
34 35
 
35
-func max(a, b int) int {
36
+func maxInt(a, b int) int {
36 37
 	if a > b {
37 38
 		return a
38 39
 	}
... ...
@@ -427,12 +428,12 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
427 427
 	if codes[0].Tag == 'e' {
428 428
 		c := codes[0]
429 429
 		i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
430
-		codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
430
+		codes[0] = OpCode{c.Tag, maxInt(i1, i2-n), i2, maxInt(j1, j2-n), j2}
431 431
 	}
432 432
 	if codes[len(codes)-1].Tag == 'e' {
433 433
 		c := codes[len(codes)-1]
434 434
 		i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
435
-		codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
435
+		codes[len(codes)-1] = OpCode{c.Tag, i1, minInt(i2, i1+n), j1, minInt(j2, j1+n)}
436 436
 	}
437 437
 	nn := n + n
438 438
 	groups := [][]OpCode{}
... ...
@@ -443,12 +444,12 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
443 443
 		// there is a large range with no changes.
444 444
 		if c.Tag == 'e' && i2-i1 > nn {
445 445
 			group = append(group, OpCode{
446
-				c.Tag, i1, min(i2, i1+n),
447
-				j1, min(j2, j1+n),
446
+				c.Tag, i1, minInt(i2, i1+n),
447
+				j1, minInt(j2, j1+n),
448 448
 			})
449 449
 			groups = append(groups, group)
450 450
 			group = []OpCode{}
451
-			i1, j1 = max(i1, i2-n), max(j1, j2-n)
451
+			i1, j1 = maxInt(i1, i2-n), maxInt(j1, j2-n)
452 452
 		}
453 453
 		group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
454 454
 	}
... ...
@@ -515,7 +516,7 @@ func (m *SequenceMatcher) QuickRatio() float64 {
515 515
 // is faster to compute than either .Ratio() or .QuickRatio().
516 516
 func (m *SequenceMatcher) RealQuickRatio() float64 {
517 517
 	la, lb := len(m.a), len(m.b)
518
-	return calculateRatio(min(la, lb), la+lb)
518
+	return calculateRatio(minInt(la, lb), la+lb)
519 519
 }
520 520
 
521 521
 // Convert range to the "ed" format
... ...
@@ -524,7 +525,7 @@ func formatRangeUnified(start, stop int) string {
524 524
 	beginning := start + 1 // lines start numbering with one
525 525
 	length := stop - start
526 526
 	if length == 1 {
527
-		return fmt.Sprintf("%d", beginning)
527
+		return strconv.Itoa(beginning)
528 528
 	}
529 529
 	if length == 0 {
530 530
 		beginning-- // empty ranges begin at line just before the range
... ...
@@ -66,7 +66,8 @@ func RuntimeMetricsToProm(d *metrics.Description) (string, string, string, bool)
66 66
 		name += "_total"
67 67
 	}
68 68
 
69
-	valid := model.IsValidMetricName(model.LabelValue(namespace + "_" + subsystem + "_" + name))
69
+	// Our current conversion moves to legacy naming, so use legacy validation.
70
+	valid := model.IsValidLegacyMetricName(namespace + "_" + subsystem + "_" + name)
70 71
 	switch d.Kind {
71 72
 	case metrics.KindUint64:
72 73
 	case metrics.KindFloat64:
... ...
@@ -108,15 +108,23 @@ func BuildFQName(namespace, subsystem, name string) string {
108 108
 	if name == "" {
109 109
 		return ""
110 110
 	}
111
-	switch {
112
-	case namespace != "" && subsystem != "":
113
-		return strings.Join([]string{namespace, subsystem, name}, "_")
114
-	case namespace != "":
115
-		return strings.Join([]string{namespace, name}, "_")
116
-	case subsystem != "":
117
-		return strings.Join([]string{subsystem, name}, "_")
111
+
112
+	sb := strings.Builder{}
113
+	sb.Grow(len(namespace) + len(subsystem) + len(name) + 2)
114
+
115
+	if namespace != "" {
116
+		sb.WriteString(namespace)
117
+		sb.WriteString("_")
118 118
 	}
119
-	return name
119
+
120
+	if subsystem != "" {
121
+		sb.WriteString(subsystem)
122
+		sb.WriteString("_")
123
+	}
124
+
125
+	sb.WriteString(name)
126
+
127
+	return sb.String()
120 128
 }
121 129
 
122 130
 type invalidMetric struct {
... ...
@@ -23,6 +23,7 @@ import (
23 23
 
24 24
 type processCollector struct {
25 25
 	collectFn         func(chan<- Metric)
26
+	describeFn        func(chan<- *Desc)
26 27
 	pidFn             func() (int, error)
27 28
 	reportErrors      bool
28 29
 	cpuTotal          *Desc
... ...
@@ -122,26 +123,23 @@ func NewProcessCollector(opts ProcessCollectorOpts) Collector {
122 122
 	// Set up process metric collection if supported by the runtime.
123 123
 	if canCollectProcess() {
124 124
 		c.collectFn = c.processCollect
125
+		c.describeFn = c.describe
125 126
 	} else {
126
-		c.collectFn = func(ch chan<- Metric) {
127
-			c.reportError(ch, nil, errors.New("process metrics not supported on this platform"))
128
-		}
127
+		c.collectFn = c.errorCollectFn
128
+		c.describeFn = c.errorDescribeFn
129 129
 	}
130 130
 
131 131
 	return c
132 132
 }
133 133
 
134
-// Describe returns all descriptions of the collector.
135
-func (c *processCollector) Describe(ch chan<- *Desc) {
136
-	ch <- c.cpuTotal
137
-	ch <- c.openFDs
138
-	ch <- c.maxFDs
139
-	ch <- c.vsize
140
-	ch <- c.maxVsize
141
-	ch <- c.rss
142
-	ch <- c.startTime
143
-	ch <- c.inBytes
144
-	ch <- c.outBytes
134
+func (c *processCollector) errorCollectFn(ch chan<- Metric) {
135
+	c.reportError(ch, nil, errors.New("process metrics not supported on this platform"))
136
+}
137
+
138
+func (c *processCollector) errorDescribeFn(ch chan<- *Desc) {
139
+	if c.reportErrors {
140
+		ch <- NewInvalidDesc(errors.New("process metrics not supported on this platform"))
141
+	}
145 142
 }
146 143
 
147 144
 // Collect returns the current state of all metrics of the collector.
... ...
@@ -149,6 +147,11 @@ func (c *processCollector) Collect(ch chan<- Metric) {
149 149
 	c.collectFn(ch)
150 150
 }
151 151
 
152
+// Describe returns all descriptions of the collector.
153
+func (c *processCollector) Describe(ch chan<- *Desc) {
154
+	c.describeFn(ch)
155
+}
156
+
152 157
 func (c *processCollector) reportError(ch chan<- Metric, desc *Desc, err error) {
153 158
 	if !c.reportErrors {
154 159
 		return
155 160
new file mode 100644
... ...
@@ -0,0 +1,130 @@
0
+// Copyright 2024 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build darwin && !ios
14
+
15
+package prometheus
16
+
17
+import (
18
+	"errors"
19
+	"fmt"
20
+	"os"
21
+	"syscall"
22
+	"time"
23
+
24
+	"golang.org/x/sys/unix"
25
+)
26
+
27
+// notImplementedErr is returned by stub functions that replace cgo functions, when cgo
28
+// isn't available.
29
+var notImplementedErr = errors.New("not implemented")
30
+
31
+type memoryInfo struct {
32
+	vsize uint64 // Virtual memory size in bytes
33
+	rss   uint64 // Resident memory size in bytes
34
+}
35
+
36
+func canCollectProcess() bool {
37
+	return true
38
+}
39
+
40
+func getSoftLimit(which int) (uint64, error) {
41
+	rlimit := syscall.Rlimit{}
42
+
43
+	if err := syscall.Getrlimit(which, &rlimit); err != nil {
44
+		return 0, err
45
+	}
46
+
47
+	return rlimit.Cur, nil
48
+}
49
+
50
+func getOpenFileCount() (float64, error) {
51
+	// Alternately, the undocumented proc_pidinfo(PROC_PIDLISTFDS) can be used to
52
+	// return a list of open fds, but that requires a way to call C APIs.  The
53
+	// benefits, however, include fewer system calls and not failing when at the
54
+	// open file soft limit.
55
+
56
+	if dir, err := os.Open("/dev/fd"); err != nil {
57
+		return 0.0, err
58
+	} else {
59
+		defer dir.Close()
60
+
61
+		// Avoid ReadDir(), as it calls stat(2) on each descriptor.  Not only is
62
+		// that info not used, but KQUEUE descriptors fail stat(2), which causes
63
+		// the whole method to fail.
64
+		if names, err := dir.Readdirnames(0); err != nil {
65
+			return 0.0, err
66
+		} else {
67
+			// Subtract 1 to ignore the open /dev/fd descriptor above.
68
+			return float64(len(names) - 1), nil
69
+		}
70
+	}
71
+}
72
+
73
+func (c *processCollector) processCollect(ch chan<- Metric) {
74
+	if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", os.Getpid()); err == nil {
75
+		if len(procs) == 1 {
76
+			startTime := float64(procs[0].Proc.P_starttime.Nano() / 1e9)
77
+			ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
78
+		} else {
79
+			err = fmt.Errorf("sysctl() returned %d proc structs (expected 1)", len(procs))
80
+			c.reportError(ch, c.startTime, err)
81
+		}
82
+	} else {
83
+		c.reportError(ch, c.startTime, err)
84
+	}
85
+
86
+	// The proc structure returned by kern.proc.pid above has an Rusage member,
87
+	// but it is not filled in, so it needs to be fetched by getrusage(2).  For
88
+	// that call, the UTime, STime, and Maxrss members are filled out, but not
89
+	// Ixrss, Idrss, or Isrss for the memory usage.  Memory stats will require
90
+	// access to the C API to call task_info(TASK_BASIC_INFO).
91
+	rusage := unix.Rusage{}
92
+
93
+	if err := unix.Getrusage(syscall.RUSAGE_SELF, &rusage); err == nil {
94
+		cpuTime := time.Duration(rusage.Stime.Nano() + rusage.Utime.Nano()).Seconds()
95
+		ch <- MustNewConstMetric(c.cpuTotal, CounterValue, cpuTime)
96
+	} else {
97
+		c.reportError(ch, c.cpuTotal, err)
98
+	}
99
+
100
+	if memInfo, err := getMemory(); err == nil {
101
+		ch <- MustNewConstMetric(c.rss, GaugeValue, float64(memInfo.rss))
102
+		ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(memInfo.vsize))
103
+	} else if !errors.Is(err, notImplementedErr) {
104
+		// Don't report an error when support is not compiled in.
105
+		c.reportError(ch, c.rss, err)
106
+		c.reportError(ch, c.vsize, err)
107
+	}
108
+
109
+	if fds, err := getOpenFileCount(); err == nil {
110
+		ch <- MustNewConstMetric(c.openFDs, GaugeValue, fds)
111
+	} else {
112
+		c.reportError(ch, c.openFDs, err)
113
+	}
114
+
115
+	if openFiles, err := getSoftLimit(syscall.RLIMIT_NOFILE); err == nil {
116
+		ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(openFiles))
117
+	} else {
118
+		c.reportError(ch, c.maxFDs, err)
119
+	}
120
+
121
+	if addressSpace, err := getSoftLimit(syscall.RLIMIT_AS); err == nil {
122
+		ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(addressSpace))
123
+	} else {
124
+		c.reportError(ch, c.maxVsize, err)
125
+	}
126
+
127
+	// TODO: socket(PF_SYSTEM) to fetch "com.apple.network.statistics" might
128
+	//  be able to get the per-process network send/receive counts.
129
+}
0 130
deleted file mode 100644
... ...
@@ -1,26 +0,0 @@
1
-// Copyright 2019 The Prometheus Authors
2
-// Licensed under the Apache License, Version 2.0 (the "License");
3
-// you may not use this file except in compliance with the License.
4
-// You may obtain a copy of the License at
5
-//
6
-// http://www.apache.org/licenses/LICENSE-2.0
7
-//
8
-// Unless required by applicable law or agreed to in writing, software
9
-// distributed under the License is distributed on an "AS IS" BASIS,
10
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
-// See the License for the specific language governing permissions and
12
-// limitations under the License.
13
-
14
-//go:build js
15
-// +build js
16
-
17
-package prometheus
18
-
19
-func canCollectProcess() bool {
20
-	return false
21
-}
22
-
23
-func (c *processCollector) processCollect(ch chan<- Metric) {
24
-	// noop on this platform
25
-	return
26
-}
27 1
new file mode 100644
... ...
@@ -0,0 +1,84 @@
0
+// Copyright 2024 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build darwin && !ios && cgo
14
+
15
+#include <mach/mach_init.h>
16
+#include <mach/task.h>
17
+#include <mach/mach_vm.h>
18
+
19
+// The compiler warns that mach/shared_memory_server.h is deprecated, and to use
20
+// mach/shared_region.h instead.  But that doesn't define
21
+// SHARED_DATA_REGION_SIZE or SHARED_TEXT_REGION_SIZE, so redefine them here and
22
+// avoid a warning message when running tests.
23
+#define GLOBAL_SHARED_TEXT_SEGMENT      0x90000000U
24
+#define SHARED_DATA_REGION_SIZE         0x10000000
25
+#define SHARED_TEXT_REGION_SIZE         0x10000000
26
+
27
+
28
+int get_memory_info(unsigned long long *rss, unsigned long long *vsize)
29
+{
30
+    // This is lightly adapted from how ps(1) obtains its memory info.
31
+    // https://github.com/apple-oss-distributions/adv_cmds/blob/8744084ea0ff41ca4bb96b0f9c22407d0e48e9b7/ps/tasks.c#L109
32
+
33
+    kern_return_t               error;
34
+    task_t                      task = MACH_PORT_NULL;
35
+    mach_task_basic_info_data_t info;
36
+    mach_msg_type_number_t      info_count = MACH_TASK_BASIC_INFO_COUNT;
37
+
38
+    error = task_info(
39
+                mach_task_self(),
40
+                MACH_TASK_BASIC_INFO,
41
+                (task_info_t) &info,
42
+                &info_count );
43
+
44
+    if( error != KERN_SUCCESS )
45
+    {
46
+        return error;
47
+    }
48
+
49
+    *rss   = info.resident_size;
50
+    *vsize = info.virtual_size;
51
+
52
+    {
53
+        vm_region_basic_info_data_64_t    b_info;
54
+        mach_vm_address_t                 address = GLOBAL_SHARED_TEXT_SEGMENT;
55
+        mach_vm_size_t                    size;
56
+        mach_port_t                       object_name;
57
+
58
+        /*
59
+         * try to determine if this task has the split libraries
60
+         * mapped in... if so, adjust its virtual size down by
61
+         * the 2 segments that are used for split libraries
62
+         */
63
+        info_count = VM_REGION_BASIC_INFO_COUNT_64;
64
+
65
+        error = mach_vm_region(
66
+                    mach_task_self(),
67
+                    &address,
68
+                    &size,
69
+                    VM_REGION_BASIC_INFO_64,
70
+                    (vm_region_info_t) &b_info,
71
+                    &info_count,
72
+                    &object_name);
73
+
74
+        if (error == KERN_SUCCESS) {
75
+            if (b_info.reserved && size == (SHARED_TEXT_REGION_SIZE) &&
76
+                *vsize > (SHARED_TEXT_REGION_SIZE + SHARED_DATA_REGION_SIZE)) {
77
+                    *vsize -= (SHARED_TEXT_REGION_SIZE + SHARED_DATA_REGION_SIZE);
78
+            }
79
+        }
80
+    }
81
+
82
+    return 0;
83
+}
0 84
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+// Copyright 2024 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build darwin && !ios && cgo
14
+
15
+package prometheus
16
+
17
+/*
18
+int get_memory_info(unsigned long long *rss, unsigned long long *vs);
19
+*/
20
+import "C"
21
+import "fmt"
22
+
23
+func getMemory() (*memoryInfo, error) {
24
+	var rss, vsize C.ulonglong
25
+
26
+	if err := C.get_memory_info(&rss, &vsize); err != 0 {
27
+		return nil, fmt.Errorf("task_info() failed with 0x%x", int(err))
28
+	}
29
+
30
+	return &memoryInfo{vsize: uint64(vsize), rss: uint64(rss)}, nil
31
+}
32
+
33
+// describe returns all descriptions of the collector for Darwin.
34
+// Ensure that this list of descriptors is kept in sync with the metrics collected
35
+// in the processCollect method. Any changes to the metrics in processCollect
36
+// (such as adding or removing metrics) should be reflected in this list of descriptors.
37
+func (c *processCollector) describe(ch chan<- *Desc) {
38
+	ch <- c.cpuTotal
39
+	ch <- c.openFDs
40
+	ch <- c.maxFDs
41
+	ch <- c.maxVsize
42
+	ch <- c.startTime
43
+	ch <- c.rss
44
+	ch <- c.vsize
45
+
46
+	/* the process could be collected but not implemented yet
47
+	ch <- c.inBytes
48
+	ch <- c.outBytes
49
+	*/
50
+}
0 51
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+// Copyright 2024 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build darwin && !ios && !cgo
14
+
15
+package prometheus
16
+
17
+func getMemory() (*memoryInfo, error) {
18
+	return nil, notImplementedErr
19
+}
20
+
21
+// describe returns all descriptions of the collector for Darwin.
22
+// Ensure that this list of descriptors is kept in sync with the metrics collected
23
+// in the processCollect method. Any changes to the metrics in processCollect
24
+// (such as adding or removing metrics) should be reflected in this list of descriptors.
25
+func (c *processCollector) describe(ch chan<- *Desc) {
26
+	ch <- c.cpuTotal
27
+	ch <- c.openFDs
28
+	ch <- c.maxFDs
29
+	ch <- c.maxVsize
30
+	ch <- c.startTime
31
+
32
+	/* the process could be collected but not implemented yet
33
+	ch <- c.rss
34
+	ch <- c.vsize
35
+	ch <- c.inBytes
36
+	ch <- c.outBytes
37
+	*/
38
+}
0 39
new file mode 100644
... ...
@@ -0,0 +1,33 @@
0
+// Copyright 2023 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build wasip1 || js || ios
14
+// +build wasip1 js ios
15
+
16
+package prometheus
17
+
18
+func canCollectProcess() bool {
19
+	return false
20
+}
21
+
22
+func (c *processCollector) processCollect(ch chan<- Metric) {
23
+	c.errorCollectFn(ch)
24
+}
25
+
26
+// describe returns all descriptions of the collector for wasip1 and js.
27
+// Ensure that this list of descriptors is kept in sync with the metrics collected
28
+// in the processCollect method. Any changes to the metrics in processCollect
29
+// (such as adding or removing metrics) should be reflected in this list of descriptors.
30
+func (c *processCollector) describe(ch chan<- *Desc) {
31
+	c.errorDescribeFn(ch)
32
+}
0 33
deleted file mode 100644
... ...
@@ -1,80 +0,0 @@
1
-// Copyright 2019 The Prometheus Authors
2
-// Licensed under the Apache License, Version 2.0 (the "License");
3
-// you may not use this file except in compliance with the License.
4
-// You may obtain a copy of the License at
5
-//
6
-// http://www.apache.org/licenses/LICENSE-2.0
7
-//
8
-// Unless required by applicable law or agreed to in writing, software
9
-// distributed under the License is distributed on an "AS IS" BASIS,
10
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
-// See the License for the specific language governing permissions and
12
-// limitations under the License.
13
-
14
-//go:build !windows && !js && !wasip1
15
-// +build !windows,!js,!wasip1
16
-
17
-package prometheus
18
-
19
-import (
20
-	"github.com/prometheus/procfs"
21
-)
22
-
23
-func canCollectProcess() bool {
24
-	_, err := procfs.NewDefaultFS()
25
-	return err == nil
26
-}
27
-
28
-func (c *processCollector) processCollect(ch chan<- Metric) {
29
-	pid, err := c.pidFn()
30
-	if err != nil {
31
-		c.reportError(ch, nil, err)
32
-		return
33
-	}
34
-
35
-	p, err := procfs.NewProc(pid)
36
-	if err != nil {
37
-		c.reportError(ch, nil, err)
38
-		return
39
-	}
40
-
41
-	if stat, err := p.Stat(); err == nil {
42
-		ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
43
-		ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
44
-		ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
45
-		if startTime, err := stat.StartTime(); err == nil {
46
-			ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
47
-		} else {
48
-			c.reportError(ch, c.startTime, err)
49
-		}
50
-	} else {
51
-		c.reportError(ch, nil, err)
52
-	}
53
-
54
-	if fds, err := p.FileDescriptorsLen(); err == nil {
55
-		ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
56
-	} else {
57
-		c.reportError(ch, c.openFDs, err)
58
-	}
59
-
60
-	if limits, err := p.Limits(); err == nil {
61
-		ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
62
-		ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
63
-	} else {
64
-		c.reportError(ch, nil, err)
65
-	}
66
-
67
-	if netstat, err := p.Netstat(); err == nil {
68
-		var inOctets, outOctets float64
69
-		if netstat.IpExt.InOctets != nil {
70
-			inOctets = *netstat.IpExt.InOctets
71
-		}
72
-		if netstat.IpExt.OutOctets != nil {
73
-			outOctets = *netstat.IpExt.OutOctets
74
-		}
75
-		ch <- MustNewConstMetric(c.inBytes, CounterValue, inOctets)
76
-		ch <- MustNewConstMetric(c.outBytes, CounterValue, outOctets)
77
-	} else {
78
-		c.reportError(ch, nil, err)
79
-	}
80
-}
81 1
new file mode 100644
... ...
@@ -0,0 +1,96 @@
0
+// Copyright 2019 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+//go:build !windows && !js && !wasip1 && !darwin
14
+// +build !windows,!js,!wasip1,!darwin
15
+
16
+package prometheus
17
+
18
+import (
19
+	"github.com/prometheus/procfs"
20
+)
21
+
22
+func canCollectProcess() bool {
23
+	_, err := procfs.NewDefaultFS()
24
+	return err == nil
25
+}
26
+
27
+func (c *processCollector) processCollect(ch chan<- Metric) {
28
+	pid, err := c.pidFn()
29
+	if err != nil {
30
+		c.reportError(ch, nil, err)
31
+		return
32
+	}
33
+
34
+	p, err := procfs.NewProc(pid)
35
+	if err != nil {
36
+		c.reportError(ch, nil, err)
37
+		return
38
+	}
39
+
40
+	if stat, err := p.Stat(); err == nil {
41
+		ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
42
+		ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
43
+		ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
44
+		if startTime, err := stat.StartTime(); err == nil {
45
+			ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
46
+		} else {
47
+			c.reportError(ch, c.startTime, err)
48
+		}
49
+	} else {
50
+		c.reportError(ch, nil, err)
51
+	}
52
+
53
+	if fds, err := p.FileDescriptorsLen(); err == nil {
54
+		ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
55
+	} else {
56
+		c.reportError(ch, c.openFDs, err)
57
+	}
58
+
59
+	if limits, err := p.Limits(); err == nil {
60
+		ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
61
+		ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
62
+	} else {
63
+		c.reportError(ch, nil, err)
64
+	}
65
+
66
+	if netstat, err := p.Netstat(); err == nil {
67
+		var inOctets, outOctets float64
68
+		if netstat.IpExt.InOctets != nil {
69
+			inOctets = *netstat.IpExt.InOctets
70
+		}
71
+		if netstat.IpExt.OutOctets != nil {
72
+			outOctets = *netstat.IpExt.OutOctets
73
+		}
74
+		ch <- MustNewConstMetric(c.inBytes, CounterValue, inOctets)
75
+		ch <- MustNewConstMetric(c.outBytes, CounterValue, outOctets)
76
+	} else {
77
+		c.reportError(ch, nil, err)
78
+	}
79
+}
80
+
81
+// describe returns all descriptions of the collector for others than windows, js, wasip1 and darwin.
82
+// Ensure that this list of descriptors is kept in sync with the metrics collected
83
+// in the processCollect method. Any changes to the metrics in processCollect
84
+// (such as adding or removing metrics) should be reflected in this list of descriptors.
85
+func (c *processCollector) describe(ch chan<- *Desc) {
86
+	ch <- c.cpuTotal
87
+	ch <- c.openFDs
88
+	ch <- c.maxFDs
89
+	ch <- c.vsize
90
+	ch <- c.maxVsize
91
+	ch <- c.rss
92
+	ch <- c.startTime
93
+	ch <- c.inBytes
94
+	ch <- c.outBytes
95
+}
0 96
deleted file mode 100644
... ...
@@ -1,26 +0,0 @@
1
-// Copyright 2023 The Prometheus Authors
2
-// Licensed under the Apache License, Version 2.0 (the "License");
3
-// you may not use this file except in compliance with the License.
4
-// You may obtain a copy of the License at
5
-//
6
-// http://www.apache.org/licenses/LICENSE-2.0
7
-//
8
-// Unless required by applicable law or agreed to in writing, software
9
-// distributed under the License is distributed on an "AS IS" BASIS,
10
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
-// See the License for the specific language governing permissions and
12
-// limitations under the License.
13
-
14
-//go:build wasip1
15
-// +build wasip1
16
-
17
-package prometheus
18
-
19
-func canCollectProcess() bool {
20
-	return false
21
-}
22
-
23
-func (*processCollector) processCollect(chan<- Metric) {
24
-	// noop on this platform
25
-	return
26
-}
... ...
@@ -79,14 +79,10 @@ func getProcessHandleCount(handle windows.Handle) (uint32, error) {
79 79
 }
80 80
 
81 81
 func (c *processCollector) processCollect(ch chan<- Metric) {
82
-	h, err := windows.GetCurrentProcess()
83
-	if err != nil {
84
-		c.reportError(ch, nil, err)
85
-		return
86
-	}
82
+	h := windows.CurrentProcess()
87 83
 
88 84
 	var startTime, exitTime, kernelTime, userTime windows.Filetime
89
-	err = windows.GetProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime)
85
+	err := windows.GetProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime)
90 86
 	if err != nil {
91 87
 		c.reportError(ch, nil, err)
92 88
 		return
... ...
@@ -111,6 +107,19 @@ func (c *processCollector) processCollect(ch chan<- Metric) {
111 111
 	ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(16*1024*1024)) // Windows has a hard-coded max limit, not per-process.
112 112
 }
113 113
 
114
+// describe returns all descriptions of the collector for windows.
115
+// Ensure that this list of descriptors is kept in sync with the metrics collected
116
+// in the processCollect method. Any changes to the metrics in processCollect
117
+// (such as adding or removing metrics) should be reflected in this list of descriptors.
118
+func (c *processCollector) describe(ch chan<- *Desc) {
119
+	ch <- c.cpuTotal
120
+	ch <- c.openFDs
121
+	ch <- c.maxFDs
122
+	ch <- c.vsize
123
+	ch <- c.rss
124
+	ch <- c.startTime
125
+}
126
+
114 127
 func fileTimeToSeconds(ft windows.Filetime) float64 {
115 128
 	return float64(uint64(ft.HighDateTime)<<32+uint64(ft.LowDateTime)) / 1e7
116 129
 }
... ...
@@ -41,11 +41,11 @@ import (
41 41
 	"sync"
42 42
 	"time"
43 43
 
44
-	"github.com/klauspost/compress/zstd"
45 44
 	"github.com/prometheus/common/expfmt"
46 45
 
47 46
 	"github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil"
48 47
 	"github.com/prometheus/client_golang/prometheus"
48
+	"github.com/prometheus/client_golang/prometheus/promhttp/internal"
49 49
 )
50 50
 
51 51
 const (
... ...
@@ -65,7 +65,13 @@ const (
65 65
 	Zstd     Compression = "zstd"
66 66
 )
67 67
 
68
-var defaultCompressionFormats = []Compression{Identity, Gzip, Zstd}
68
+func defaultCompressionFormats() []Compression {
69
+	if internal.NewZstdWriter != nil {
70
+		return []Compression{Identity, Gzip, Zstd}
71
+	} else {
72
+		return []Compression{Identity, Gzip}
73
+	}
74
+}
69 75
 
70 76
 var gzipPool = sync.Pool{
71 77
 	New: func() interface{} {
... ...
@@ -138,7 +144,7 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
138 138
 	// Select compression formats to offer based on default or user choice.
139 139
 	var compressions []string
140 140
 	if !opts.DisableCompression {
141
-		offers := defaultCompressionFormats
141
+		offers := defaultCompressionFormats()
142 142
 		if len(opts.OfferedCompressions) > 0 {
143 143
 			offers = opts.OfferedCompressions
144 144
 		}
... ...
@@ -207,7 +213,13 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
207 207
 		if encodingHeader != string(Identity) {
208 208
 			rsp.Header().Set(contentEncodingHeader, encodingHeader)
209 209
 		}
210
-		enc := expfmt.NewEncoder(w, contentType)
210
+
211
+		var enc expfmt.Encoder
212
+		if opts.EnableOpenMetricsTextCreatedSamples {
213
+			enc = expfmt.NewEncoder(w, contentType, expfmt.WithCreatedLines())
214
+		} else {
215
+			enc = expfmt.NewEncoder(w, contentType)
216
+		}
211 217
 
212 218
 		// handleError handles the error according to opts.ErrorHandling
213 219
 		// and returns true if we have to abort after the handling.
... ...
@@ -408,6 +420,21 @@ type HandlerOpts struct {
408 408
 	// (which changes the identity of the resulting series on the Prometheus
409 409
 	// server).
410 410
 	EnableOpenMetrics bool
411
+	// EnableOpenMetricsTextCreatedSamples specifies if this handler should add, extra, synthetic
412
+	// Created Timestamps for counters, histograms and summaries, which for the current
413
+	// version of OpenMetrics are defined as extra series with the same name and "_created"
414
+	// suffix. See also the OpenMetrics specification for more details
415
+	// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1
416
+	//
417
+	// Created timestamps are used to improve the accuracy of reset detection,
418
+	// but the way it's designed in OpenMetrics 1.0 it also dramatically increases cardinality
419
+	// if the scraper does not handle those metrics correctly (converting to created timestamp
420
+	// instead of leaving those series as-is). New OpenMetrics versions might improve
421
+	// this situation.
422
+	//
423
+	// Prometheus introduced the feature flag 'created-timestamp-zero-ingestion'
424
+	// in version 2.50.0 to handle this situation.
425
+	EnableOpenMetricsTextCreatedSamples bool
411 426
 	// ProcessStartTime allows setting process start timevalue that will be exposed
412 427
 	// with "Process-Start-Time-Unix" response header along with the metrics
413 428
 	// payload. This allow callers to have efficient transformations to cumulative
... ...
@@ -445,14 +472,12 @@ func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []strin
445 445
 
446 446
 	switch selected {
447 447
 	case "zstd":
448
-		// TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented.
449
-		z, err := zstd.NewWriter(rw, zstd.WithEncoderLevel(zstd.SpeedFastest))
450
-		if err != nil {
451
-			return nil, "", func() {}, err
448
+		if internal.NewZstdWriter == nil {
449
+			// The content encoding was not implemented yet.
450
+			return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats())
452 451
 		}
453
-
454
-		z.Reset(rw)
455
-		return z, selected, func() { _ = z.Close() }, nil
452
+		writer, closeWriter, err := internal.NewZstdWriter(rw)
453
+		return writer, selected, closeWriter, err
456 454
 	case "gzip":
457 455
 		gz := gzipPool.Get().(*gzip.Writer)
458 456
 		gz.Reset(rw)
... ...
@@ -462,6 +487,6 @@ func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []strin
462 462
 		return rw, selected, func() {}, nil
463 463
 	default:
464 464
 		// The content encoding was not implemented yet.
465
-		return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats)
465
+		return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats())
466 466
 	}
467 467
 }
468 468
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+// Copyright 2025 The Prometheus Authors
1
+// Licensed under the Apache License, Version 2.0 (the "License");
2
+// you may not use this file except in compliance with the License.
3
+// You may obtain a copy of the License at
4
+//
5
+// http://www.apache.org/licenses/LICENSE-2.0
6
+//
7
+// Unless required by applicable law or agreed to in writing, software
8
+// distributed under the License is distributed on an "AS IS" BASIS,
9
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+// See the License for the specific language governing permissions and
11
+// limitations under the License.
12
+
13
+package internal
14
+
15
+import (
16
+	"io"
17
+)
18
+
19
+// NewZstdWriter enables zstd write support if non-nil.
20
+var NewZstdWriter func(rw io.Writer) (_ io.Writer, closeWriter func(), _ error)
... ...
@@ -243,6 +243,7 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
243 243
 
244 244
 	s := &summary{
245 245
 		desc: desc,
246
+		now:  opts.now,
246 247
 
247 248
 		objectives:       opts.Objectives,
248 249
 		sortedObjectives: make([]float64, 0, len(opts.Objectives)),
... ...
@@ -280,6 +281,8 @@ type summary struct {
280 280
 
281 281
 	desc *Desc
282 282
 
283
+	now func() time.Time
284
+
283 285
 	objectives       map[float64]float64
284 286
 	sortedObjectives []float64
285 287
 
... ...
@@ -307,7 +310,7 @@ func (s *summary) Observe(v float64) {
307 307
 	s.bufMtx.Lock()
308 308
 	defer s.bufMtx.Unlock()
309 309
 
310
-	now := time.Now()
310
+	now := s.now()
311 311
 	if now.After(s.hotBufExpTime) {
312 312
 		s.asyncFlush(now)
313 313
 	}
... ...
@@ -326,7 +329,7 @@ func (s *summary) Write(out *dto.Metric) error {
326 326
 	s.bufMtx.Lock()
327 327
 	s.mtx.Lock()
328 328
 	// Swap bufs even if hotBuf is empty to set new hotBufExpTime.
329
-	s.swapBufs(time.Now())
329
+	s.swapBufs(s.now())
330 330
 	s.bufMtx.Unlock()
331 331
 
332 332
 	s.flushColdBuf()
... ...
@@ -45,7 +45,7 @@ func ResponseFormat(h http.Header) Format {
45 45
 
46 46
 	mediatype, params, err := mime.ParseMediaType(ct)
47 47
 	if err != nil {
48
-		return fmtUnknown
48
+		return FmtUnknown
49 49
 	}
50 50
 
51 51
 	const textType = "text/plain"
... ...
@@ -53,21 +53,21 @@ func ResponseFormat(h http.Header) Format {
53 53
 	switch mediatype {
54 54
 	case ProtoType:
55 55
 		if p, ok := params["proto"]; ok && p != ProtoProtocol {
56
-			return fmtUnknown
56
+			return FmtUnknown
57 57
 		}
58 58
 		if e, ok := params["encoding"]; ok && e != "delimited" {
59
-			return fmtUnknown
59
+			return FmtUnknown
60 60
 		}
61
-		return fmtProtoDelim
61
+		return FmtProtoDelim
62 62
 
63 63
 	case textType:
64 64
 		if v, ok := params["version"]; ok && v != TextVersion {
65
-			return fmtUnknown
65
+			return FmtUnknown
66 66
 		}
67
-		return fmtText
67
+		return FmtText
68 68
 	}
69 69
 
70
-	return fmtUnknown
70
+	return FmtUnknown
71 71
 }
72 72
 
73 73
 // NewDecoder returns a new decoder based on the given input format.
... ...
@@ -68,7 +68,7 @@ func Negotiate(h http.Header) Format {
68 68
 		if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
69 69
 			switch Format(escapeParam) {
70 70
 			case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
71
-				escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam))
71
+				escapingScheme = Format("; escaping=" + escapeParam)
72 72
 			default:
73 73
 				// If the escaping parameter is unknown, ignore it.
74 74
 			}
... ...
@@ -77,18 +77,18 @@ func Negotiate(h http.Header) Format {
77 77
 		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
78 78
 			switch ac.Params["encoding"] {
79 79
 			case "delimited":
80
-				return fmtProtoDelim + escapingScheme
80
+				return FmtProtoDelim + escapingScheme
81 81
 			case "text":
82
-				return fmtProtoText + escapingScheme
82
+				return FmtProtoText + escapingScheme
83 83
 			case "compact-text":
84
-				return fmtProtoCompact + escapingScheme
84
+				return FmtProtoCompact + escapingScheme
85 85
 			}
86 86
 		}
87 87
 		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
88
-			return fmtText + escapingScheme
88
+			return FmtText + escapingScheme
89 89
 		}
90 90
 	}
91
-	return fmtText + escapingScheme
91
+	return FmtText + escapingScheme
92 92
 }
93 93
 
94 94
 // NegotiateIncludingOpenMetrics works like Negotiate but includes
... ...
@@ -101,7 +101,7 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
101 101
 		if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
102 102
 			switch Format(escapeParam) {
103 103
 			case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
104
-				escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam))
104
+				escapingScheme = Format("; escaping=" + escapeParam)
105 105
 			default:
106 106
 				// If the escaping parameter is unknown, ignore it.
107 107
 			}
... ...
@@ -110,26 +110,26 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
110 110
 		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
111 111
 			switch ac.Params["encoding"] {
112 112
 			case "delimited":
113
-				return fmtProtoDelim + escapingScheme
113
+				return FmtProtoDelim + escapingScheme
114 114
 			case "text":
115
-				return fmtProtoText + escapingScheme
115
+				return FmtProtoText + escapingScheme
116 116
 			case "compact-text":
117
-				return fmtProtoCompact + escapingScheme
117
+				return FmtProtoCompact + escapingScheme
118 118
 			}
119 119
 		}
120 120
 		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
121
-			return fmtText + escapingScheme
121
+			return FmtText + escapingScheme
122 122
 		}
123 123
 		if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") {
124 124
 			switch ver {
125 125
 			case OpenMetricsVersion_1_0_0:
126
-				return fmtOpenMetrics_1_0_0 + escapingScheme
126
+				return FmtOpenMetrics_1_0_0 + escapingScheme
127 127
 			default:
128
-				return fmtOpenMetrics_0_0_1 + escapingScheme
128
+				return FmtOpenMetrics_0_0_1 + escapingScheme
129 129
 			}
130 130
 		}
131 131
 	}
132
-	return fmtText + escapingScheme
132
+	return FmtText + escapingScheme
133 133
 }
134 134
 
135 135
 // NewEncoder returns a new encoder based on content type negotiation. All
... ...
@@ -15,7 +15,7 @@
15 15
 package expfmt
16 16
 
17 17
 import (
18
-	"fmt"
18
+	"errors"
19 19
 	"strings"
20 20
 
21 21
 	"github.com/prometheus/common/model"
... ...
@@ -32,24 +32,31 @@ type Format string
32 32
 // it on the wire, new content-type strings will have to be agreed upon and
33 33
 // added here.
34 34
 const (
35
-	TextVersion              = "0.0.4"
36
-	ProtoType                = `application/vnd.google.protobuf`
37
-	ProtoProtocol            = `io.prometheus.client.MetricFamily`
38
-	protoFmt                 = ProtoType + "; proto=" + ProtoProtocol + ";"
35
+	TextVersion   = "0.0.4"
36
+	ProtoType     = `application/vnd.google.protobuf`
37
+	ProtoProtocol = `io.prometheus.client.MetricFamily`
38
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
39
+	ProtoFmt                 = ProtoType + "; proto=" + ProtoProtocol + ";"
39 40
 	OpenMetricsType          = `application/openmetrics-text`
40 41
 	OpenMetricsVersion_0_0_1 = "0.0.1"
41 42
 	OpenMetricsVersion_1_0_0 = "1.0.0"
42 43
 
43
-	// The Content-Type values for the different wire protocols. Note that these
44
-	// values are now unexported. If code was relying on comparisons to these
45
-	// constants, instead use FormatType().
46
-	fmtUnknown           Format = `<unknown>`
47
-	fmtText              Format = `text/plain; version=` + TextVersion + `; charset=utf-8`
48
-	fmtProtoDelim        Format = protoFmt + ` encoding=delimited`
49
-	fmtProtoText         Format = protoFmt + ` encoding=text`
50
-	fmtProtoCompact      Format = protoFmt + ` encoding=compact-text`
51
-	fmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
52
-	fmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
44
+	// The Content-Type values for the different wire protocols. Do not do direct
45
+	// comparisons to these constants, instead use the comparison functions.
46
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeUnknown) instead.
47
+	FmtUnknown Format = `<unknown>`
48
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeTextPlain) instead.
49
+	FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8`
50
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoDelim) instead.
51
+	FmtProtoDelim Format = ProtoFmt + ` encoding=delimited`
52
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoText) instead.
53
+	FmtProtoText Format = ProtoFmt + ` encoding=text`
54
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
55
+	FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
56
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
57
+	FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
58
+	// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
59
+	FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
53 60
 )
54 61
 
55 62
 const (
... ...
@@ -79,17 +86,17 @@ const (
79 79
 func NewFormat(t FormatType) Format {
80 80
 	switch t {
81 81
 	case TypeProtoCompact:
82
-		return fmtProtoCompact
82
+		return FmtProtoCompact
83 83
 	case TypeProtoDelim:
84
-		return fmtProtoDelim
84
+		return FmtProtoDelim
85 85
 	case TypeProtoText:
86
-		return fmtProtoText
86
+		return FmtProtoText
87 87
 	case TypeTextPlain:
88
-		return fmtText
88
+		return FmtText
89 89
 	case TypeOpenMetrics:
90
-		return fmtOpenMetrics_1_0_0
90
+		return FmtOpenMetrics_1_0_0
91 91
 	default:
92
-		return fmtUnknown
92
+		return FmtUnknown
93 93
 	}
94 94
 }
95 95
 
... ...
@@ -97,12 +104,35 @@ func NewFormat(t FormatType) Format {
97 97
 // specified version number.
98 98
 func NewOpenMetricsFormat(version string) (Format, error) {
99 99
 	if version == OpenMetricsVersion_0_0_1 {
100
-		return fmtOpenMetrics_0_0_1, nil
100
+		return FmtOpenMetrics_0_0_1, nil
101 101
 	}
102 102
 	if version == OpenMetricsVersion_1_0_0 {
103
-		return fmtOpenMetrics_1_0_0, nil
103
+		return FmtOpenMetrics_1_0_0, nil
104 104
 	}
105
-	return fmtUnknown, fmt.Errorf("unknown open metrics version string")
105
+	return FmtUnknown, errors.New("unknown open metrics version string")
106
+}
107
+
108
+// WithEscapingScheme returns a copy of Format with the specified escaping
109
+// scheme appended to the end. If an escaping scheme already exists it is
110
+// removed.
111
+func (f Format) WithEscapingScheme(s model.EscapingScheme) Format {
112
+	var terms []string
113
+	for _, p := range strings.Split(string(f), ";") {
114
+		toks := strings.Split(p, "=")
115
+		if len(toks) != 2 {
116
+			trimmed := strings.TrimSpace(p)
117
+			if len(trimmed) > 0 {
118
+				terms = append(terms, trimmed)
119
+			}
120
+			continue
121
+		}
122
+		key := strings.TrimSpace(toks[0])
123
+		if key != model.EscapingKey {
124
+			terms = append(terms, strings.TrimSpace(p))
125
+		}
126
+	}
127
+	terms = append(terms, model.EscapingKey+"="+s.String())
128
+	return Format(strings.Join(terms, "; "))
106 129
 }
107 130
 
108 131
 // FormatType deduces an overall FormatType for the given format.
... ...
@@ -38,7 +38,7 @@ type EncoderOption func(*encoderOption)
38 38
 
39 39
 // WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder
40 40
 // to include _created lines (See
41
-// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1).
41
+// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1).
42 42
 // Created timestamps can improve the accuracy of series reset detection, but
43 43
 // come with a bandwidth cost.
44 44
 //
... ...
@@ -102,7 +102,7 @@ func WithUnit() EncoderOption {
102 102
 //
103 103
 //   - According to the OM specs, the `# UNIT` line is optional, but if populated,
104 104
 //     the unit has to be present in the metric name as its suffix:
105
-//     (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit).
105
+//     (see https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#unit).
106 106
 //     However, in order to accommodate any potential scenario where such a change in the
107 107
 //     metric name is not desirable, the users are here given the choice of either explicitly
108 108
 //     opt in, in case they wish for the unit to be included in the output AND in the metric name
... ...
@@ -152,8 +152,8 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
152 152
 	if metricType == dto.MetricType_COUNTER && strings.HasSuffix(compliantName, "_total") {
153 153
 		compliantName = name[:len(name)-6]
154 154
 	}
155
-	if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, fmt.Sprintf("_%s", *in.Unit)) {
156
-		compliantName = compliantName + fmt.Sprintf("_%s", *in.Unit)
155
+	if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, "_"+*in.Unit) {
156
+		compliantName = compliantName + "_" + *in.Unit
157 157
 	}
158 158
 
159 159
 	// Comments, first HELP, then TYPE.
... ...
@@ -477,7 +477,7 @@ func writeOpenMetricsNameAndLabelPairs(
477 477
 	if name != "" {
478 478
 		// If the name does not pass the legacy validity check, we must put the
479 479
 		// metric name inside the braces, quoted.
480
-		if !model.IsValidLegacyMetricName(model.LabelValue(name)) {
480
+		if !model.IsValidLegacyMetricName(name) {
481 481
 			metricInsideBraces = true
482 482
 			err := w.WriteByte(separator)
483 483
 			written++
... ...
@@ -354,7 +354,7 @@ func writeNameAndLabelPairs(
354 354
 	if name != "" {
355 355
 		// If the name does not pass the legacy validity check, we must put the
356 356
 		// metric name inside the braces.
357
-		if !model.IsValidLegacyMetricName(model.LabelValue(name)) {
357
+		if !model.IsValidLegacyMetricName(name) {
358 358
 			metricInsideBraces = true
359 359
 			err := w.WriteByte(separator)
360 360
 			written++
... ...
@@ -498,7 +498,7 @@ func writeInt(w enhancedWriter, i int64) (int, error) {
498 498
 // writeName writes a string as-is if it complies with the legacy naming
499 499
 // scheme, or escapes it in double quotes if not.
500 500
 func writeName(w enhancedWriter, name string) (int, error) {
501
-	if model.IsValidLegacyMetricName(model.LabelValue(name)) {
501
+	if model.IsValidLegacyMetricName(name) {
502 502
 		return w.WriteString(name)
503 503
 	}
504 504
 	var written int
... ...
@@ -22,9 +22,9 @@ import (
22 22
 	"math"
23 23
 	"strconv"
24 24
 	"strings"
25
+	"unicode/utf8"
25 26
 
26 27
 	dto "github.com/prometheus/client_model/go"
27
-
28 28
 	"google.golang.org/protobuf/proto"
29 29
 
30 30
 	"github.com/prometheus/common/model"
... ...
@@ -60,6 +60,7 @@ type TextParser struct {
60 60
 	currentMF            *dto.MetricFamily
61 61
 	currentMetric        *dto.Metric
62 62
 	currentLabelPair     *dto.LabelPair
63
+	currentLabelPairs    []*dto.LabelPair // Temporarily stores label pairs while parsing a metric line.
63 64
 
64 65
 	// The remaining member variables are only used for summaries/histograms.
65 66
 	currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'/'le'
... ...
@@ -74,6 +75,9 @@ type TextParser struct {
74 74
 	// count and sum of that summary/histogram.
75 75
 	currentIsSummaryCount, currentIsSummarySum     bool
76 76
 	currentIsHistogramCount, currentIsHistogramSum bool
77
+	// These indicate if the metric name from the current line being parsed is inside
78
+	// braces and if that metric name was found respectively.
79
+	currentMetricIsInsideBraces, currentMetricInsideBracesIsPresent bool
77 80
 }
78 81
 
79 82
 // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
... ...
@@ -137,12 +141,15 @@ func (p *TextParser) reset(in io.Reader) {
137 137
 	}
138 138
 	p.currentQuantile = math.NaN()
139 139
 	p.currentBucket = math.NaN()
140
+	p.currentMF = nil
140 141
 }
141 142
 
142 143
 // startOfLine represents the state where the next byte read from p.buf is the
143 144
 // start of a line (or whitespace leading up to it).
144 145
 func (p *TextParser) startOfLine() stateFn {
145 146
 	p.lineCount++
147
+	p.currentMetricIsInsideBraces = false
148
+	p.currentMetricInsideBracesIsPresent = false
146 149
 	if p.skipBlankTab(); p.err != nil {
147 150
 		// This is the only place that we expect to see io.EOF,
148 151
 		// which is not an error but the signal that we are done.
... ...
@@ -158,6 +165,9 @@ func (p *TextParser) startOfLine() stateFn {
158 158
 		return p.startComment
159 159
 	case '\n':
160 160
 		return p.startOfLine // Empty line, start the next one.
161
+	case '{':
162
+		p.currentMetricIsInsideBraces = true
163
+		return p.readingLabels
161 164
 	}
162 165
 	return p.readingMetricName
163 166
 }
... ...
@@ -275,6 +285,8 @@ func (p *TextParser) startLabelName() stateFn {
275 275
 		return nil // Unexpected end of input.
276 276
 	}
277 277
 	if p.currentByte == '}' {
278
+		p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...)
279
+		p.currentLabelPairs = nil
278 280
 		if p.skipBlankTab(); p.err != nil {
279 281
 			return nil // Unexpected end of input.
280 282
 		}
... ...
@@ -287,6 +299,45 @@ func (p *TextParser) startLabelName() stateFn {
287 287
 		p.parseError(fmt.Sprintf("invalid label name for metric %q", p.currentMF.GetName()))
288 288
 		return nil
289 289
 	}
290
+	if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
291
+		return nil // Unexpected end of input.
292
+	}
293
+	if p.currentByte != '=' {
294
+		if p.currentMetricIsInsideBraces {
295
+			if p.currentMetricInsideBracesIsPresent {
296
+				p.parseError(fmt.Sprintf("multiple metric names for metric %q", p.currentMF.GetName()))
297
+				return nil
298
+			}
299
+			switch p.currentByte {
300
+			case ',':
301
+				p.setOrCreateCurrentMF()
302
+				if p.currentMF.Type == nil {
303
+					p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
304
+				}
305
+				p.currentMetric = &dto.Metric{}
306
+				p.currentMetricInsideBracesIsPresent = true
307
+				return p.startLabelName
308
+			case '}':
309
+				p.setOrCreateCurrentMF()
310
+				if p.currentMF.Type == nil {
311
+					p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
312
+				}
313
+				p.currentMetric = &dto.Metric{}
314
+				p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...)
315
+				p.currentLabelPairs = nil
316
+				if p.skipBlankTab(); p.err != nil {
317
+					return nil // Unexpected end of input.
318
+				}
319
+				return p.readingValue
320
+			default:
321
+				p.parseError(fmt.Sprintf("unexpected end of metric name %q", p.currentByte))
322
+				return nil
323
+			}
324
+		}
325
+		p.parseError(fmt.Sprintf("expected '=' after label name, found %q", p.currentByte))
326
+		p.currentLabelPairs = nil
327
+		return nil
328
+	}
290 329
 	p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())}
291 330
 	if p.currentLabelPair.GetName() == string(model.MetricNameLabel) {
292 331
 		p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
... ...
@@ -296,23 +347,17 @@ func (p *TextParser) startLabelName() stateFn {
296 296
 	// labels to 'real' labels.
297 297
 	if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) &&
298 298
 		!(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) {
299
-		p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair)
300
-	}
301
-	if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
302
-		return nil // Unexpected end of input.
303
-	}
304
-	if p.currentByte != '=' {
305
-		p.parseError(fmt.Sprintf("expected '=' after label name, found %q", p.currentByte))
306
-		return nil
299
+		p.currentLabelPairs = append(p.currentLabelPairs, p.currentLabelPair)
307 300
 	}
308 301
 	// Check for duplicate label names.
309 302
 	labels := make(map[string]struct{})
310
-	for _, l := range p.currentMetric.Label {
303
+	for _, l := range p.currentLabelPairs {
311 304
 		lName := l.GetName()
312 305
 		if _, exists := labels[lName]; !exists {
313 306
 			labels[lName] = struct{}{}
314 307
 		} else {
315 308
 			p.parseError(fmt.Sprintf("duplicate label names for metric %q", p.currentMF.GetName()))
309
+			p.currentLabelPairs = nil
316 310
 			return nil
317 311
 		}
318 312
 	}
... ...
@@ -345,6 +390,7 @@ func (p *TextParser) startLabelValue() stateFn {
345 345
 			if p.currentQuantile, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil {
346 346
 				// Create a more helpful error message.
347 347
 				p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue()))
348
+				p.currentLabelPairs = nil
348 349
 				return nil
349 350
 			}
350 351
 		} else {
... ...
@@ -371,12 +417,19 @@ func (p *TextParser) startLabelValue() stateFn {
371 371
 		return p.startLabelName
372 372
 
373 373
 	case '}':
374
+		if p.currentMF == nil {
375
+			p.parseError("invalid metric name")
376
+			return nil
377
+		}
378
+		p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...)
379
+		p.currentLabelPairs = nil
374 380
 		if p.skipBlankTab(); p.err != nil {
375 381
 			return nil // Unexpected end of input.
376 382
 		}
377 383
 		return p.readingValue
378 384
 	default:
379 385
 		p.parseError(fmt.Sprintf("unexpected end of label value %q", p.currentLabelPair.GetValue()))
386
+		p.currentLabelPairs = nil
380 387
 		return nil
381 388
 	}
382 389
 }
... ...
@@ -585,6 +638,8 @@ func (p *TextParser) readTokenUntilNewline(recognizeEscapeSequence bool) {
585 585
 				p.currentToken.WriteByte(p.currentByte)
586 586
 			case 'n':
587 587
 				p.currentToken.WriteByte('\n')
588
+			case '"':
589
+				p.currentToken.WriteByte('"')
588 590
 			default:
589 591
 				p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
590 592
 				return
... ...
@@ -610,13 +665,45 @@ func (p *TextParser) readTokenUntilNewline(recognizeEscapeSequence bool) {
610 610
 // but not into p.currentToken.
611 611
 func (p *TextParser) readTokenAsMetricName() {
612 612
 	p.currentToken.Reset()
613
+	// A UTF-8 metric name must be quoted and may have escaped characters.
614
+	quoted := false
615
+	escaped := false
613 616
 	if !isValidMetricNameStart(p.currentByte) {
614 617
 		return
615 618
 	}
616
-	for {
617
-		p.currentToken.WriteByte(p.currentByte)
619
+	for p.err == nil {
620
+		if escaped {
621
+			switch p.currentByte {
622
+			case '\\':
623
+				p.currentToken.WriteByte(p.currentByte)
624
+			case 'n':
625
+				p.currentToken.WriteByte('\n')
626
+			case '"':
627
+				p.currentToken.WriteByte('"')
628
+			default:
629
+				p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
630
+				return
631
+			}
632
+			escaped = false
633
+		} else {
634
+			switch p.currentByte {
635
+			case '"':
636
+				quoted = !quoted
637
+				if !quoted {
638
+					p.currentByte, p.err = p.buf.ReadByte()
639
+					return
640
+				}
641
+			case '\n':
642
+				p.parseError(fmt.Sprintf("metric name %q contains unescaped new-line", p.currentToken.String()))
643
+				return
644
+			case '\\':
645
+				escaped = true
646
+			default:
647
+				p.currentToken.WriteByte(p.currentByte)
648
+			}
649
+		}
618 650
 		p.currentByte, p.err = p.buf.ReadByte()
619
-		if p.err != nil || !isValidMetricNameContinuation(p.currentByte) {
651
+		if !isValidMetricNameContinuation(p.currentByte, quoted) || (!quoted && p.currentByte == ' ') {
620 652
 			return
621 653
 		}
622 654
 	}
... ...
@@ -628,13 +715,45 @@ func (p *TextParser) readTokenAsMetricName() {
628 628
 // but not into p.currentToken.
629 629
 func (p *TextParser) readTokenAsLabelName() {
630 630
 	p.currentToken.Reset()
631
+	// A UTF-8 label name must be quoted and may have escaped characters.
632
+	quoted := false
633
+	escaped := false
631 634
 	if !isValidLabelNameStart(p.currentByte) {
632 635
 		return
633 636
 	}
634
-	for {
635
-		p.currentToken.WriteByte(p.currentByte)
637
+	for p.err == nil {
638
+		if escaped {
639
+			switch p.currentByte {
640
+			case '\\':
641
+				p.currentToken.WriteByte(p.currentByte)
642
+			case 'n':
643
+				p.currentToken.WriteByte('\n')
644
+			case '"':
645
+				p.currentToken.WriteByte('"')
646
+			default:
647
+				p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
648
+				return
649
+			}
650
+			escaped = false
651
+		} else {
652
+			switch p.currentByte {
653
+			case '"':
654
+				quoted = !quoted
655
+				if !quoted {
656
+					p.currentByte, p.err = p.buf.ReadByte()
657
+					return
658
+				}
659
+			case '\n':
660
+				p.parseError(fmt.Sprintf("label name %q contains unescaped new-line", p.currentToken.String()))
661
+				return
662
+			case '\\':
663
+				escaped = true
664
+			default:
665
+				p.currentToken.WriteByte(p.currentByte)
666
+			}
667
+		}
636 668
 		p.currentByte, p.err = p.buf.ReadByte()
637
-		if p.err != nil || !isValidLabelNameContinuation(p.currentByte) {
669
+		if !isValidLabelNameContinuation(p.currentByte, quoted) || (!quoted && p.currentByte == '=') {
638 670
 			return
639 671
 		}
640 672
 	}
... ...
@@ -660,6 +779,7 @@ func (p *TextParser) readTokenAsLabelValue() {
660 660
 				p.currentToken.WriteByte('\n')
661 661
 			default:
662 662
 				p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
663
+				p.currentLabelPairs = nil
663 664
 				return
664 665
 			}
665 666
 			escaped = false
... ...
@@ -718,19 +838,19 @@ func (p *TextParser) setOrCreateCurrentMF() {
718 718
 }
719 719
 
720 720
 func isValidLabelNameStart(b byte) bool {
721
-	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_'
721
+	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == '"'
722 722
 }
723 723
 
724
-func isValidLabelNameContinuation(b byte) bool {
725
-	return isValidLabelNameStart(b) || (b >= '0' && b <= '9')
724
+func isValidLabelNameContinuation(b byte, quoted bool) bool {
725
+	return isValidLabelNameStart(b) || (b >= '0' && b <= '9') || (quoted && utf8.ValidString(string(b)))
726 726
 }
727 727
 
728 728
 func isValidMetricNameStart(b byte) bool {
729 729
 	return isValidLabelNameStart(b) || b == ':'
730 730
 }
731 731
 
732
-func isValidMetricNameContinuation(b byte) bool {
733
-	return isValidLabelNameContinuation(b) || b == ':'
732
+func isValidMetricNameContinuation(b byte, quoted bool) bool {
733
+	return isValidLabelNameContinuation(b, quoted) || b == ':'
734 734
 }
735 735
 
736 736
 func isBlankOrTab(b byte) bool {
... ...
@@ -775,7 +895,7 @@ func histogramMetricName(name string) string {
775 775
 
776 776
 func parseFloat(s string) (float64, error) {
777 777
 	if strings.ContainsAny(s, "pP_") {
778
-		return 0, fmt.Errorf("unsupported character in float")
778
+		return 0, errors.New("unsupported character in float")
779 779
 	}
780 780
 	return strconv.ParseFloat(s, 64)
781 781
 }
... ...
@@ -14,6 +14,7 @@
14 14
 package model
15 15
 
16 16
 import (
17
+	"errors"
17 18
 	"fmt"
18 19
 	"time"
19 20
 )
... ...
@@ -89,16 +90,16 @@ func (a *Alert) StatusAt(ts time.Time) AlertStatus {
89 89
 // Validate checks whether the alert data is inconsistent.
90 90
 func (a *Alert) Validate() error {
91 91
 	if a.StartsAt.IsZero() {
92
-		return fmt.Errorf("start time missing")
92
+		return errors.New("start time missing")
93 93
 	}
94 94
 	if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) {
95
-		return fmt.Errorf("start time must be before end time")
95
+		return errors.New("start time must be before end time")
96 96
 	}
97 97
 	if err := a.Labels.Validate(); err != nil {
98 98
 		return fmt.Errorf("invalid label set: %w", err)
99 99
 	}
100 100
 	if len(a.Labels) == 0 {
101
-		return fmt.Errorf("at least one label pair required")
101
+		return errors.New("at least one label pair required")
102 102
 	}
103 103
 	if err := a.Annotations.Validate(); err != nil {
104 104
 		return fmt.Errorf("invalid annotations: %w", err)
... ...
@@ -97,26 +97,35 @@ var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
97 97
 // therewith.
98 98
 type LabelName string
99 99
 
100
-// IsValid returns true iff name matches the pattern of LabelNameRE for legacy
101
-// names, and iff it's valid UTF-8 if NameValidationScheme is set to
102
-// UTF8Validation. For the legacy matching, it does not use LabelNameRE for the
103
-// check but a much faster hardcoded implementation.
100
+// IsValid returns true iff the name matches the pattern of LabelNameRE when
101
+// NameValidationScheme is set to LegacyValidation, or valid UTF-8 if
102
+// NameValidationScheme is set to UTF8Validation.
104 103
 func (ln LabelName) IsValid() bool {
105 104
 	if len(ln) == 0 {
106 105
 		return false
107 106
 	}
108 107
 	switch NameValidationScheme {
109 108
 	case LegacyValidation:
110
-		for i, b := range ln {
111
-			if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) {
112
-				return false
113
-			}
114
-		}
109
+		return ln.IsValidLegacy()
115 110
 	case UTF8Validation:
116 111
 		return utf8.ValidString(string(ln))
117 112
 	default:
118 113
 		panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
119 114
 	}
115
+}
116
+
117
+// IsValidLegacy returns true iff name matches the pattern of LabelNameRE for
118
+// legacy names. It does not use LabelNameRE for the check but a much faster
119
+// hardcoded implementation.
120
+func (ln LabelName) IsValidLegacy() bool {
121
+	if len(ln) == 0 {
122
+		return false
123
+	}
124
+	for i, b := range ln {
125
+		if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) {
126
+			return false
127
+		}
128
+	}
120 129
 	return true
121 130
 }
122 131
 
... ...
@@ -11,8 +11,6 @@
11 11
 // See the License for the specific language governing permissions and
12 12
 // limitations under the License.
13 13
 
14
-//go:build go1.21
15
-
16 14
 package model
17 15
 
18 16
 import (
19 17
deleted file mode 100644
... ...
@@ -1,39 +0,0 @@
1
-// Copyright 2024 The Prometheus Authors
2
-// Licensed under the Apache License, Version 2.0 (the "License");
3
-// you may not use this file except in compliance with the License.
4
-// You may obtain a copy of the License at
5
-//
6
-// http://www.apache.org/licenses/LICENSE-2.0
7
-//
8
-// Unless required by applicable law or agreed to in writing, software
9
-// distributed under the License is distributed on an "AS IS" BASIS,
10
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
-// See the License for the specific language governing permissions and
12
-// limitations under the License.
13
-
14
-//go:build !go1.21
15
-
16
-package model
17
-
18
-import (
19
-	"fmt"
20
-	"sort"
21
-	"strings"
22
-)
23
-
24
-// String was optimized using functions not available for go 1.20
25
-// or lower. We keep the old implementation for compatibility with client_golang.
26
-// Once client golang drops support for go 1.20 (scheduled for August 2024), this
27
-// file can be removed.
28
-func (l LabelSet) String() string {
29
-	labelNames := make([]string, 0, len(l))
30
-	for name := range l {
31
-		labelNames = append(labelNames, string(name))
32
-	}
33
-	sort.Strings(labelNames)
34
-	lstrs := make([]string, 0, len(l))
35
-	for _, name := range labelNames {
36
-		lstrs = append(lstrs, fmt.Sprintf("%s=%q", name, l[LabelName(name)]))
37
-	}
38
-	return fmt.Sprintf("{%s}", strings.Join(lstrs, ", "))
39
-}
... ...
@@ -14,9 +14,11 @@
14 14
 package model
15 15
 
16 16
 import (
17
+	"errors"
17 18
 	"fmt"
18 19
 	"regexp"
19 20
 	"sort"
21
+	"strconv"
20 22
 	"strings"
21 23
 	"unicode/utf8"
22 24
 
... ...
@@ -26,18 +28,21 @@ import (
26 26
 
27 27
 var (
28 28
 	// NameValidationScheme determines the method of name validation to be used by
29
-	// all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8 mode
30
-	// in isolation from other components that don't support UTF-8 may result in
31
-	// bugs or other undefined behavior. This value is intended to be set by
32
-	// UTF-8-aware binaries as part of their startup. To avoid need for locking,
33
-	// this value should be set once, ideally in an init(), before multiple
34
-	// goroutines are started.
35
-	NameValidationScheme = LegacyValidation
36
-
37
-	// NameEscapingScheme defines the default way that names will be
38
-	// escaped when presented to systems that do not support UTF-8 names. If the
39
-	// Content-Type "escaping" term is specified, that will override this value.
40
-	NameEscapingScheme = ValueEncodingEscaping
29
+	// all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8
30
+	// mode in isolation from other components that don't support UTF-8 may result
31
+	// in bugs or other undefined behavior. This value can be set to
32
+	// LegacyValidation during startup if a binary is not UTF-8-aware binaries. To
33
+	// avoid need for locking, this value should be set once, ideally in an
34
+	// init(), before multiple goroutines are started.
35
+	NameValidationScheme = UTF8Validation
36
+
37
+	// NameEscapingScheme defines the default way that names will be escaped when
38
+	// presented to systems that do not support UTF-8 names. If the Content-Type
39
+	// "escaping" term is specified, that will override this value.
40
+	// NameEscapingScheme should not be set to the NoEscaping value. That string
41
+	// is used in content negotiation to indicate that a system supports UTF-8 and
42
+	// has that feature enabled.
43
+	NameEscapingScheme = UnderscoreEscaping
41 44
 )
42 45
 
43 46
 // ValidationScheme is a Go enum for determining how metric and label names will
... ...
@@ -161,7 +166,7 @@ func (m Metric) FastFingerprint() Fingerprint {
161 161
 func IsValidMetricName(n LabelValue) bool {
162 162
 	switch NameValidationScheme {
163 163
 	case LegacyValidation:
164
-		return IsValidLegacyMetricName(n)
164
+		return IsValidLegacyMetricName(string(n))
165 165
 	case UTF8Validation:
166 166
 		if len(n) == 0 {
167 167
 			return false
... ...
@@ -176,7 +181,7 @@ func IsValidMetricName(n LabelValue) bool {
176 176
 // legacy validation scheme regardless of the value of NameValidationScheme.
177 177
 // This function, however, does not use MetricNameRE for the check but a much
178 178
 // faster hardcoded implementation.
179
-func IsValidLegacyMetricName(n LabelValue) bool {
179
+func IsValidLegacyMetricName(n string) bool {
180 180
 	if len(n) == 0 {
181 181
 		return false
182 182
 	}
... ...
@@ -208,7 +213,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
208 208
 	}
209 209
 
210 210
 	// If the name is nil, copy as-is, don't try to escape.
211
-	if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) {
211
+	if v.Name == nil || IsValidLegacyMetricName(v.GetName()) {
212 212
 		out.Name = v.Name
213 213
 	} else {
214 214
 		out.Name = proto.String(EscapeName(v.GetName(), scheme))
... ...
@@ -230,7 +235,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
230 230
 
231 231
 		for _, l := range m.Label {
232 232
 			if l.GetName() == MetricNameLabel {
233
-				if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) {
233
+				if l.Value == nil || IsValidLegacyMetricName(l.GetValue()) {
234 234
 					escaped.Label = append(escaped.Label, l)
235 235
 					continue
236 236
 				}
... ...
@@ -240,7 +245,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
240 240
 				})
241 241
 				continue
242 242
 			}
243
-			if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) {
243
+			if l.Name == nil || IsValidLegacyMetricName(l.GetName()) {
244 244
 				escaped.Label = append(escaped.Label, l)
245 245
 				continue
246 246
 			}
... ...
@@ -256,20 +261,16 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
256 256
 
257 257
 func metricNeedsEscaping(m *dto.Metric) bool {
258 258
 	for _, l := range m.Label {
259
-		if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) {
259
+		if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(l.GetValue()) {
260 260
 			return true
261 261
 		}
262
-		if !IsValidLegacyMetricName(LabelValue(l.GetName())) {
262
+		if !IsValidLegacyMetricName(l.GetName()) {
263 263
 			return true
264 264
 		}
265 265
 	}
266 266
 	return false
267 267
 }
268 268
 
269
-const (
270
-	lowerhex = "0123456789abcdef"
271
-)
272
-
273 269
 // EscapeName escapes the incoming name according to the provided escaping
274 270
 // scheme. Depending on the rules of escaping, this may cause no change in the
275 271
 // string that is returned. (Especially NoEscaping, which by definition is a
... ...
@@ -283,7 +284,7 @@ func EscapeName(name string, scheme EscapingScheme) string {
283 283
 	case NoEscaping:
284 284
 		return name
285 285
 	case UnderscoreEscaping:
286
-		if IsValidLegacyMetricName(LabelValue(name)) {
286
+		if IsValidLegacyMetricName(name) {
287 287
 			return name
288 288
 		}
289 289
 		for i, b := range name {
... ...
@@ -304,31 +305,25 @@ func EscapeName(name string, scheme EscapingScheme) string {
304 304
 			} else if isValidLegacyRune(b, i) {
305 305
 				escaped.WriteRune(b)
306 306
 			} else {
307
-				escaped.WriteRune('_')
307
+				escaped.WriteString("__")
308 308
 			}
309 309
 		}
310 310
 		return escaped.String()
311 311
 	case ValueEncodingEscaping:
312
-		if IsValidLegacyMetricName(LabelValue(name)) {
312
+		if IsValidLegacyMetricName(name) {
313 313
 			return name
314 314
 		}
315 315
 		escaped.WriteString("U__")
316 316
 		for i, b := range name {
317
-			if isValidLegacyRune(b, i) {
317
+			if b == '_' {
318
+				escaped.WriteString("__")
319
+			} else if isValidLegacyRune(b, i) {
318 320
 				escaped.WriteRune(b)
319 321
 			} else if !utf8.ValidRune(b) {
320 322
 				escaped.WriteString("_FFFD_")
321
-			} else if b < 0x100 {
322
-				escaped.WriteRune('_')
323
-				for s := 4; s >= 0; s -= 4 {
324
-					escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
325
-				}
326
-				escaped.WriteRune('_')
327
-			} else if b < 0x10000 {
323
+			} else {
328 324
 				escaped.WriteRune('_')
329
-				for s := 12; s >= 0; s -= 4 {
330
-					escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
331
-				}
325
+				escaped.WriteString(strconv.FormatInt(int64(b), 16))
332 326
 				escaped.WriteRune('_')
333 327
 			}
334 328
 		}
... ...
@@ -386,8 +381,9 @@ func UnescapeName(name string, scheme EscapingScheme) string {
386 386
 			// We think we are in a UTF-8 code, process it.
387 387
 			var utf8Val uint
388 388
 			for j := 0; i < len(escapedName); j++ {
389
-				// This is too many characters for a utf8 value.
390
-				if j > 4 {
389
+				// This is too many characters for a utf8 value based on the MaxRune
390
+				// value of '\U0010FFFF'.
391
+				if j >= 6 {
391 392
 					return name
392 393
 				}
393 394
 				// Found a closing underscore, convert to a rune, check validity, and append.
... ...
@@ -440,7 +436,7 @@ func (e EscapingScheme) String() string {
440 440
 
441 441
 func ToEscapingScheme(s string) (EscapingScheme, error) {
442 442
 	if s == "" {
443
-		return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme")
443
+		return NoEscaping, errors.New("got empty string instead of escaping scheme")
444 444
 	}
445 445
 	switch s {
446 446
 	case AllowUTF8:
... ...
@@ -452,6 +448,6 @@ func ToEscapingScheme(s string) (EscapingScheme, error) {
452 452
 	case EscapeValues:
453 453
 		return ValueEncodingEscaping, nil
454 454
 	default:
455
-		return NoEscaping, fmt.Errorf("unknown format scheme " + s)
455
+		return NoEscaping, fmt.Errorf("unknown format scheme %s", s)
456 456
 	}
457 457
 }
... ...
@@ -15,6 +15,7 @@ package model
15 15
 
16 16
 import (
17 17
 	"encoding/json"
18
+	"errors"
18 19
 	"fmt"
19 20
 	"regexp"
20 21
 	"time"
... ...
@@ -34,7 +35,7 @@ func (m *Matcher) UnmarshalJSON(b []byte) error {
34 34
 	}
35 35
 
36 36
 	if len(m.Name) == 0 {
37
-		return fmt.Errorf("label name in matcher must not be empty")
37
+		return errors.New("label name in matcher must not be empty")
38 38
 	}
39 39
 	if m.IsRegex {
40 40
 		if _, err := regexp.Compile(m.Value); err != nil {
... ...
@@ -77,7 +78,7 @@ type Silence struct {
77 77
 // Validate returns true iff all fields of the silence have valid values.
78 78
 func (s *Silence) Validate() error {
79 79
 	if len(s.Matchers) == 0 {
80
-		return fmt.Errorf("at least one matcher required")
80
+		return errors.New("at least one matcher required")
81 81
 	}
82 82
 	for _, m := range s.Matchers {
83 83
 		if err := m.Validate(); err != nil {
... ...
@@ -85,22 +86,22 @@ func (s *Silence) Validate() error {
85 85
 		}
86 86
 	}
87 87
 	if s.StartsAt.IsZero() {
88
-		return fmt.Errorf("start time missing")
88
+		return errors.New("start time missing")
89 89
 	}
90 90
 	if s.EndsAt.IsZero() {
91
-		return fmt.Errorf("end time missing")
91
+		return errors.New("end time missing")
92 92
 	}
93 93
 	if s.EndsAt.Before(s.StartsAt) {
94
-		return fmt.Errorf("start time must be before end time")
94
+		return errors.New("start time must be before end time")
95 95
 	}
96 96
 	if s.CreatedBy == "" {
97
-		return fmt.Errorf("creator information missing")
97
+		return errors.New("creator information missing")
98 98
 	}
99 99
 	if s.Comment == "" {
100
-		return fmt.Errorf("comment missing")
100
+		return errors.New("comment missing")
101 101
 	}
102 102
 	if s.CreatedAt.IsZero() {
103
-		return fmt.Errorf("creation timestamp missing")
103
+		return errors.New("creation timestamp missing")
104 104
 	}
105 105
 	return nil
106 106
 }
... ...
@@ -15,6 +15,7 @@ package model
15 15
 
16 16
 import (
17 17
 	"encoding/json"
18
+	"errors"
18 19
 	"fmt"
19 20
 	"math"
20 21
 	"strconv"
... ...
@@ -39,7 +40,7 @@ func (v SampleValue) MarshalJSON() ([]byte, error) {
39 39
 // UnmarshalJSON implements json.Unmarshaler.
40 40
 func (v *SampleValue) UnmarshalJSON(b []byte) error {
41 41
 	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
42
-		return fmt.Errorf("sample value must be a quoted string")
42
+		return errors.New("sample value must be a quoted string")
43 43
 	}
44 44
 	f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
45 45
 	if err != nil {
... ...
@@ -15,6 +15,7 @@ package model
15 15
 
16 16
 import (
17 17
 	"encoding/json"
18
+	"errors"
18 19
 	"fmt"
19 20
 	"strconv"
20 21
 	"strings"
... ...
@@ -32,7 +33,7 @@ func (v FloatString) MarshalJSON() ([]byte, error) {
32 32
 
33 33
 func (v *FloatString) UnmarshalJSON(b []byte) error {
34 34
 	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
35
-		return fmt.Errorf("float value must be a quoted string")
35
+		return errors.New("float value must be a quoted string")
36 36
 	}
37 37
 	f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
38 38
 	if err != nil {
... ...
@@ -141,7 +142,7 @@ type SampleHistogramPair struct {
141 141
 
142 142
 func (s SampleHistogramPair) MarshalJSON() ([]byte, error) {
143 143
 	if s.Histogram == nil {
144
-		return nil, fmt.Errorf("histogram is nil")
144
+		return nil, errors.New("histogram is nil")
145 145
 	}
146 146
 	t, err := json.Marshal(s.Timestamp)
147 147
 	if err != nil {
... ...
@@ -164,7 +165,7 @@ func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error {
164 164
 		return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen)
165 165
 	}
166 166
 	if s.Histogram == nil {
167
-		return fmt.Errorf("histogram is null")
167
+		return errors.New("histogram is null")
168 168
 	}
169 169
 	return nil
170 170
 }
... ...
@@ -1117,18 +1117,19 @@ github.com/planetscale/vtprotobuf/vtproto
1117 1117
 # github.com/pmezard/go-difflib v1.0.0
1118 1118
 ## explicit
1119 1119
 github.com/pmezard/go-difflib/difflib
1120
-# github.com/prometheus/client_golang v1.20.5
1121
-## explicit; go 1.20
1120
+# github.com/prometheus/client_golang v1.22.0
1121
+## explicit; go 1.22
1122 1122
 github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
1123 1123
 github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header
1124 1124
 github.com/prometheus/client_golang/prometheus
1125 1125
 github.com/prometheus/client_golang/prometheus/internal
1126 1126
 github.com/prometheus/client_golang/prometheus/promhttp
1127
+github.com/prometheus/client_golang/prometheus/promhttp/internal
1127 1128
 # github.com/prometheus/client_model v0.6.1
1128 1129
 ## explicit; go 1.19
1129 1130
 github.com/prometheus/client_model/go
1130
-# github.com/prometheus/common v0.55.0
1131
-## explicit; go 1.20
1131
+# github.com/prometheus/common v0.62.0
1132
+## explicit; go 1.21
1132 1133
 github.com/prometheus/common/expfmt
1133 1134
 github.com/prometheus/common/model
1134 1135
 # github.com/prometheus/procfs v0.15.1