@@ -6,6 +6,8 @@ package internal // import "go.opentelemetry.io/collector/cmd/mdatagen/internal"
66import (
77 "errors"
88 "fmt"
9+ "regexp"
10+ "strings"
911
1012 "golang.org/x/text/cases"
1113 "golang.org/x/text/language"
@@ -15,6 +17,8 @@ import (
1517 "go.opentelemetry.io/collector/pdata/pmetric"
1618)
1719
20+ var reNonAlnum = regexp .MustCompile (`[^a-z0-9]+` )
21+
1822type MetricName string
1923
2024func (mn MetricName ) Render () (string , error ) {
@@ -69,7 +73,7 @@ func (s *Stability) Unmarshal(parser *confmap.Conf) error {
6973 return parser .Unmarshal (s )
7074}
7175
72- func (m * Metric ) validate () error {
76+ func (m * Metric ) validate (metricName MetricName , semConvVersion string ) error {
7377 var errs error
7478 if m .Sum == nil && m .Gauge == nil && m .Histogram == nil {
7579 errs = errors .Join (errs , errors .New ("missing metric type key, " +
@@ -91,9 +95,49 @@ func (m *Metric) validate() error {
9195 if m .Gauge != nil {
9296 errs = errors .Join (errs , m .Gauge .Validate ())
9397 }
98+ if m .SemanticConvention != nil {
99+ if err := validateSemConvMetricURL (m .SemanticConvention .SemanticConventionRef , semConvVersion , string (metricName )); err != nil {
100+ errs = errors .Join (errs , err )
101+ }
102+ }
94103 return errs
95104}
96105
106+ func metricAnchor (metricName string ) string {
107+ m := strings .ToLower (strings .TrimSpace (metricName ))
108+ m = reNonAlnum .ReplaceAllString (m , "" )
109+ return "metric-" + m
110+ }
111+
112+ // validateSemConvMetricURL verifies the URL matches exactly:
113+ // https://github.com/open-telemetry/semantic-conventions/blob/<semConvVersion>/*#metric-<metricName>
114+ func validateSemConvMetricURL (rawURL , semConvVersion , metricName string ) error {
115+ if strings .TrimSpace (rawURL ) == "" {
116+ return errors .New ("url is empty" )
117+ }
118+ if strings .TrimSpace (semConvVersion ) == "" {
119+ return errors .New ("semConvVersion is empty" )
120+ }
121+ if strings .TrimSpace (metricName ) == "" {
122+ return errors .New ("metricName is empty" )
123+ }
124+ semConvVersion = "v" + semConvVersion
125+
126+ anchor := metricAnchor (metricName )
127+ // Build a strict regex that enforces https, repo, blob, given version, any doc path, and exact anchor.
128+ pattern := fmt .Sprintf (`^https://github\.com/open-telemetry/semantic-conventions/blob/%s/[^#\s]+#%s$` ,
129+ semConvVersion ,
130+ anchor ,
131+ )
132+ re := regexp .MustCompile (pattern )
133+ if ! re .MatchString (rawURL ) {
134+ return fmt .Errorf (
135+ "invalid semantic-conventions URL: want https://github.com/open-telemetry/semantic-conventions/blob/%s/*#%s, got %q" ,
136+ semConvVersion , anchor , rawURL )
137+ }
138+ return nil
139+ }
140+
97141func (m * Metric ) Unmarshal (parser * confmap.Conf ) error {
98142 if ! parser .IsSet ("enabled" ) {
99143 return errors .New ("missing required field: `enabled`" )
0 commit comments