1
2
3
4
5
6
7
8
9
10
11
12
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
223 {
224 metric: metric1,
225 format: fmtProtoDelim,
226 },
227
228 {
229 metric: metric1,
230 format: fmtProtoCompact,
231 },
232
233 {
234 metric: metric1,
235 format: fmtProtoText,
236 },
237
238 {
239 metric: metric1,
240 format: fmtText,
241 expOut: `# TYPE foo_metric untyped
242 foo_metric 1.234
243 `,
244 },
245
246 {
247 metric: metric1,
248 format: fmtOpenMetrics_0_0_1,
249 expOut: `# TYPE foo_metric unknown
250 foo_metric 1.234
251 `,
252 },
253
254 {
255 metric: metric1,
256 format: fmtOpenMetrics_1_0_0,
257 expOut: `# TYPE foo_metric unknown
258 foo_metric 1.234
259 `,
260 },
261
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
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