@ -14,13 +14,45 @@
package expfmt
import (
"bytes"
"fmt"
"io"
"math"
"strconv"
"strings"
"sync"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
)
// enhancedWriter has all the enhanced write functions needed here. bytes.Buffer
// implements it.
type enhancedWriter interface {
io . Writer
WriteRune ( r rune ) ( n int , err error )
WriteString ( s string ) ( n int , err error )
WriteByte ( c byte ) error
}
const (
initialBufSize = 512
initialNumBufSize = 24
)
var (
bufPool = sync . Pool {
New : func ( ) interface { } {
return bytes . NewBuffer ( make ( [ ] byte , 0 , initialBufSize ) )
} ,
}
numBufPool = sync . Pool {
New : func ( ) interface { } {
b := make ( [ ] byte , 0 , initialNumBufSize )
return & b
} ,
}
)
// MetricFamilyToText converts a MetricFamily proto message into text format and
@ -32,37 +64,92 @@ import (
// will result in invalid text format output.
//
// This method fulfills the type 'prometheus.encoder'.
func MetricFamilyToText ( out io . Writer , in * dto . MetricFamily ) ( int , error ) {
var written int
func MetricFamilyToText ( out io . Writer , in * dto . MetricFamily ) ( written int , err error ) {
// Fail-fast checks.
if len ( in . Metric ) == 0 {
return written , fmt . Errorf ( "MetricFamily has no metrics: %s" , in )
return 0 , fmt . Errorf ( "MetricFamily has no metrics: %s" , in )
}
name := in . GetName ( )
if name == "" {
return written , fmt . Errorf ( "MetricFamily has no name: %s" , in )
return 0 , fmt . Errorf ( "MetricFamily has no name: %s" , in )
}
// Try the interface upgrade. If it doesn't work, we'll use a
// bytes.Buffer from the sync.Pool and write out its content to out in a
// single go in the end.
w , ok := out . ( enhancedWriter )
if ! ok {
b := bufPool . Get ( ) . ( * bytes . Buffer )
b . Reset ( )
w = b
defer func ( ) {
bWritten , bErr := out . Write ( b . Bytes ( ) )
written = bWritten
if err == nil {
err = bErr
}
bufPool . Put ( b )
} ( )
}
var n int
// Comments, first HELP, then TYPE.
if in . Help != nil {
n , err := fmt . Fprintf (
out , "# HELP %s %s\n" ,
name , escapeString ( * in . Help , false ) ,
)
n , err = w . WriteString ( "# HELP " )
written += n
if err != nil {
return written , err
return
}
n , err = w . WriteString ( name )
written += n
if err != nil {
return
}
err = w . WriteByte ( ' ' )
written ++
if err != nil {
return
}
n , err = writeEscapedString ( w , * in . Help , false )
written += n
if err != nil {
return
}
err = w . WriteByte ( '\n' )
written ++
if err != nil {
return
}
}
n , err = w . WriteString ( "# TYPE " )
written += n
if err != nil {
return
}
n , err = w . WriteString ( name )
written += n
if err != nil {
return
}
metricType := in . GetType ( )
n , err := fmt . Fprintf (
out , "# TYPE %s %s\n" ,
name , strings . ToLower ( metricType . String ( ) ) ,
)
switch metricType {
case dto . MetricType_COUNTER :
n , err = w . WriteString ( " counter\n" )
case dto . MetricType_GAUGE :
n , err = w . WriteString ( " gauge\n" )
case dto . MetricType_SUMMARY :
n , err = w . WriteString ( " summary\n" )
case dto . MetricType_UNTYPED :
n , err = w . WriteString ( " untyped\n" )
case dto . MetricType_HISTOGRAM :
n , err = w . WriteString ( " histogram\n" )
default :
return written , fmt . Errorf ( "unknown metric type %s" , metricType . String ( ) )
}
written += n
if err != nil {
return written , err
return
}
// Finally the samples, one line for each.
@ -75,9 +162,8 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
)
}
n , err = writeSample (
name , metric , "" , "" ,
w , name , "" , metric , "" , 0 ,
metric . Counter . GetValue ( ) ,
out ,
)
case dto . MetricType_GAUGE :
if metric . Gauge == nil {
@ -86,9 +172,8 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
)
}
n , err = writeSample (
name , metric , "" , "" ,
w , name , "" , metric , "" , 0 ,
metric . Gauge . GetValue ( ) ,
out ,
)
case dto . MetricType_UNTYPED :
if metric . Untyped == nil {
@ -97,9 +182,8 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
)
}
n , err = writeSample (
name , metric , "" , "" ,
w , name , "" , metric , "" , 0 ,
metric . Untyped . GetValue ( ) ,
out ,
)
case dto . MetricType_SUMMARY :
if metric . Summary == nil {
@ -109,29 +193,26 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
}
for _ , q := range metric . Summary . Quantile {
n , err = writeSample (
name , metric ,
model . QuantileLabel , fmt . Sprint ( q . GetQuantile ( ) ) ,
w , name , "" , metric ,
model . QuantileLabel , q . GetQuantile ( ) ,
q . GetValue ( ) ,
out ,
)
written += n
if err != nil {
return written , err
return
}
}
n , err = writeSample (
name + "_sum" , metric , "" , "" ,
w , name , "_sum" , metric , "" , 0 ,
metric . Summary . GetSampleSum ( ) ,
out ,
)
written += n
if err != nil {
return written , err
return
}
written += n
n , err = writeSample (
name + "_count" , metric , "" , "" ,
w , name , "_count" , metric , "" , 0 ,
float64 ( metric . Summary . GetSampleCount ( ) ) ,
out ,
)
case dto . MetricType_HISTOGRAM :
if metric . Histogram == nil {
@ -140,46 +221,42 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
)
}
infSeen := false
for _ , q := range metric . Histogram . Bucket {
for _ , b := range metric . Histogram . Bucket {
n , err = writeSample (
name + "_bucket" , metric ,
model . BucketLabel , fmt . Sprint ( q . GetUpperBound ( ) ) ,
float64 ( q . GetCumulativeCount ( ) ) ,
out ,
w , name , "_bucket" , metric ,
model . BucketLabel , b . GetUpperBound ( ) ,
float64 ( b . GetCumulativeCount ( ) ) ,
)
written += n
if err != nil {
return written , err
return
}
if math . IsInf ( q . GetUpperBound ( ) , + 1 ) {
if math . IsInf ( b . GetUpperBound ( ) , + 1 ) {
infSeen = true
}
}
if ! infSeen {
n , err = writeSample (
name + "_bucket" , metric ,
model . BucketLabel , "+Inf" ,
w , name , "_bucket" , metric ,
model . BucketLabel , math . Inf ( + 1 ) ,
float64 ( metric . Histogram . GetSampleCount ( ) ) ,
out ,
)
written += n
if err != nil {
return written , err
return
}
written += n
}
n , err = writeSample (
name + "_sum" , metric , "" , "" ,
w , name , "_sum" , metric , "" , 0 ,
metric . Histogram . GetSampleSum ( ) ,
out ,
)
written += n
if err != nil {
return written , err
return
}
written += n
n , err = writeSample (
name + "_count" , metric , "" , "" ,
w , name , "_count" , metric , "" , 0 ,
float64 ( metric . Histogram . GetSampleCount ( ) ) ,
out ,
)
default :
return written , fmt . Errorf (
@ -188,116 +265,204 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
}
written += n
if err != nil {
return written , err
return
}
}
return written , nil
return
}
// writeSample writes a single sample in text format to out , given the metric
// writeSample writes a single sample in text format to w , given the metric
// name, the metric proto message itself, optionally an additional label name
// and value (use empty strings if not required), and the value. The function
// returns the number of bytes written and any error encountered.
// with a float64 value (use empty string as label name if not required), and
// the value. The function returns the number of bytes written and any error
// encountered.
func writeSample (
name string ,
w enhancedWriter ,
name , suffix string ,
metric * dto . Metric ,
additionalLabelName , additionalLabelValue string ,
additionalLabelName string , additionalLabelValue float64 ,
value float64 ,
out io . Writer ,
) ( int , error ) {
var written int
n , err := fmt . Fprint ( out , name )
n , err := w . WriteString ( name )
written += n
if err != nil {
return written , err
}
n , err = labelPairsToText (
metric . Label ,
additionalLabelName , additionalLabelValue ,
out ,
if suffix != "" {
n , err = w . WriteString ( suffix )
written += n
if err != nil {
return written , err
}
}
n , err = writeLabelPairs (
w , metric . Label , additionalLabelName , additionalLabelValue ,
)
written += n
if err != nil {
return written , err
}
n , err = fmt . Fprintf ( out , " %v" , value )
err = w . WriteByte ( ' ' )
written ++
if err != nil {
return written , err
}
n , err = writeFloat ( w , value )
written += n
if err != nil {
return written , err
}
if metric . TimestampMs != nil {
n , err = fmt . Fprintf ( out , " %v" , * metric . TimestampMs )
err = w . WriteByte ( ' ' )
written ++
if err != nil {
return written , err
}
n , err = writeInt ( w , * metric . TimestampMs )
written += n
if err != nil {
return written , err
}
}
n , err = out . Write ( [ ] byte { '\n' } )
written += n
err = w . WriteByte ( '\n' )
written ++
if err != nil {
return written , err
}
return written , nil
}
// labelPairsToText converts a slice of LabelPair proto messages plus the
// writeLabelPairs converts a slice of LabelPair proto messages plus the
// explicitly given additional label pair into text formatted as required by the
// text format and writes it to 'out'. An empty slice in combination with an
// empty string 'additionalLabelName' results in nothing being
// written. Otherwise, the label pairs are written, escaped as required by the
// text format, and enclosed in '{...}'. The function returns the number of
// bytes written and any error encountered.
func labelPairsToText (
// text format and writes it to 'w'. An empty slice in combination with an empty
// string 'additionalLabelName' results in nothing being written. Otherwise, the
// label pairs are written, escaped as required by the text format, and enclosed
// in '{...}'. The function returns the number of bytes written and any error
// encountered.
func writeLabelPairs (
w enhancedWriter ,
in [ ] * dto . LabelPair ,
additionalLabelName , additionalLabelValue string ,
out io . Writer ,
additionalLabelName string , additionalLabelValue float64 ,
) ( int , error ) {
if len ( in ) == 0 && additionalLabelName == "" {
return 0 , nil
}
var written int
separator := '{'
var (
written int
separator byte = '{'
)
for _ , lp := range in {
n , err := fmt . Fprintf (
out , ` %c%s="%s" ` ,
separator , lp . GetName ( ) , escapeString ( lp . GetValue ( ) , true ) ,
)
err := w . WriteByte ( separator )
written ++
if err != nil {
return written , err
}
n , err := w . WriteString ( lp . GetName ( ) )
written += n
if err != nil {
return written , err
}
n , err = w . WriteString ( ` =" ` )
written += n
if err != nil {
return written , err
}
n , err = writeEscapedString ( w , lp . GetValue ( ) , true )
written += n
if err != nil {
return written , err
}
err = w . WriteByte ( '"' )
written ++
if err != nil {
return written , err
}
separator = ','
}
if additionalLabelName != "" {
n , err := fmt . Fprintf (
out , ` %c%s="%s" ` ,
separator , additionalLabelName ,
escapeString ( additionalLabelValue , true ) ,
)
err := w . WriteByte ( separator )
written ++
if err != nil {
return written , err
}
n , err := w . WriteString ( additionalLabelName )
written += n
if err != nil {
return written , err
}
n , err = w . WriteString ( ` =" ` )
written += n
if err != nil {
return written , err
}
n , err = writeFloat ( w , additionalLabelValue )
written += n
if err != nil {
return written , err
}
err = w . WriteByte ( '"' )
written ++
if err != nil {
return written , err
}
}
n , err := out . Write ( [ ] byte { '}' } )
written += n
err := w . WriteByte ( '}' )
written ++
if err != nil {
return written , err
}
return written , nil
}
// writeEscapedString replaces '\' by '\\', new line character by '\n', and - if
// includeDoubleQuote is true - '"' by '\"'.
var (
escape = strings . NewReplacer ( "\\" , ` \\ ` , "\n" , ` \n ` )
escapeWithDoubleQuote = strings . NewReplacer ( "\\" , ` \\ ` , "\n" , ` \n ` , "\"" , ` \" ` )
escaper = strings . NewReplacer ( "\\" , ` \\ ` , "\n" , ` \n ` )
quotedEscaper = strings . NewReplacer ( "\\" , ` \\ ` , "\n" , ` \n ` , "\"" , ` \" ` )
)
// escapeString replaces '\' by '\\', new line character by '\n', and - if
// includeDoubleQuote is true - '"' by '\"'.
func escapeString ( v string , includeDoubleQuote bool ) string {
func writeEscapedString ( w enhancedWriter , v string , includeDoubleQuote bool ) ( int , error ) {
if includeDoubleQuote {
return escapeWithDoubleQuote . Replace ( v )
return quotedEscaper . WriteString ( w , v )
} else {
return escaper . WriteString ( w , v )
}
}
// writeFloat is equivalent to fmt.Fprint with a float64 argument but hardcodes
// a few common cases for increased efficiency. For non-hardcoded cases, it uses
// strconv.AppendFloat to avoid allocations, similar to writeInt.
func writeFloat ( w enhancedWriter , f float64 ) ( int , error ) {
switch {
case f == 1 :
return 1 , w . WriteByte ( '1' )
case f == 0 :
return 1 , w . WriteByte ( '0' )
case f == - 1 :
return w . WriteString ( "-1" )
case math . IsNaN ( f ) :
return w . WriteString ( "NaN" )
case math . IsInf ( f , + 1 ) :
return w . WriteString ( "+Inf" )
case math . IsInf ( f , - 1 ) :
return w . WriteString ( "-Inf" )
default :
bp := numBufPool . Get ( ) . ( * [ ] byte )
* bp = strconv . AppendFloat ( ( * bp ) [ : 0 ] , f , 'g' , - 1 , 64 )
written , err := w . Write ( * bp )
numBufPool . Put ( bp )
return written , err
}
}
return escape . Replace ( v )
// writeInt is equivalent to fmt.Fprint with an int64 argument but uses
// strconv.AppendInt with a byte slice taken from a sync.Pool to avoid
// allocations.
func writeInt ( w enhancedWriter , i int64 ) ( int , error ) {
bp := numBufPool . Get ( ) . ( * [ ] byte )
* bp = strconv . AppendInt ( ( * bp ) [ : 0 ] , i , 10 )
written , err := w . Write ( * bp )
numBufPool . Put ( bp )
return written , err
}