// 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 expfmt

import (
	
	
	
	
	
	
	

	

	

	dto 
)

type encoderOption struct {
	withCreatedLines bool
	withUnit         bool
}

type EncoderOption func(*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 {
	return func( *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 {
	return func( *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  == "" {
		return 0, 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()
		 = 
		defer func() {
			 := .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  {
	case dto.MetricType_COUNTER:
		if strings.HasSuffix(, "_total") {
			,  = .WriteString(" counter\n")
		} else {
			,  = .WriteString(" unknown\n")
		}
	case dto.MetricType_GAUGE:
		,  = .WriteString(" gauge\n")
	case dto.MetricType_SUMMARY:
		,  = .WriteString(" summary\n")
	case dto.MetricType_UNTYPED:
		,  = .WriteString(" unknown\n")
	case dto.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
		}
	}

	var  int

	// Finally the samples, one line for each.
	if  == dto.MetricType_COUNTER && strings.HasSuffix(, "_total") {
		 =  + "_total"
	}
	for ,  := range .Metric {
		switch  {
		case dto.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())
				 += 
			}
		case dto.MetricType_GAUGE:
			if .Gauge == nil {
				return , fmt.Errorf(
					"expected gauge in metric %s %s", , ,
				)
			}
			,  = writeOpenMetricsSample(
				, , "", , "", 0,
				.Gauge.GetValue(), 0, false,
				nil,
			)
		case dto.MetricType_UNTYPED:
			if .Untyped == nil {
				return , fmt.Errorf(
					"expected untyped in metric %s %s", , ,
				)
			}
			,  = writeOpenMetricsSample(
				, , "", , "", 0,
				.Untyped.GetValue(), 0, false,
				nil,
			)
		case dto.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())
				 += 
			}
		case dto.MetricType_HISTOGRAM:
			if .Histogram == nil {
				return , fmt.Errorf(
					"expected histogram in metric %s %s", , ,
				)
			}
			 := false
			for ,  := range .Histogram.Bucket {
				,  = writeOpenMetricsSample(
					, , "_bucket", ,
					model.BucketLabel, .GetUpperBound(),
					0, .GetCumulativeCount(), true,
					.Exemplar,
				)
				 += 
				if  != nil {
					return
				}
				if math.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 (
		            int
		          byte = '{'
		      = 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 , 
		}
	}

	if len() == 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")
	case math.IsNaN():
		return .WriteString("NaN")
	case math.IsInf(, +1):
		return .WriteString("+Inf")
	case math.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 , 
}