...

Source file src/github.com/prometheus/common/model/metric.go

Documentation: github.com/prometheus/common/model

     1  // Copyright 2013 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  package model
    15  
    16  import (
    17  	"fmt"
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  	"unicode/utf8"
    22  
    23  	dto "github.com/prometheus/client_model/go"
    24  	"google.golang.org/protobuf/proto"
    25  )
    26  
    27  var (
    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
    41  )
    42  
    43  // ValidationScheme is a Go enum for determining how metric and label names will
    44  // be validated by this library.
    45  type ValidationScheme int
    46  
    47  const (
    48  	// LegacyValidation is a setting that requirets that metric and label names
    49  	// conform to the original Prometheus character requirements described by
    50  	// MetricNameRE and LabelNameRE.
    51  	LegacyValidation ValidationScheme = iota
    52  
    53  	// UTF8Validation only requires that metric and label names be valid UTF-8
    54  	// strings.
    55  	UTF8Validation
    56  )
    57  
    58  type EscapingScheme int
    59  
    60  const (
    61  	// NoEscaping indicates that a name will not be escaped. Unescaped names that
    62  	// do not conform to the legacy validity check will use a new exposition
    63  	// format syntax that will be officially standardized in future versions.
    64  	NoEscaping EscapingScheme = iota
    65  
    66  	// UnderscoreEscaping replaces all legacy-invalid characters with underscores.
    67  	UnderscoreEscaping
    68  
    69  	// DotsEscaping is similar to UnderscoreEscaping, except that dots are
    70  	// converted to `_dot_` and pre-existing underscores are converted to `__`.
    71  	DotsEscaping
    72  
    73  	// ValueEncodingEscaping prepends the name with `U__` and replaces all invalid
    74  	// characters with the unicode value, surrounded by underscores. Single
    75  	// underscores are replaced with double underscores.
    76  	ValueEncodingEscaping
    77  )
    78  
    79  const (
    80  	// EscapingKey is the key in an Accept or Content-Type header that defines how
    81  	// metric and label names that do not conform to the legacy character
    82  	// requirements should be escaped when being scraped by a legacy prometheus
    83  	// system. If a system does not explicitly pass an escaping parameter in the
    84  	// Accept header, the default NameEscapingScheme will be used.
    85  	EscapingKey = "escaping"
    86  
    87  	// Possible values for Escaping Key:
    88  	AllowUTF8         = "allow-utf-8" // No escaping required.
    89  	EscapeUnderscores = "underscores"
    90  	EscapeDots        = "dots"
    91  	EscapeValues      = "values"
    92  )
    93  
    94  // MetricNameRE is a regular expression matching valid metric
    95  // names. Note that the IsValidMetricName function performs the same
    96  // check but faster than a match with this regular expression.
    97  var MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
    98  
    99  // A Metric is similar to a LabelSet, but the key difference is that a Metric is
   100  // a singleton and refers to one and only one stream of samples.
   101  type Metric LabelSet
   102  
   103  // Equal compares the metrics.
   104  func (m Metric) Equal(o Metric) bool {
   105  	return LabelSet(m).Equal(LabelSet(o))
   106  }
   107  
   108  // Before compares the metrics' underlying label sets.
   109  func (m Metric) Before(o Metric) bool {
   110  	return LabelSet(m).Before(LabelSet(o))
   111  }
   112  
   113  // Clone returns a copy of the Metric.
   114  func (m Metric) Clone() Metric {
   115  	clone := make(Metric, len(m))
   116  	for k, v := range m {
   117  		clone[k] = v
   118  	}
   119  	return clone
   120  }
   121  
   122  func (m Metric) String() string {
   123  	metricName, hasName := m[MetricNameLabel]
   124  	numLabels := len(m) - 1
   125  	if !hasName {
   126  		numLabels = len(m)
   127  	}
   128  	labelStrings := make([]string, 0, numLabels)
   129  	for label, value := range m {
   130  		if label != MetricNameLabel {
   131  			labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
   132  		}
   133  	}
   134  
   135  	switch numLabels {
   136  	case 0:
   137  		if hasName {
   138  			return string(metricName)
   139  		}
   140  		return "{}"
   141  	default:
   142  		sort.Strings(labelStrings)
   143  		return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", "))
   144  	}
   145  }
   146  
   147  // Fingerprint returns a Metric's Fingerprint.
   148  func (m Metric) Fingerprint() Fingerprint {
   149  	return LabelSet(m).Fingerprint()
   150  }
   151  
   152  // FastFingerprint returns a Metric's Fingerprint calculated by a faster hashing
   153  // algorithm, which is, however, more susceptible to hash collisions.
   154  func (m Metric) FastFingerprint() Fingerprint {
   155  	return LabelSet(m).FastFingerprint()
   156  }
   157  
   158  // IsValidMetricName returns true iff name matches the pattern of MetricNameRE
   159  // for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is
   160  // selected.
   161  func IsValidMetricName(n LabelValue) bool {
   162  	switch NameValidationScheme {
   163  	case LegacyValidation:
   164  		return IsValidLegacyMetricName(n)
   165  	case UTF8Validation:
   166  		if len(n) == 0 {
   167  			return false
   168  		}
   169  		return utf8.ValidString(string(n))
   170  	default:
   171  		panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
   172  	}
   173  }
   174  
   175  // IsValidLegacyMetricName is similar to IsValidMetricName but always uses the
   176  // legacy validation scheme regardless of the value of NameValidationScheme.
   177  // This function, however, does not use MetricNameRE for the check but a much
   178  // faster hardcoded implementation.
   179  func IsValidLegacyMetricName(n LabelValue) bool {
   180  	if len(n) == 0 {
   181  		return false
   182  	}
   183  	for i, b := range n {
   184  		if !isValidLegacyRune(b, i) {
   185  			return false
   186  		}
   187  	}
   188  	return true
   189  }
   190  
   191  // EscapeMetricFamily escapes the given metric names and labels with the given
   192  // escaping scheme. Returns a new object that uses the same pointers to fields
   193  // when possible and creates new escaped versions so as not to mutate the
   194  // input.
   195  func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricFamily {
   196  	if v == nil {
   197  		return nil
   198  	}
   199  
   200  	if scheme == NoEscaping {
   201  		return v
   202  	}
   203  
   204  	out := &dto.MetricFamily{
   205  		Help: v.Help,
   206  		Type: v.Type,
   207  		Unit: v.Unit,
   208  	}
   209  
   210  	// If the name is nil, copy as-is, don't try to escape.
   211  	if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) {
   212  		out.Name = v.Name
   213  	} else {
   214  		out.Name = proto.String(EscapeName(v.GetName(), scheme))
   215  	}
   216  	for _, m := range v.Metric {
   217  		if !metricNeedsEscaping(m) {
   218  			out.Metric = append(out.Metric, m)
   219  			continue
   220  		}
   221  
   222  		escaped := &dto.Metric{
   223  			Gauge:       m.Gauge,
   224  			Counter:     m.Counter,
   225  			Summary:     m.Summary,
   226  			Untyped:     m.Untyped,
   227  			Histogram:   m.Histogram,
   228  			TimestampMs: m.TimestampMs,
   229  		}
   230  
   231  		for _, l := range m.Label {
   232  			if l.GetName() == MetricNameLabel {
   233  				if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) {
   234  					escaped.Label = append(escaped.Label, l)
   235  					continue
   236  				}
   237  				escaped.Label = append(escaped.Label, &dto.LabelPair{
   238  					Name:  proto.String(MetricNameLabel),
   239  					Value: proto.String(EscapeName(l.GetValue(), scheme)),
   240  				})
   241  				continue
   242  			}
   243  			if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) {
   244  				escaped.Label = append(escaped.Label, l)
   245  				continue
   246  			}
   247  			escaped.Label = append(escaped.Label, &dto.LabelPair{
   248  				Name:  proto.String(EscapeName(l.GetName(), scheme)),
   249  				Value: l.Value,
   250  			})
   251  		}
   252  		out.Metric = append(out.Metric, escaped)
   253  	}
   254  	return out
   255  }
   256  
   257  func metricNeedsEscaping(m *dto.Metric) bool {
   258  	for _, l := range m.Label {
   259  		if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) {
   260  			return true
   261  		}
   262  		if !IsValidLegacyMetricName(LabelValue(l.GetName())) {
   263  			return true
   264  		}
   265  	}
   266  	return false
   267  }
   268  
   269  const (
   270  	lowerhex = "0123456789abcdef"
   271  )
   272  
   273  // EscapeName escapes the incoming name according to the provided escaping
   274  // scheme. Depending on the rules of escaping, this may cause no change in the
   275  // string that is returned. (Especially NoEscaping, which by definition is a
   276  // noop). This function does not do any validation of the name.
   277  func EscapeName(name string, scheme EscapingScheme) string {
   278  	if len(name) == 0 {
   279  		return name
   280  	}
   281  	var escaped strings.Builder
   282  	switch scheme {
   283  	case NoEscaping:
   284  		return name
   285  	case UnderscoreEscaping:
   286  		if IsValidLegacyMetricName(LabelValue(name)) {
   287  			return name
   288  		}
   289  		for i, b := range name {
   290  			if isValidLegacyRune(b, i) {
   291  				escaped.WriteRune(b)
   292  			} else {
   293  				escaped.WriteRune('_')
   294  			}
   295  		}
   296  		return escaped.String()
   297  	case DotsEscaping:
   298  		// Do not early return for legacy valid names, we still escape underscores.
   299  		for i, b := range name {
   300  			if b == '_' {
   301  				escaped.WriteString("__")
   302  			} else if b == '.' {
   303  				escaped.WriteString("_dot_")
   304  			} else if isValidLegacyRune(b, i) {
   305  				escaped.WriteRune(b)
   306  			} else {
   307  				escaped.WriteRune('_')
   308  			}
   309  		}
   310  		return escaped.String()
   311  	case ValueEncodingEscaping:
   312  		if IsValidLegacyMetricName(LabelValue(name)) {
   313  			return name
   314  		}
   315  		escaped.WriteString("U__")
   316  		for i, b := range name {
   317  			if isValidLegacyRune(b, i) {
   318  				escaped.WriteRune(b)
   319  			} else if !utf8.ValidRune(b) {
   320  				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 {
   328  				escaped.WriteRune('_')
   329  				for s := 12; s >= 0; s -= 4 {
   330  					escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
   331  				}
   332  				escaped.WriteRune('_')
   333  			}
   334  		}
   335  		return escaped.String()
   336  	default:
   337  		panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
   338  	}
   339  }
   340  
   341  // lower function taken from strconv.atoi
   342  func lower(c byte) byte {
   343  	return c | ('x' - 'X')
   344  }
   345  
   346  // UnescapeName unescapes the incoming name according to the provided escaping
   347  // scheme if possible. Some schemes are partially or totally non-roundtripable.
   348  // If any error is enountered, returns the original input.
   349  func UnescapeName(name string, scheme EscapingScheme) string {
   350  	if len(name) == 0 {
   351  		return name
   352  	}
   353  	switch scheme {
   354  	case NoEscaping:
   355  		return name
   356  	case UnderscoreEscaping:
   357  		// It is not possible to unescape from underscore replacement.
   358  		return name
   359  	case DotsEscaping:
   360  		name = strings.ReplaceAll(name, "_dot_", ".")
   361  		name = strings.ReplaceAll(name, "__", "_")
   362  		return name
   363  	case ValueEncodingEscaping:
   364  		escapedName, found := strings.CutPrefix(name, "U__")
   365  		if !found {
   366  			return name
   367  		}
   368  
   369  		var unescaped strings.Builder
   370  	TOP:
   371  		for i := 0; i < len(escapedName); i++ {
   372  			// All non-underscores are treated normally.
   373  			if escapedName[i] != '_' {
   374  				unescaped.WriteByte(escapedName[i])
   375  				continue
   376  			}
   377  			i++
   378  			if i >= len(escapedName) {
   379  				return name
   380  			}
   381  			// A double underscore is a single underscore.
   382  			if escapedName[i] == '_' {
   383  				unescaped.WriteByte('_')
   384  				continue
   385  			}
   386  			// We think we are in a UTF-8 code, process it.
   387  			var utf8Val uint
   388  			for j := 0; i < len(escapedName); j++ {
   389  				// This is too many characters for a utf8 value.
   390  				if j > 4 {
   391  					return name
   392  				}
   393  				// Found a closing underscore, convert to a rune, check validity, and append.
   394  				if escapedName[i] == '_' {
   395  					utf8Rune := rune(utf8Val)
   396  					if !utf8.ValidRune(utf8Rune) {
   397  						return name
   398  					}
   399  					unescaped.WriteRune(utf8Rune)
   400  					continue TOP
   401  				}
   402  				r := lower(escapedName[i])
   403  				utf8Val *= 16
   404  				if r >= '0' && r <= '9' {
   405  					utf8Val += uint(r) - '0'
   406  				} else if r >= 'a' && r <= 'f' {
   407  					utf8Val += uint(r) - 'a' + 10
   408  				} else {
   409  					return name
   410  				}
   411  				i++
   412  			}
   413  			// Didn't find closing underscore, invalid.
   414  			return name
   415  		}
   416  		return unescaped.String()
   417  	default:
   418  		panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
   419  	}
   420  }
   421  
   422  func isValidLegacyRune(b rune, i int) bool {
   423  	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)
   424  }
   425  
   426  func (e EscapingScheme) String() string {
   427  	switch e {
   428  	case NoEscaping:
   429  		return AllowUTF8
   430  	case UnderscoreEscaping:
   431  		return EscapeUnderscores
   432  	case DotsEscaping:
   433  		return EscapeDots
   434  	case ValueEncodingEscaping:
   435  		return EscapeValues
   436  	default:
   437  		panic(fmt.Sprintf("unknown format scheme %d", e))
   438  	}
   439  }
   440  
   441  func ToEscapingScheme(s string) (EscapingScheme, error) {
   442  	if s == "" {
   443  		return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme")
   444  	}
   445  	switch s {
   446  	case AllowUTF8:
   447  		return NoEscaping, nil
   448  	case EscapeUnderscores:
   449  		return UnderscoreEscaping, nil
   450  	case EscapeDots:
   451  		return DotsEscaping, nil
   452  	case EscapeValues:
   453  		return ValueEncodingEscaping, nil
   454  	default:
   455  		return NoEscaping, fmt.Errorf("unknown format scheme " + s)
   456  	}
   457  }
   458  

View as plain text