Skip to content

Commit

Permalink
[datadog_logs_metric][datadog_metric_metadata] Normalize invalid metr…
Browse files Browse the repository at this point in the history
…ic name (#2433)

* add warning to invalid metric names

* Update datadog/internal/validators/validators.go

* use statefunc to normalize metric names

* fmt
  • Loading branch information
nkzou authored Jun 17, 2024
1 parent be659fd commit eae15a6
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 2 deletions.
78 changes: 76 additions & 2 deletions datadog/internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,6 @@ func DeleteKeyInMap(mapObject map[string]interface{}, keyList []string) {
} else if m, ok := mapObject[keyList[0]].(map[string]interface{}); ok {
DeleteKeyInMap(m, keyList[1:])
}

return
}

// GetStringSlice returns string slice for the given key if present, otherwise returns an empty slice
Expand Down Expand Up @@ -320,3 +318,79 @@ func StringSliceDifference(slice1, slice2 []string) []string {
}
return diff
}

// fast isAlpha for ascii
func isAlpha(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}

// fast isAlphaNumeric for ascii
func isAlphaNum(b byte) bool {
return isAlpha(b) || (b >= '0' && b <= '9')
}

// ValidateMetricName ensures the given metric name length is in [0, MaxMetricLen] and
// contains at least one alphabetic character whose index is returned
func ValidateMetricName(name string) (int, error) {
var i int
if name == "" {
return 0, fmt.Errorf("metric name is empty")
}

// skip non-alphabetic characters
for ; i < len(name) && !isAlpha(name[i]); i++ {
}

// if there were no alphabetic characters it wasn't valid
if i == len(name) {
return 0, fmt.Errorf("metric name %s is invalid. it must contain at least one alphabetic character", name)
}

return i, nil
}

// NormMetricNameParse normalizes metric names with a parser instead of using
// garbage-creating string replacement routines.
func NormMetricNameParse(name string) string {
i, err := ValidateMetricName(name)
if err != nil {
return name
}

var ptr int
res := make([]byte, 0, len(name))

for ; i < len(name); i++ {
switch {
case isAlphaNum(name[i]):
res = append(res, name[i])
ptr++
case name[i] == '.':
// we skipped all non-alpha chars up front so we have seen at least one
switch res[ptr-1] {
// overwrite underscores that happen before periods
case '_':
res[ptr-1] = '.'
default:
res = append(res, '.')
ptr++
}
default:
// we skipped all non-alpha chars up front so we have seen at least one
switch res[ptr-1] {
// no double underscores, no underscores after periods
case '.', '_':
default:
res = append(res, '_')
ptr++
}
}
}

if res[ptr-1] == '_' {
res = res[:ptr-1]
}
// safe because res does not escape this function
return string(res)

}
35 changes: 35 additions & 0 deletions datadog/internal/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,38 @@ func (r *mockResourceData) GetOk(key string) (interface{}, bool) {
v, ok := r.values[key]
return v, ok
}

var testMetricNames = map[string]string{
// bad metric names, need remapping
"test*&(*._-_Metrictastic*(*)( wtf_who_doesthis??": "test.Metrictastic_wtf_who_doesthis",
"?does.this.work?": "does.this.work",
"5-2 arsenal over spurs": "arsenal_over_spurs",
"dd.crawler.amazon web services.run_time": "dd.crawler.amazon_web_services.run_time",

// multiple metric names that normalize to the same thing
"multiple-norm-1": "multiple_norm_1",
"multiple_norm-1": "multiple_norm_1",

// for whatever reason, invalid characters x
"a$.b": "a.b",
"a_.b": "a.b",
"__init__.metric": "init.metric",
"a___..b": "a..b",
"a_.": "a.",
}

func TestNormMetricNameParse(t *testing.T) {
for src, target := range testMetricNames {
normed := NormMetricNameParse(src)
if normed != target {
t.Errorf("Expected tag '%s' normalized to '%s', got '%s' instead.", src, target, normed)
return
}
// double check that we're idempotent
again := NormMetricNameParse(normed)
if again != normed {
t.Errorf("Expected tag '%s' to be idempotent', got '%s' instead.", normed, again)
return
}
}
}
3 changes: 3 additions & 0 deletions datadog/resource_datadog_logs_metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ func resourceDatadogLogsMetric() *schema.Resource {
Required: true,
ForceNew: true,
Description: "The name of the log-based metric. This field can't be updated after creation.",
StateFunc: func(val any) string {
return utils.NormMetricNameParse(val.(string))
},
},
}
},
Expand Down
3 changes: 3 additions & 0 deletions datadog/resource_datadog_metric_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func resourceDatadogMetricMetadata() *schema.Resource {
Description: "The name of the metric.",
Type: schema.TypeString,
Required: true,
StateFunc: func(val any) string {
return utils.NormMetricNameParse(val.(string))
},
},
"type": {
Description: "Metric type such as `count`, `gauge`, or `rate`. Updating a metric of type `distribution` is not supported. If you would like to see the `distribution` type returned, contact [Datadog support](https://docs.datadoghq.com/help/).",
Expand Down

0 comments on commit eae15a6

Please sign in to comment.