// Copyright 2020 The Prometheus Authors// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.package expfmtimport (dto)type encoderOption struct { withCreatedLines bool withUnit bool}typeEncoderOptionfunc(*encoderOption)// WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder// to include _created lines (See// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1).// Created timestamps can improve the accuracy of series reset detection, but// come with a bandwidth cost.//// At the time of writing, created timestamp ingestion is still experimental in// Prometheus and need to be enabled with the feature-flag// `--feature-flag=created-timestamp-zero-ingestion`, and breaking changes are// still possible. Therefore, it is recommended to use this feature with caution.func () EncoderOption {returnfunc( *encoderOption) { .withCreatedLines = true }}// WithUnit is an EncoderOption enabling a set unit to be written to the output// and to be added to the metric name, if it's not there already, as a suffix.// Without opting in this way, the unit will not be added to the metric name and,// on top of that, the unit will not be passed onto the output, even if it// were declared in the *dto.MetricFamily struct, i.e. even if in.Unit !=nil.func () EncoderOption {returnfunc( *encoderOption) { .withUnit = true }}// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the// OpenMetrics text format and writes the resulting lines to 'out'. It returns// the number of bytes written and any error encountered. The output will have// the same order as the input, no further sorting is performed. Furthermore,// this function assumes the input is already sanitized and does not perform any// sanity checks. If the input contains duplicate metrics or invalid metric or// label names, the conversion will result in invalid text format output.//// If metric names conform to the legacy validation pattern, they will be placed// outside the brackets in the traditional way, like `foo{}`. If the metric name// fails the legacy validation check, it will be placed quoted inside the// brackets: `{"foo"}`. As stated above, the input is assumed to be santized and// no error will be thrown in this case.//// Similar to metric names, if label names conform to the legacy validation// pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label// name fails the legacy validation check, it will be quoted:// `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and// no error will be thrown in this case.//// This function fulfills the type 'expfmt.encoder'.//// Note that OpenMetrics requires a final `# EOF` line. Since this function acts// on individual metric families, it is the responsibility of the caller to// append this line to 'out' once all metric families have been written.// Conveniently, this can be done by calling FinalizeOpenMetrics.//// The output should be fully OpenMetrics compliant. However, there are a few// missing features and peculiarities to avoid complications when switching from// Prometheus to OpenMetrics or vice versa://// - Counters are expected to have the `_total` suffix in their metric name. In// the output, the suffix will be truncated from the `# TYPE`, `# HELP` and `# UNIT`// lines. A counter with a missing `_total` suffix is not an error. However,// its type will be set to `unknown` in that case to avoid invalid OpenMetrics// output.//// - According to the OM specs, the `# UNIT` line is optional, but if populated,// the unit has to be present in the metric name as its suffix:// (see https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#unit).// However, in order to accommodate any potential scenario where such a change in the// metric name is not desirable, the users are here given the choice of either explicitly// opt in, in case they wish for the unit to be included in the output AND in the metric name// as a suffix (see the description of the WithUnit function above),// or not to opt in, in case they don't want for any of that to happen.//// - No support for the following (optional) features: info type,// stateset type, gaugehistogram type.//// - The size of exemplar labels is not checked (i.e. it's possible to create// exemplars that are larger than allowed by the OpenMetrics specification).//// - The value of Counters is not checked. (OpenMetrics doesn't allow counters// with a `NaN` value.)func ( io.Writer, *dto.MetricFamily, ...EncoderOption) ( int, error) { := encoderOption{}for , := range { (&) } := .GetName()if == "" {return0, fmt.Errorf("MetricFamily has no name: %s", ) }// Try the interface upgrade. If it doesn't work, we'll use a // bufio.Writer from the sync.Pool. , := .(enhancedWriter)if ! { := bufPool.Get().(*bufio.Writer) .Reset() = deferfunc() { := .Flush()if == nil { = }bufPool.Put() }() }var (int = .GetType() = )if == dto.MetricType_COUNTER && strings.HasSuffix(, "_total") { = [:len()-6] }if .withUnit && .Unit != nil && !strings.HasSuffix(, "_"+*.Unit) { = + "_" + *.Unit }// Comments, first HELP, then TYPE.if .Help != nil { , = .WriteString("# HELP ") += if != nil {return } , = writeName(, ) += if != nil {return } = .WriteByte(' ') ++if != nil {return } , = writeEscapedString(, *.Help, true) += if != nil {return } = .WriteByte('\n') ++if != nil {return } } , = .WriteString("# TYPE ") += if != nil {return } , = writeName(, ) += if != nil {return }switch {casedto.MetricType_COUNTER:ifstrings.HasSuffix(, "_total") { , = .WriteString(" counter\n") } else { , = .WriteString(" unknown\n") }casedto.MetricType_GAUGE: , = .WriteString(" gauge\n")casedto.MetricType_SUMMARY: , = .WriteString(" summary\n")casedto.MetricType_UNTYPED: , = .WriteString(" unknown\n")casedto.MetricType_HISTOGRAM: , = .WriteString(" histogram\n")default:return , fmt.Errorf("unknown metric type %s", .String()) } += if != nil {return }if .withUnit && .Unit != nil { , = .WriteString("# UNIT ") += if != nil {return } , = writeName(, ) += if != nil {return } = .WriteByte(' ') ++if != nil {return } , = writeEscapedString(, *.Unit, true) += if != nil {return } = .WriteByte('\n') ++if != nil {return } }varint// Finally the samples, one line for each.if == dto.MetricType_COUNTER && strings.HasSuffix(, "_total") { = + "_total" }for , := range .Metric {switch {casedto.MetricType_COUNTER:if .Counter == nil {return , fmt.Errorf("expected counter in metric %s %s", , , ) } , = writeOpenMetricsSample( , , "", , "", 0, .Counter.GetValue(), 0, false, .Counter.Exemplar, )if .withCreatedLines && .Counter.CreatedTimestamp != nil { , = writeOpenMetricsCreated(, , "_total", , "", 0, .Counter.GetCreatedTimestamp()) += }casedto.MetricType_GAUGE:if .Gauge == nil {return , fmt.Errorf("expected gauge in metric %s %s", , , ) } , = writeOpenMetricsSample( , , "", , "", 0, .Gauge.GetValue(), 0, false,nil, )casedto.MetricType_UNTYPED:if .Untyped == nil {return , fmt.Errorf("expected untyped in metric %s %s", , , ) } , = writeOpenMetricsSample( , , "", , "", 0, .Untyped.GetValue(), 0, false,nil, )casedto.MetricType_SUMMARY:if .Summary == nil {return , fmt.Errorf("expected summary in metric %s %s", , , ) }for , := range .Summary.Quantile { , = writeOpenMetricsSample( , , "", ,model.QuantileLabel, .GetQuantile(), .GetValue(), 0, false,nil, ) += if != nil {return } } , = writeOpenMetricsSample( , , "_sum", , "", 0, .Summary.GetSampleSum(), 0, false,nil, ) += if != nil {return } , = writeOpenMetricsSample( , , "_count", , "", 0,0, .Summary.GetSampleCount(), true,nil, )if .withCreatedLines && .Summary.CreatedTimestamp != nil { , = writeOpenMetricsCreated(, , "", , "", 0, .Summary.GetCreatedTimestamp()) += }casedto.MetricType_HISTOGRAM:if .Histogram == nil {return , fmt.Errorf("expected histogram in metric %s %s", , , ) } := falsefor , := range .Histogram.Bucket { , = writeOpenMetricsSample( , , "_bucket", ,model.BucketLabel, .GetUpperBound(),0, .GetCumulativeCount(), true, .Exemplar, ) += if != nil {return }ifmath.IsInf(.GetUpperBound(), +1) { = true } }if ! { , = writeOpenMetricsSample( , , "_bucket", ,model.BucketLabel, math.Inf(+1),0, .Histogram.GetSampleCount(), true,nil, ) += if != nil {return } } , = writeOpenMetricsSample( , , "_sum", , "", 0, .Histogram.GetSampleSum(), 0, false,nil, ) += if != nil {return } , = writeOpenMetricsSample( , , "_count", , "", 0,0, .Histogram.GetSampleCount(), true,nil, )if .withCreatedLines && .Histogram.CreatedTimestamp != nil { , = writeOpenMetricsCreated(, , "", , "", 0, .Histogram.GetCreatedTimestamp()) += }default:return , fmt.Errorf("unexpected type in metric %s %s", , , ) } += if != nil {return } }return}// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics.func ( io.Writer) ( int, error) {return .Write([]byte("# EOF\n"))}// writeOpenMetricsSample writes a single sample in OpenMetrics text format to// w, given the metric name, the metric proto message itself, optionally an// additional label name with a float64 value (use empty string as label name if// not required), the value (optionally as float64 or uint64, determined by// useIntValue), and optionally an exemplar (use nil if not required). The// function returns the number of bytes written and any error encountered.func writeOpenMetricsSample(enhancedWriter, , string, *dto.Metric,string, float64,float64, uint64, bool, *dto.Exemplar,) (int, error) { := 0 , := writeOpenMetricsNameAndLabelPairs( , +, .Label, , , ) += if != nil {return , } = .WriteByte(' ') ++if != nil {return , }if { , = writeUint(, ) } else { , = writeOpenMetricsFloat(, ) } += if != nil {return , }if .TimestampMs != nil { = .WriteByte(' ') ++if != nil {return , }// TODO(beorn7): Format this directly without converting to a float first. , = writeOpenMetricsFloat(, float64(*.TimestampMs)/1000) += if != nil {return , } }if != nil && len(.Label) > 0 { , = writeExemplar(, ) += if != nil {return , } } = .WriteByte('\n') ++if != nil {return , }return , nil}// writeOpenMetricsNameAndLabelPairs works like writeOpenMetricsSample but// formats the float in OpenMetrics style.func writeOpenMetricsNameAndLabelPairs(enhancedWriter,string, []*dto.LabelPair,string, float64,) (int, error) {var (intbyte = '{' = false )if != "" {// If the name does not pass the legacy validity check, we must put the // metric name inside the braces, quoted.if !model.IsValidLegacyMetricName() { = true := .WriteByte() ++if != nil {return , } = ',' } , := writeName(, ) += if != nil {return , } }iflen() == 0 && == "" {if { := .WriteByte('}') ++if != nil {return , } }return , nil }for , := range { := .WriteByte() ++if != nil {return , } , := writeName(, .GetName()) += if != nil {return , } , = .WriteString(`="`) += if != nil {return , } , = writeEscapedString(, .GetValue(), true) += if != nil {return , } = .WriteByte('"') ++if != nil {return , } = ',' }if != "" { := .WriteByte() ++if != nil {return , } , := .WriteString() += if != nil {return , } , = .WriteString(`="`) += if != nil {return , } , = writeOpenMetricsFloat(, ) += if != nil {return , } = .WriteByte('"') ++if != nil {return , } } := .WriteByte('}') ++if != nil {return , }return , nil}// writeOpenMetricsCreated writes the created timestamp for a single time series// following OpenMetrics text format to w, given the metric name, the metric proto// message itself, optionally a suffix to be removed, e.g. '_total' for counters,// an additional label name with a float64 value (use empty string as label name if// not required) and the timestamp that represents the created timestamp.// The function returns the number of bytes written and any error encountered.func writeOpenMetricsCreated( enhancedWriter, , string, *dto.Metric,string, float64, *timestamppb.Timestamp,) (int, error) { := 0 , := writeOpenMetricsNameAndLabelPairs( , strings.TrimSuffix(, )+"_created", .Label, , , ) += if != nil {return , } = .WriteByte(' ') ++if != nil {return , }// TODO(beorn7): Format this directly from components of ts to // avoid overflow/underflow and precision issues of the float // conversion. , = writeOpenMetricsFloat(, float64(.AsTime().UnixNano())/1e9) += if != nil {return , } = .WriteByte('\n') ++if != nil {return , }return , nil}// writeExemplar writes the provided exemplar in OpenMetrics format to w. The// function returns the number of bytes written and any error encountered.func writeExemplar( enhancedWriter, *dto.Exemplar) (int, error) { := 0 , := .WriteString(" # ") += if != nil {return , } , = writeOpenMetricsNameAndLabelPairs(, "", .Label, "", 0) += if != nil {return , } = .WriteByte(' ') ++if != nil {return , } , = writeOpenMetricsFloat(, .GetValue()) += if != nil {return , }if .Timestamp != nil { = .WriteByte(' ') ++if != nil {return , } = (*).Timestamp.CheckValid()if != nil {return , } := (*).Timestamp.AsTime()// TODO(beorn7): Format this directly from components of ts to // avoid overflow/underflow and precision issues of the float // conversion. , = writeOpenMetricsFloat(, float64(.UnixNano())/1e9) += if != nil {return , } }return , nil}// writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting// number would otherwise contain neither a "." nor an "e".func writeOpenMetricsFloat( enhancedWriter, float64) (int, error) {switch {case == 1:return .WriteString("1.0")case == 0:return .WriteString("0.0")case == -1:return .WriteString("-1.0")casemath.IsNaN():return .WriteString("NaN")casemath.IsInf(, +1):return .WriteString("+Inf")casemath.IsInf(, -1):return .WriteString("-Inf")default: := numBufPool.Get().(*[]byte) * = strconv.AppendFloat((*)[:0], , 'g', -1, 64)if !bytes.ContainsAny(*, "e.") { * = append(*, '.', '0') } , := .Write(*)numBufPool.Put()return , }}// writeUint is like writeInt just for uint64.func writeUint( enhancedWriter, uint64) (int, error) { := numBufPool.Get().(*[]byte) * = strconv.AppendUint((*)[:0], , 10) , := .Write(*)numBufPool.Put()return , }
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.