@@ -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,21 @@ 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+ if metric == nil {
167+ return written , fmt .Errorf ("expected non-nil metric in MetricFamily %s" , name )
168+ }
169+ if err := validateMetric20 (name , metricType , metric ); err != nil {
170+ return written , err
171+ }
172+ key := labelSetKey (metric .Label )
173+ if _ , ok := seenLabelSets [key ]; ok {
174+ return written , fmt .Errorf ("duplicate label set in MetricFamily %s" , name )
175+ }
176+ seenLabelSets [key ] = struct {}{}
153177 switch metricType {
154178 case dto .MetricType_COUNTER :
155- if metric .Counter == nil {
156- return written , fmt .Errorf ("expected counter in metric %s %s" , name , metric )
157- }
158179 n , err = writeOpenMetrics20Sample (w , name , metric , metric .Counter .GetValue (), 0 , false , metric .Counter .Exemplar )
159180 case dto .MetricType_GAUGE :
160181 if metric .Gauge == nil {
@@ -334,3 +355,87 @@ func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric,
334355 _ = isGauge
335356 return 0 , errors .New ("histogram not implemented yet" )
336357}
358+
359+ func validateMetric20 (name string , metricType dto.MetricType , metric * dto.Metric ) error {
360+ if err := validateLabels20 (metric .Label ); err != nil {
361+ return fmt .Errorf ("invalid label in metric %s: %w" , name , err )
362+ }
363+
364+ switch metricType {
365+ case dto .MetricType_COUNTER :
366+ if metric .Counter == nil {
367+ return fmt .Errorf ("expected counter in metric %s %s" , name , metric )
368+ }
369+ val := metric .Counter .GetValue ()
370+ if math .IsNaN (val ) {
371+ return fmt .Errorf ("counter value cannot be NaN in metric %s" , name )
372+ }
373+ if val < 0 {
374+ return fmt .Errorf ("counter value cannot be negative (%g) in metric %s" , val , name )
375+ }
376+ if metric .Counter .CreatedTimestamp != nil {
377+ if err := metric .Counter .CreatedTimestamp .CheckValid (); err != nil {
378+ return fmt .Errorf ("invalid created timestamp in metric %s: %w" , name , err )
379+ }
380+ }
381+ if ex := metric .Counter .Exemplar ; ex != nil && ex .Timestamp != nil {
382+ if err := validateExemplar20 (ex ); err != nil {
383+ return fmt .Errorf ("invalid exemplar in metric %s: %w" , name , err )
384+ }
385+ }
386+ case dto .MetricType_GAUGE :
387+ if metric .Gauge == nil {
388+ return fmt .Errorf ("expected gauge in metric %s %s" , name , metric )
389+ }
390+ case dto .MetricType_UNTYPED :
391+ if metric .Untyped == nil {
392+ return fmt .Errorf ("expected untyped in metric %s %s" , name , metric )
393+ }
394+ }
395+ return nil
396+ }
397+
398+ func validateLabels20 (labels []* dto.LabelPair ) error {
399+ seen := make (map [string ]struct {}, len (labels ))
400+ for _ , lp := range labels {
401+ if lp == nil {
402+ return errors .New ("expected non-nil label pair" )
403+ }
404+ lname := lp .GetName ()
405+ if lname == "" {
406+ return errors .New ("label name cannot be empty" )
407+ }
408+ if ! utf8 .ValidString (lname ) || strings .ContainsAny (lname , "\n \r " ) {
409+ return fmt .Errorf ("label name %q is not valid UTF-8 or contains raw newlines" , lname )
410+ }
411+ if ! utf8 .ValidString (lp .GetValue ()) {
412+ return fmt .Errorf ("label value %q is not valid UTF-8" , lp .GetValue ())
413+ }
414+ if _ , ok := seen [lname ]; ok {
415+ return fmt .Errorf ("duplicate label name %q" , lname )
416+ }
417+ seen [lname ] = struct {}{}
418+ }
419+ return nil
420+ }
421+
422+ func validateExemplar20 (e * dto.Exemplar ) error {
423+ if err := e .Timestamp .CheckValid (); err != nil {
424+ return err
425+ }
426+ return validateLabels20 (e .Label )
427+ }
428+
429+ func labelSetKey (labels []* dto.LabelPair ) string {
430+ if len (labels ) == 0 {
431+ return ""
432+ }
433+ pairs := make ([]string , len (labels ))
434+ for i , lp := range labels {
435+ if lp != nil {
436+ pairs [i ] = lp .GetName () + "\x00 " + lp .GetValue ()
437+ }
438+ }
439+ sort .Strings (pairs )
440+ return strings .Join (pairs , "\x00 \x00 " )
441+ }
0 commit comments