1// Copyright 2020 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 14package expfmt 15 16import ( 17 "bufio" 18 "bytes" 19 "fmt" 20 "io" 21 "math" 22 "strconv" 23 "strings" 24 25 "github.com/golang/protobuf/ptypes" 26 "github.com/prometheus/common/model" 27 28 dto "github.com/prometheus/client_model/go" 29) 30 31// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the 32// OpenMetrics text format and writes the resulting lines to 'out'. It returns 33// the number of bytes written and any error encountered. The output will have 34// the same order as the input, no further sorting is performed. Furthermore, 35// this function assumes the input is already sanitized and does not perform any 36// sanity checks. If the input contains duplicate metrics or invalid metric or 37// label names, the conversion will result in invalid text format output. 38// 39// This function fulfills the type 'expfmt.encoder'. 40// 41// Note that OpenMetrics requires a final `# EOF` line. Since this function acts 42// on individual metric families, it is the responsibility of the caller to 43// append this line to 'out' once all metric families have been written. 44// Conveniently, this can be done by calling FinalizeOpenMetrics. 45// 46// The output should be fully OpenMetrics compliant. However, there are a few 47// missing features and peculiarities to avoid complications when switching from 48// Prometheus to OpenMetrics or vice versa: 49// 50// - Counters are expected to have the `_total` suffix in their metric name. In 51// the output, the suffix will be truncated from the `# TYPE` and `# HELP` 52// line. A counter with a missing `_total` suffix is not an error. However, 53// its type will be set to `unknown` in that case to avoid invalid OpenMetrics 54// output. 55// 56// - No support for the following (optional) features: `# UNIT` line, `_created` 57// line, info type, stateset type, gaugehistogram type. 58// 59// - The size of exemplar labels is not checked (i.e. it's possible to create 60// exemplars that are larger than allowed by the OpenMetrics specification). 61// 62// - The value of Counters is not checked. (OpenMetrics doesn't allow counters 63// with a `NaN` value.) 64func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) { 65 name := in.GetName() 66 if name == "" { 67 return 0, fmt.Errorf("MetricFamily has no name: %s", in) 68 } 69 70 // Try the interface upgrade. If it doesn't work, we'll use a 71 // bufio.Writer from the sync.Pool. 72 w, ok := out.(enhancedWriter) 73 if !ok { 74 b := bufPool.Get().(*bufio.Writer) 75 b.Reset(out) 76 w = b 77 defer func() { 78 bErr := b.Flush() 79 if err == nil { 80 err = bErr 81 } 82 bufPool.Put(b) 83 }() 84 } 85 86 var ( 87 n int 88 metricType = in.GetType() 89 shortName = name 90 ) 91 if metricType == dto.MetricType_COUNTER && strings.HasSuffix(shortName, "_total") { 92 shortName = name[:len(name)-6] 93 } 94 95 // Comments, first HELP, then TYPE. 96 if in.Help != nil { 97 n, err = w.WriteString("# HELP ") 98 written += n 99 if err != nil { 100 return 101 } 102 n, err = w.WriteString(shortName) 103 written += n 104 if err != nil { 105 return 106 } 107 err = w.WriteByte(' ') 108 written++ 109 if err != nil { 110 return 111 } 112 n, err = writeEscapedString(w, *in.Help, true) 113 written += n 114 if err != nil { 115 return 116 } 117 err = w.WriteByte('\n') 118 written++ 119 if err != nil { 120 return 121 } 122 } 123 n, err = w.WriteString("# TYPE ") 124 written += n 125 if err != nil { 126 return 127 } 128 n, err = w.WriteString(shortName) 129 written += n 130 if err != nil { 131 return 132 } 133 switch metricType { 134 case dto.MetricType_COUNTER: 135 if strings.HasSuffix(name, "_total") { 136 n, err = w.WriteString(" counter\n") 137 } else { 138 n, err = w.WriteString(" unknown\n") 139 } 140 case dto.MetricType_GAUGE: 141 n, err = w.WriteString(" gauge\n") 142 case dto.MetricType_SUMMARY: 143 n, err = w.WriteString(" summary\n") 144 case dto.MetricType_UNTYPED: 145 n, err = w.WriteString(" unknown\n") 146 case dto.MetricType_HISTOGRAM: 147 n, err = w.WriteString(" histogram\n") 148 default: 149 return written, fmt.Errorf("unknown metric type %s", metricType.String()) 150 } 151 written += n 152 if err != nil { 153 return 154 } 155 156 // Finally the samples, one line for each. 157 for _, metric := range in.Metric { 158 switch metricType { 159 case dto.MetricType_COUNTER: 160 if metric.Counter == nil { 161 return written, fmt.Errorf( 162 "expected counter in metric %s %s", name, metric, 163 ) 164 } 165 // Note that we have ensured above that either the name 166 // ends on `_total` or that the rendered type is 167 // `unknown`. Therefore, no `_total` must be added here. 168 n, err = writeOpenMetricsSample( 169 w, name, "", metric, "", 0, 170 metric.Counter.GetValue(), 0, false, 171 metric.Counter.Exemplar, 172 ) 173 case dto.MetricType_GAUGE: 174 if metric.Gauge == nil { 175 return written, fmt.Errorf( 176 "expected gauge in metric %s %s", name, metric, 177 ) 178 } 179 n, err = writeOpenMetricsSample( 180 w, name, "", metric, "", 0, 181 metric.Gauge.GetValue(), 0, false, 182 nil, 183 ) 184 case dto.MetricType_UNTYPED: 185 if metric.Untyped == nil { 186 return written, fmt.Errorf( 187 "expected untyped in metric %s %s", name, metric, 188 ) 189 } 190 n, err = writeOpenMetricsSample( 191 w, name, "", metric, "", 0, 192 metric.Untyped.GetValue(), 0, false, 193 nil, 194 ) 195 case dto.MetricType_SUMMARY: 196 if metric.Summary == nil { 197 return written, fmt.Errorf( 198 "expected summary in metric %s %s", name, metric, 199 ) 200 } 201 for _, q := range metric.Summary.Quantile { 202 n, err = writeOpenMetricsSample( 203 w, name, "", metric, 204 model.QuantileLabel, q.GetQuantile(), 205 q.GetValue(), 0, false, 206 nil, 207 ) 208 written += n 209 if err != nil { 210 return 211 } 212 } 213 n, err = writeOpenMetricsSample( 214 w, name, "_sum", metric, "", 0, 215 metric.Summary.GetSampleSum(), 0, false, 216 nil, 217 ) 218 written += n 219 if err != nil { 220 return 221 } 222 n, err = writeOpenMetricsSample( 223 w, name, "_count", metric, "", 0, 224 0, metric.Summary.GetSampleCount(), true, 225 nil, 226 ) 227 case dto.MetricType_HISTOGRAM: 228 if metric.Histogram == nil { 229 return written, fmt.Errorf( 230 "expected histogram in metric %s %s", name, metric, 231 ) 232 } 233 infSeen := false 234 for _, b := range metric.Histogram.Bucket { 235 n, err = writeOpenMetricsSample( 236 w, name, "_bucket", metric, 237 model.BucketLabel, b.GetUpperBound(), 238 0, b.GetCumulativeCount(), true, 239 b.Exemplar, 240 ) 241 written += n 242 if err != nil { 243 return 244 } 245 if math.IsInf(b.GetUpperBound(), +1) { 246 infSeen = true 247 } 248 } 249 if !infSeen { 250 n, err = writeOpenMetricsSample( 251 w, name, "_bucket", metric, 252 model.BucketLabel, math.Inf(+1), 253 0, metric.Histogram.GetSampleCount(), true, 254 nil, 255 ) 256 written += n 257 if err != nil { 258 return 259 } 260 } 261 n, err = writeOpenMetricsSample( 262 w, name, "_sum", metric, "", 0, 263 metric.Histogram.GetSampleSum(), 0, false, 264 nil, 265 ) 266 written += n 267 if err != nil { 268 return 269 } 270 n, err = writeOpenMetricsSample( 271 w, name, "_count", metric, "", 0, 272 0, metric.Histogram.GetSampleCount(), true, 273 nil, 274 ) 275 default: 276 return written, fmt.Errorf( 277 "unexpected type in metric %s %s", name, metric, 278 ) 279 } 280 written += n 281 if err != nil { 282 return 283 } 284 } 285 return 286} 287 288// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics. 289func FinalizeOpenMetrics(w io.Writer) (written int, err error) { 290 return w.Write([]byte("# EOF\n")) 291} 292 293// writeOpenMetricsSample writes a single sample in OpenMetrics text format to 294// w, given the metric name, the metric proto message itself, optionally an 295// additional label name with a float64 value (use empty string as label name if 296// not required), the value (optionally as float64 or uint64, determined by 297// useIntValue), and optionally an exemplar (use nil if not required). The 298// function returns the number of bytes written and any error encountered. 299func writeOpenMetricsSample( 300 w enhancedWriter, 301 name, suffix string, 302 metric *dto.Metric, 303 additionalLabelName string, additionalLabelValue float64, 304 floatValue float64, intValue uint64, useIntValue bool, 305 exemplar *dto.Exemplar, 306) (int, error) { 307 var written int 308 n, err := w.WriteString(name) 309 written += n 310 if err != nil { 311 return written, err 312 } 313 if suffix != "" { 314 n, err = w.WriteString(suffix) 315 written += n 316 if err != nil { 317 return written, err 318 } 319 } 320 n, err = writeOpenMetricsLabelPairs( 321 w, metric.Label, additionalLabelName, additionalLabelValue, 322 ) 323 written += n 324 if err != nil { 325 return written, err 326 } 327 err = w.WriteByte(' ') 328 written++ 329 if err != nil { 330 return written, err 331 } 332 if useIntValue { 333 n, err = writeUint(w, intValue) 334 } else { 335 n, err = writeOpenMetricsFloat(w, floatValue) 336 } 337 written += n 338 if err != nil { 339 return written, err 340 } 341 if metric.TimestampMs != nil { 342 err = w.WriteByte(' ') 343 written++ 344 if err != nil { 345 return written, err 346 } 347 // TODO(beorn7): Format this directly without converting to a float first. 348 n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000) 349 written += n 350 if err != nil { 351 return written, err 352 } 353 } 354 if exemplar != nil { 355 n, err = writeExemplar(w, exemplar) 356 written += n 357 if err != nil { 358 return written, err 359 } 360 } 361 err = w.WriteByte('\n') 362 written++ 363 if err != nil { 364 return written, err 365 } 366 return written, nil 367} 368 369// writeOpenMetricsLabelPairs works like writeOpenMetrics but formats the float 370// in OpenMetrics style. 371func writeOpenMetricsLabelPairs( 372 w enhancedWriter, 373 in []*dto.LabelPair, 374 additionalLabelName string, additionalLabelValue float64, 375) (int, error) { 376 if len(in) == 0 && additionalLabelName == "" { 377 return 0, nil 378 } 379 var ( 380 written int 381 separator byte = '{' 382 ) 383 for _, lp := range in { 384 err := w.WriteByte(separator) 385 written++ 386 if err != nil { 387 return written, err 388 } 389 n, err := w.WriteString(lp.GetName()) 390 written += n 391 if err != nil { 392 return written, err 393 } 394 n, err = w.WriteString(`="`) 395 written += n 396 if err != nil { 397 return written, err 398 } 399 n, err = writeEscapedString(w, lp.GetValue(), true) 400 written += n 401 if err != nil { 402 return written, err 403 } 404 err = w.WriteByte('"') 405 written++ 406 if err != nil { 407 return written, err 408 } 409 separator = ',' 410 } 411 if additionalLabelName != "" { 412 err := w.WriteByte(separator) 413 written++ 414 if err != nil { 415 return written, err 416 } 417 n, err := w.WriteString(additionalLabelName) 418 written += n 419 if err != nil { 420 return written, err 421 } 422 n, err = w.WriteString(`="`) 423 written += n 424 if err != nil { 425 return written, err 426 } 427 n, err = writeOpenMetricsFloat(w, additionalLabelValue) 428 written += n 429 if err != nil { 430 return written, err 431 } 432 err = w.WriteByte('"') 433 written++ 434 if err != nil { 435 return written, err 436 } 437 } 438 err := w.WriteByte('}') 439 written++ 440 if err != nil { 441 return written, err 442 } 443 return written, nil 444} 445 446// writeExemplar writes the provided exemplar in OpenMetrics format to w. The 447// function returns the number of bytes written and any error encountered. 448func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) { 449 written := 0 450 n, err := w.WriteString(" # ") 451 written += n 452 if err != nil { 453 return written, err 454 } 455 n, err = writeOpenMetricsLabelPairs(w, e.Label, "", 0) 456 written += n 457 if err != nil { 458 return written, err 459 } 460 err = w.WriteByte(' ') 461 written++ 462 if err != nil { 463 return written, err 464 } 465 n, err = writeOpenMetricsFloat(w, e.GetValue()) 466 written += n 467 if err != nil { 468 return written, err 469 } 470 if e.Timestamp != nil { 471 err = w.WriteByte(' ') 472 written++ 473 if err != nil { 474 return written, err 475 } 476 ts, err := ptypes.Timestamp((*e).Timestamp) 477 if err != nil { 478 return written, err 479 } 480 // TODO(beorn7): Format this directly from components of ts to 481 // avoid overflow/underflow and precision issues of the float 482 // conversion. 483 n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9) 484 written += n 485 if err != nil { 486 return written, err 487 } 488 } 489 return written, nil 490} 491 492// writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting 493// number would otherwise contain neither a "." nor an "e". 494func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) { 495 switch { 496 case f == 1: 497 return w.WriteString("1.0") 498 case f == 0: 499 return w.WriteString("0.0") 500 case f == -1: 501 return w.WriteString("-1.0") 502 case math.IsNaN(f): 503 return w.WriteString("NaN") 504 case math.IsInf(f, +1): 505 return w.WriteString("+Inf") 506 case math.IsInf(f, -1): 507 return w.WriteString("-Inf") 508 default: 509 bp := numBufPool.Get().(*[]byte) 510 *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64) 511 if !bytes.ContainsAny(*bp, "e.") { 512 *bp = append(*bp, '.', '0') 513 } 514 written, err := w.Write(*bp) 515 numBufPool.Put(bp) 516 return written, err 517 } 518} 519 520// writeUint is like writeInt just for uint64. 521func writeUint(w enhancedWriter, u uint64) (int, error) { 522 bp := numBufPool.Get().(*[]byte) 523 *bp = strconv.AppendUint((*bp)[:0], u, 10) 524 written, err := w.Write(*bp) 525 numBufPool.Put(bp) 526 return written, err 527} 528