@@ -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+ seenLabelSets := make (map [string ]struct {}, len (in .Metric ))
152165 for _ , metric := range in .Metric {
166+ key := labelSetKey (metric .Label )
167+ if _ , ok := seenLabelSets [key ]; ok {
168+ return written , fmt .Errorf ("duplicate label set in MetricFamily %s" , name )
169+ }
170+ seenLabelSets [key ] = struct {}{}
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,88 @@ 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+ seen := make (map [string ]struct {}, len (labels ))
397+ for _ , lp := range labels {
398+ lname := lp .GetName ()
399+ if lname == "" {
400+ return errors .New ("label name cannot be empty" )
401+ }
402+ if ! utf8 .ValidString (lname ) || strings .ContainsAny (lname , "\n \r " ) {
403+ return fmt .Errorf ("label name %q is not valid UTF-8 or contains raw newlines" , lname )
404+ }
405+ if ! utf8 .ValidString (lp .GetValue ()) {
406+ return fmt .Errorf ("label value %q is not valid UTF-8" , lp .GetValue ())
407+ }
408+ if _ , ok := seen [lname ]; ok {
409+ return fmt .Errorf ("duplicate label name %q" , lname )
410+ }
411+ seen [lname ] = struct {}{}
412+ }
413+ return nil
414+ }
415+
416+ func validateExemplar20 (e * dto.Exemplar ) error {
417+ if math .IsNaN (e .GetValue ()) {
418+ return errors .New ("exemplar value cannot be NaN" )
419+ }
420+ if err := e .Timestamp .CheckValid (); err != nil {
421+ return err
422+ }
423+ return validateLabels20 (e .Label )
424+ }
425+
426+ func labelSetKey (labels []* dto.LabelPair ) string {
427+ if len (labels ) == 0 {
428+ return ""
429+ }
430+ if len (labels ) == 1 {
431+ return labels [0 ].GetName () + "\x00 " + labels [0 ].GetValue ()
432+ }
433+ pairs := make ([]string , len (labels ))
434+ for i , lp := range labels {
435+ pairs [i ] = lp .GetName () + "\x00 " + lp .GetValue ()
436+ }
437+ sort .Strings (pairs )
438+ return strings .Join (pairs , "\x00 \x00 " )
439+ }
0 commit comments