@@ -19,7 +19,10 @@ import (
1919 "fmt"
2020 "io"
2121 "math"
22+ "sort"
2223 "strconv"
24+ "strings"
25+ "unicode/utf8"
2326
2427 dto "github.com/prometheus/client_model/go"
2528)
@@ -37,6 +40,15 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
3740 if name == "" {
3841 return 0 , fmt .Errorf ("MetricFamily has no name: %s" , in )
3942 }
43+ if ! utf8 .ValidString (name ) || strings .ContainsAny (name , "\n \r " ) {
44+ return 0 , fmt .Errorf ("MetricFamily name %q is not valid UTF-8 or contains raw newlines" , name )
45+ }
46+ if in .Help != nil && ! utf8 .ValidString (* in .Help ) {
47+ return 0 , fmt .Errorf ("MetricFamily help %q is not valid UTF-8" , * in .Help )
48+ }
49+ if in .Unit != nil && ! utf8 .ValidString (* in .Unit ) {
50+ return 0 , fmt .Errorf ("MetricFamily unit %q is not valid UTF-8" , * in .Unit )
51+ }
4052
4153 // Try the interface upgrade. If it doesn't work, we'll use a
4254 // bufio.Writer from the sync.Pool.
@@ -149,12 +161,18 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
149161 }
150162
151163 // Finally the samples, one line for each.
164+ if err := checkDuplicateLabelSets (name , in .Metric ); err != nil {
165+ return written , err
166+ }
152167 for _ , metric := range in .Metric {
168+ if metric == nil {
169+ return written , fmt .Errorf ("expected non-nil metric in MetricFamily %s" , name )
170+ }
171+ if err := validateMetric20 (name , metricType , metric ); err != nil {
172+ return written , err
173+ }
153174 switch metricType {
154175 case dto .MetricType_COUNTER :
155- if metric .Counter == nil {
156- return written , fmt .Errorf ("expected counter in metric %s %s" , name , metric )
157- }
158176 n , err = writeOpenMetrics20Sample (w , name , metric , metric .Counter .GetValue (), 0 , false , metric .Counter .Exemplar )
159177 case dto .MetricType_GAUGE :
160178 if metric .Gauge == nil {
@@ -334,3 +352,138 @@ func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric,
334352 _ = isGauge
335353 return 0 , errors .New ("histogram not implemented yet" )
336354}
355+
356+ func validateMetric20 (name string , metricType dto.MetricType , metric * dto.Metric ) error {
357+ if err := validateLabels20 (metric .Label ); err != nil {
358+ return fmt .Errorf ("invalid label in metric %s: %w" , name , err )
359+ }
360+
361+ switch metricType {
362+ case dto .MetricType_COUNTER :
363+ if metric .Counter == nil {
364+ return fmt .Errorf ("expected counter in metric %s %s" , name , metric )
365+ }
366+ val := metric .Counter .GetValue ()
367+ if math .IsNaN (val ) {
368+ return fmt .Errorf ("counter value cannot be NaN in metric %s" , name )
369+ }
370+ if val < 0 {
371+ return fmt .Errorf ("counter value cannot be negative (%g) in metric %s" , val , name )
372+ }
373+ if metric .Counter .CreatedTimestamp != nil {
374+ if err := metric .Counter .CreatedTimestamp .CheckValid (); err != nil {
375+ return fmt .Errorf ("invalid created timestamp in metric %s: %w" , name , err )
376+ }
377+ }
378+ if ex := metric .Counter .Exemplar ; ex != nil && ex .Timestamp != nil {
379+ if err := validateExemplar20 (ex ); err != nil {
380+ return fmt .Errorf ("invalid exemplar in metric %s: %w" , name , err )
381+ }
382+ }
383+ case dto .MetricType_GAUGE :
384+ if metric .Gauge == nil {
385+ return fmt .Errorf ("expected gauge in metric %s %s" , name , metric )
386+ }
387+ case dto .MetricType_UNTYPED :
388+ if metric .Untyped == nil {
389+ return fmt .Errorf ("expected untyped in metric %s %s" , name , metric )
390+ }
391+ }
392+ return nil
393+ }
394+
395+ func validateLabels20 (labels []* dto.LabelPair ) error {
396+ for i , lp := range labels {
397+ if lp == nil {
398+ return errors .New ("expected non-nil label pair" )
399+ }
400+ lname := lp .GetName ()
401+ if lname == "" {
402+ return errors .New ("label name cannot be empty" )
403+ }
404+ if ! utf8 .ValidString (lname ) || strings .ContainsAny (lname , "\n \r " ) {
405+ return fmt .Errorf ("label name %q is not valid UTF-8 or contains raw newlines" , lname )
406+ }
407+ if ! utf8 .ValidString (lp .GetValue ()) {
408+ return fmt .Errorf ("label value %q is not valid UTF-8" , lp .GetValue ())
409+ }
410+ for j := i + 1 ; j < len (labels ); j ++ {
411+ if labels [j ] != nil && labels [j ].GetName () == lname {
412+ return fmt .Errorf ("duplicate label name %q" , lname )
413+ }
414+ }
415+ }
416+ return nil
417+ }
418+
419+ func validateExemplar20 (e * dto.Exemplar ) error {
420+ if err := e .Timestamp .CheckValid (); err != nil {
421+ return err
422+ }
423+ return validateLabels20 (e .Label )
424+ }
425+
426+ func equalLabelSets (a , b []* dto.LabelPair ) bool {
427+ if len (a ) != len (b ) {
428+ return false
429+ }
430+ for _ , lA := range a {
431+ if lA == nil {
432+ return false
433+ }
434+ found := false
435+ for _ , lB := range b {
436+ if lB != nil && lA .GetName () == lB .GetName () && lA .GetValue () == lB .GetValue () {
437+ found = true
438+ break
439+ }
440+ }
441+ if ! found {
442+ return false
443+ }
444+ }
445+ return true
446+ }
447+
448+ func checkDuplicateLabelSets (name string , metrics []* dto.Metric ) error {
449+ if len (metrics ) <= 32 {
450+ for i , metric := range metrics {
451+ if metric == nil {
452+ continue
453+ }
454+ for j := range i {
455+ if metrics [j ] != nil && equalLabelSets (metric .Label , metrics [j ].Label ) {
456+ return fmt .Errorf ("duplicate label set in MetricFamily %s" , name )
457+ }
458+ }
459+ }
460+ return nil
461+ }
462+
463+ seen := make (map [string ]struct {}, len (metrics ))
464+ for _ , metric := range metrics {
465+ if metric == nil {
466+ continue
467+ }
468+ key := labelSetKey (metric .Label )
469+ if _ , ok := seen [key ]; ok {
470+ return fmt .Errorf ("duplicate label set in MetricFamily %s" , name )
471+ }
472+ seen [key ] = struct {}{}
473+ }
474+ return nil
475+ }
476+
477+ func labelSetKey (labels []* dto.LabelPair ) string {
478+ if len (labels ) == 0 {
479+ return ""
480+ }
481+ pairs := make ([]string , len (labels ))
482+ for i , lp := range labels {
483+ if lp != nil {
484+ pairs [i ] = lp .GetName () + "\x00 " + lp .GetValue ()
485+ }
486+ }
487+ sort .Strings (pairs )
488+ return strings .Join (pairs , "\x00 \x00 " )
489+ }
0 commit comments