...

Source file src/github.com/prometheus/common/expfmt/encode_test.go

Documentation: github.com/prometheus/common/expfmt

     1  // Copyright 2018 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 expfmt
    15  
    16  import (
    17  	"bytes"
    18  	"net/http"
    19  	"testing"
    20  
    21  	"google.golang.org/protobuf/proto"
    22  
    23  	"github.com/prometheus/common/model"
    24  
    25  	dto "github.com/prometheus/client_model/go"
    26  )
    27  
    28  func TestNegotiate(t *testing.T) {
    29  	acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily"
    30  	tests := []struct {
    31  		name              string
    32  		acceptHeaderValue string
    33  		expectedFmt       string
    34  	}{
    35  		{
    36  			name:              "delimited format",
    37  			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited",
    38  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
    39  		},
    40  		{
    41  			name:              "text format",
    42  			acceptHeaderValue: acceptValuePrefix + ";encoding=text",
    43  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
    44  		},
    45  		{
    46  			name:              "compact text format",
    47  			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text",
    48  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
    49  		},
    50  		{
    51  			name:              "plain text format",
    52  			acceptHeaderValue: "text/plain;version=0.0.4",
    53  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
    54  		},
    55  		{
    56  			name:              "delimited format utf-8",
    57  			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
    58  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
    59  		},
    60  		{
    61  			name:              "text format utf-8",
    62  			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
    63  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
    64  		},
    65  		{
    66  			name:              "compact text format utf-8",
    67  			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
    68  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
    69  		},
    70  		{
    71  			name:              "plain text format 0.0.4 with utf-8 not valid, falls back",
    72  			acceptHeaderValue: "text/plain;version=0.0.4;",
    73  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
    74  		},
    75  		{
    76  			name:              "plain text format 0.0.4 with utf-8 not valid, falls back",
    77  			acceptHeaderValue: "text/plain;version=0.0.4; escaping=values;",
    78  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
    79  		},
    80  	}
    81  
    82  	oldDefault := model.NameEscapingScheme
    83  	model.NameEscapingScheme = model.UnderscoreEscaping
    84  	defer func() {
    85  		model.NameEscapingScheme = oldDefault
    86  	}()
    87  
    88  	for i, test := range tests {
    89  		t.Run(test.name, func(t *testing.T) {
    90  			h := http.Header{}
    91  			h.Add(hdrAccept, test.acceptHeaderValue)
    92  			actualFmt := string(Negotiate(h))
    93  			if actualFmt != test.expectedFmt {
    94  				t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
    95  			}
    96  		})
    97  	}
    98  }
    99  
   100  func TestNegotiateOpenMetrics(t *testing.T) {
   101  	acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily"
   102  	tests := []struct {
   103  		name              string
   104  		acceptHeaderValue string
   105  		expectedFmt       string
   106  	}{
   107  		{
   108  			name:              "OM format, no version",
   109  			acceptHeaderValue: "application/openmetrics-text",
   110  			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
   111  		},
   112  		{
   113  			name:              "OM format, 0.0.1 version",
   114  			acceptHeaderValue: "application/openmetrics-text;version=0.0.1; escaping=underscores",
   115  			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=underscores",
   116  		},
   117  		{
   118  			name:              "OM format, 1.0.0 version",
   119  			acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
   120  			expectedFmt:       "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
   121  		},
   122  		{
   123  			name:              "OM format, 0.0.1 version with utf-8 is not valid, falls back",
   124  			acceptHeaderValue: "application/openmetrics-text;version=0.0.1",
   125  			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
   126  		},
   127  		{
   128  			name:              "OM format, 1.0.0 version with utf-8 is not valid, falls back",
   129  			acceptHeaderValue: "application/openmetrics-text;version=1.0.0; escaping=values;",
   130  			expectedFmt:       "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
   131  		},
   132  		{
   133  			name:              "OM format, invalid version",
   134  			acceptHeaderValue: "application/openmetrics-text;version=0.0.4",
   135  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
   136  		},
   137  		{
   138  			name:              "compact text format",
   139  			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores",
   140  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
   141  		},
   142  		{
   143  			name:              "plain text format",
   144  			acceptHeaderValue: "text/plain;version=0.0.4",
   145  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
   146  		},
   147  		{
   148  			name:              "plain text format 0.0.4",
   149  			acceptHeaderValue: "text/plain;version=0.0.4; escaping=allow-utf-8",
   150  			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8",
   151  		},
   152  		{
   153  			name:              "delimited format utf-8",
   154  			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
   155  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
   156  		},
   157  		{
   158  			name:              "text format utf-8",
   159  			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
   160  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
   161  		},
   162  		{
   163  			name:              "compact text format utf-8",
   164  			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
   165  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
   166  		},
   167  		{
   168  			name:              "delimited format escaped",
   169  			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=underscores;",
   170  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
   171  		},
   172  		{
   173  			name:              "text format escaped",
   174  			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=underscores;",
   175  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
   176  		},
   177  		{
   178  			name:              "compact text format escaped",
   179  			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores;",
   180  			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
   181  		},
   182  	}
   183  
   184  	oldDefault := model.NameEscapingScheme
   185  	model.NameEscapingScheme = model.ValueEncodingEscaping
   186  	defer func() {
   187  		model.NameEscapingScheme = oldDefault
   188  	}()
   189  
   190  	for i, test := range tests {
   191  		t.Run(test.name, func(t *testing.T) {
   192  			h := http.Header{}
   193  			h.Add(hdrAccept, test.acceptHeaderValue)
   194  			actualFmt := string(NegotiateIncludingOpenMetrics(h))
   195  			if actualFmt != test.expectedFmt {
   196  				t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
   197  			}
   198  		})
   199  	}
   200  }
   201  
   202  func TestEncode(t *testing.T) {
   203  	metric1 := &dto.MetricFamily{
   204  		Name: proto.String("foo_metric"),
   205  		Type: dto.MetricType_UNTYPED.Enum(),
   206  		Unit: proto.String("seconds"),
   207  		Metric: []*dto.Metric{
   208  			{
   209  				Untyped: &dto.Untyped{
   210  					Value: proto.Float64(1.234),
   211  				},
   212  			},
   213  		},
   214  	}
   215  
   216  	scenarios := []struct {
   217  		metric  *dto.MetricFamily
   218  		format  Format
   219  		options []EncoderOption
   220  		expOut  string
   221  	}{
   222  		// 1: Untyped ProtoDelim
   223  		{
   224  			metric: metric1,
   225  			format: fmtProtoDelim,
   226  		},
   227  		// 2: Untyped fmtProtoCompact
   228  		{
   229  			metric: metric1,
   230  			format: fmtProtoCompact,
   231  		},
   232  		// 3: Untyped fmtProtoText
   233  		{
   234  			metric: metric1,
   235  			format: fmtProtoText,
   236  		},
   237  		// 4: Untyped fmtText
   238  		{
   239  			metric: metric1,
   240  			format: fmtText,
   241  			expOut: `# TYPE foo_metric untyped
   242  foo_metric 1.234
   243  `,
   244  		},
   245  		// 5: Untyped fmtOpenMetrics_0_0_1
   246  		{
   247  			metric: metric1,
   248  			format: fmtOpenMetrics_0_0_1,
   249  			expOut: `# TYPE foo_metric unknown
   250  foo_metric 1.234
   251  `,
   252  		},
   253  		// 6: Untyped fmtOpenMetrics_1_0_0
   254  		{
   255  			metric: metric1,
   256  			format: fmtOpenMetrics_1_0_0,
   257  			expOut: `# TYPE foo_metric unknown
   258  foo_metric 1.234
   259  `,
   260  		},
   261  		// 7: Simple Counter fmtOpenMetrics_0_0_1 unit opted in
   262  		{
   263  			metric:  metric1,
   264  			format:  fmtOpenMetrics_0_0_1,
   265  			options: []EncoderOption{WithUnit()},
   266  			expOut: `# TYPE foo_metric_seconds unknown
   267  # UNIT foo_metric_seconds seconds
   268  foo_metric_seconds 1.234
   269  `,
   270  		},
   271  		// 8: Simple Counter fmtOpenMetrics_1_0_0 unit opted out
   272  		{
   273  			metric: metric1,
   274  			format: fmtOpenMetrics_1_0_0,
   275  			expOut: `# TYPE foo_metric unknown
   276  foo_metric 1.234
   277  `,
   278  		},
   279  	}
   280  	for i, scenario := range scenarios {
   281  		out := bytes.NewBuffer(make([]byte, 0, len(scenario.expOut)))
   282  		enc := NewEncoder(out, scenario.format, scenario.options...)
   283  		err := enc.Encode(scenario.metric)
   284  		if err != nil {
   285  			t.Errorf("%d. error: %s", i, err)
   286  			continue
   287  		}
   288  
   289  		if expected, got := len(scenario.expOut), len(out.Bytes()); expected != 0 && expected != got {
   290  			t.Errorf(
   291  				"%d. expected %d bytes written, got %d",
   292  				i, expected, got,
   293  			)
   294  		}
   295  		if expected, got := scenario.expOut, out.String(); expected != "" && expected != got {
   296  			t.Errorf(
   297  				"%d. expected out=%q, got %q",
   298  				i, expected, got,
   299  			)
   300  		}
   301  
   302  		if len(out.Bytes()) == 0 {
   303  			t.Errorf(
   304  				"%d. expected output not to be empty",
   305  				i,
   306  			)
   307  		}
   308  	}
   309  }
   310  
   311  func TestEscapedEncode(t *testing.T) {
   312  	var buff bytes.Buffer
   313  	delimEncoder := NewEncoder(&buff, fmtProtoDelim+"; escaping=underscores")
   314  	metric := &dto.MetricFamily{
   315  		Name: proto.String("foo.metric"),
   316  		Type: dto.MetricType_UNTYPED.Enum(),
   317  		Metric: []*dto.Metric{
   318  			{
   319  				Untyped: &dto.Untyped{
   320  					Value: proto.Float64(1.234),
   321  				},
   322  			},
   323  			{
   324  				Label: []*dto.LabelPair{
   325  					{
   326  						Name:  proto.String("dotted.label.name"),
   327  						Value: proto.String("my.label.value"),
   328  					},
   329  				},
   330  				Untyped: &dto.Untyped{
   331  					Value: proto.Float64(8),
   332  				},
   333  			},
   334  		},
   335  	}
   336  
   337  	err := delimEncoder.Encode(metric)
   338  	if err != nil {
   339  		t.Errorf("unexpected error during encode: %s", err.Error())
   340  	}
   341  
   342  	out := buff.Bytes()
   343  	if len(out) == 0 {
   344  		t.Errorf("expected the output bytes buffer to be non-empty")
   345  	}
   346  
   347  	buff.Reset()
   348  
   349  	compactEncoder := NewEncoder(&buff, fmtProtoCompact)
   350  	err = compactEncoder.Encode(metric)
   351  	if err != nil {
   352  		t.Errorf("unexpected error during encode: %s", err.Error())
   353  	}
   354  
   355  	out = buff.Bytes()
   356  	if len(out) == 0 {
   357  		t.Errorf("expected the output bytes buffer to be non-empty")
   358  	}
   359  
   360  	buff.Reset()
   361  
   362  	protoTextEncoder := NewEncoder(&buff, fmtProtoText)
   363  	err = protoTextEncoder.Encode(metric)
   364  	if err != nil {
   365  		t.Errorf("unexpected error during encode: %s", err.Error())
   366  	}
   367  
   368  	out = buff.Bytes()
   369  	if len(out) == 0 {
   370  		t.Errorf("expected the output bytes buffer to be non-empty")
   371  	}
   372  
   373  	buff.Reset()
   374  
   375  	textEncoder := NewEncoder(&buff, fmtText)
   376  	err = textEncoder.Encode(metric)
   377  	if err != nil {
   378  		t.Errorf("unexpected error during encode: %s", err.Error())
   379  	}
   380  
   381  	out = buff.Bytes()
   382  	if len(out) == 0 {
   383  		t.Errorf("expected the output bytes buffer to be non-empty")
   384  	}
   385  
   386  	expected := `# TYPE U__foo_2e_metric untyped
   387  U__foo_2e_metric 1.234
   388  U__foo_2e_metric{U__dotted_2e_label_2e_name="my.label.value"} 8
   389  `
   390  
   391  	if string(out) != expected {
   392  		t.Errorf("expected TextEncoder to return %s, but got %s instead", expected, string(out))
   393  	}
   394  }
   395  

View as plain text