forked from open-telemetry/opentelemetry-collector
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Metric metadata generation (open-telemetry#1546)
* fix go generate go generate would not work because a tool (esc) was missing from install-tools. fixing in preparation for metrics metadata generation. * Metric metadata generation This implements the proposal (phase 1) in open-telemetry#985. Currently just the cpu scraper has been migrated to gather feedback before moving additional ones. The plan is to check in the generated_metrics.go files. This makes it easier when others want to import the code as a library to not have to run the code generation in a library dependency. generated_metrics.go will be generated by running `go generate`. Resolves open-telemetry#985 * metadata improvements * acronyms (e.g. CpuState) are now formatted correctly (CPUState) * now handles labels with different semantics (e.g. state label in memory vs cpu) * they are referenced in code by unique ids (CPUState, MemState) but their string values are just "state" * specify which labels are referenced by a given metric * adding tests * merge cleanup * cleanup * cleanup * don't do codegen on windows for now * fix lint issues * fix coverage checks * fix ignore * add docs, code review feedback * invalid metric type check * update docs * cleanup * doc fix * make system.memory.usage int sum * move golint code to third_party dir
- Loading branch information
Showing
35 changed files
with
1,403 additions
and
153 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,3 +16,6 @@ coverage: | |
default: | ||
enabled: yes | ||
target: 95% | ||
|
||
ignore: | ||
- **/*/metadata/generated_metrics.go |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package main | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
"unicode" | ||
|
||
"go.opentelemetry.io/collector/cmd/mdatagen/third_party/golint" | ||
) | ||
|
||
// formatIdentifier variable in a go-safe way | ||
func formatIdentifier(s string, exported bool) (string, error) { | ||
if s == "" { | ||
return "", errors.New("string cannot be empty") | ||
} | ||
// Convert various characters to . for strings.Title to operate on. | ||
replace := strings.NewReplacer("_", ".", "-", ".", "<", ".", ">", ".", "/", ".", ":", ".") | ||
str := replace.Replace(s) | ||
str = strings.Title(str) | ||
str = strings.ReplaceAll(str, ".", "") | ||
|
||
var word string | ||
var output string | ||
|
||
// Fixup acronyms to make lint happy. | ||
for idx, r := range str { | ||
if idx == 0 { | ||
if exported { | ||
r = unicode.ToUpper(r) | ||
} else { | ||
r = unicode.ToLower(r) | ||
} | ||
} | ||
|
||
if unicode.IsUpper(r) || unicode.IsNumber(r) { | ||
// If the current word is an acronym and it's either exported or it's not the | ||
// beginning of an unexported variable then upper case it. | ||
if golint.Acronyms[strings.ToUpper(word)] && (exported || output != "") { | ||
output += strings.ToUpper(word) | ||
word = string(r) | ||
} else { | ||
output += word | ||
word = string(r) | ||
} | ||
} else { | ||
word += string(r) | ||
} | ||
} | ||
|
||
if golint.Acronyms[strings.ToUpper(word)] && output != "" { | ||
output += strings.ToUpper(word) | ||
} else { | ||
output += word | ||
} | ||
|
||
// Remove white spaces | ||
output = strings.Join(strings.Fields(output), "") | ||
|
||
return output, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Test_formatIdentifier(t *testing.T) { | ||
var tests = []struct { | ||
input string | ||
want string | ||
exported bool | ||
wantErr string | ||
}{ | ||
// Unexported. | ||
{input: "max.cpu", want: "maxCPU"}, | ||
{input: "max.foo", want: "maxFoo"}, | ||
{input: "cpu.utilization", want: "cpuUtilization"}, | ||
{input: "cpu", want: "cpu"}, | ||
{input: "max.ip.addr", want: "maxIPAddr"}, | ||
{input: "some_metric", want: "someMetric"}, | ||
{input: "some-metric", want: "someMetric"}, | ||
{input: "Upper.Case", want: "upperCase"}, | ||
{input: "max.ip6", want: "maxIP6"}, | ||
{input: "max.ip6.idle", want: "maxIP6Idle"}, | ||
{input: "node_netstat_IpExt_OutOctets", want: "nodeNetstatIPExtOutOctets"}, | ||
|
||
// Exported. | ||
{input: "cpu.state", want: "CPUState", exported: true}, | ||
|
||
// Errors | ||
{input: "", want: "", wantErr: "string cannot be empty"}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.input, func(t *testing.T) { | ||
got, err := formatIdentifier(tt.input, tt.exported) | ||
|
||
if tt.wantErr != "" { | ||
require.EqualError(t, err, tt.wantErr) | ||
} else { | ||
require.NoError(t, err) | ||
require.Equal(t, tt.want, got) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/go-playground/locales/en" | ||
ut "github.com/go-playground/universal-translator" | ||
"github.com/go-playground/validator/v10" | ||
"github.com/go-playground/validator/v10/non-standard/validators" | ||
en_translations "github.com/go-playground/validator/v10/translations/en" | ||
"gopkg.in/yaml.v2" | ||
) | ||
|
||
type metricName string | ||
|
||
func (mn metricName) Render() (string, error) { | ||
return formatIdentifier(string(mn), true) | ||
} | ||
|
||
type labelName string | ||
|
||
func (mn labelName) Render() (string, error) { | ||
return formatIdentifier(string(mn), true) | ||
} | ||
|
||
type metric struct { | ||
// Description of the metric. | ||
Description string `validate:"required,notblank"` | ||
// Unit of the metric. | ||
Unit string `validate:"oneof=s By"` | ||
|
||
// Raw data that is used to set Data interface below. | ||
YmlData *ymlMetricData `yaml:"data" validate:"required"` | ||
// Date is set to generic metric data interface after validating. | ||
Data MetricData `yaml:"-"` | ||
|
||
// Labels is the list of labels that the metric emits. | ||
Labels []labelName | ||
} | ||
|
||
type label struct { | ||
// Description describes the purpose of the label. | ||
Description string `validate:"notblank"` | ||
// Value can optionally specify the value this label will have. | ||
// For example, the label may have the identifier `MemState` to its | ||
// value may be `state` when used. | ||
Value string | ||
// Enum can optionally describe the set of values to which the label can belong. | ||
Enum []string | ||
} | ||
|
||
type metadata struct { | ||
// Name of the component. | ||
Name string `validate:"notblank"` | ||
// Labels emitted by one or more metrics. | ||
Labels map[labelName]label `validate:"dive"` | ||
// Metrics that can be emitted by the component. | ||
Metrics map[metricName]metric `validate:"dive"` | ||
} | ||
|
||
type templateContext struct { | ||
metadata | ||
// Package name for generated code. | ||
Package string | ||
} | ||
|
||
func loadMetadata(ymlData []byte) (metadata, error) { | ||
var out metadata | ||
|
||
// Unmarshal metadata. | ||
if err := yaml.Unmarshal(ymlData, &out); err != nil { | ||
return metadata{}, fmt.Errorf("unable to unmarshal yaml: %v", err) | ||
} | ||
|
||
// Validate metadata. | ||
if err := validateMetadata(out); err != nil { | ||
return metadata{}, err | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func validateMetadata(out metadata) error { | ||
v := validator.New() | ||
if err := v.RegisterValidation("notblank", validators.NotBlank); err != nil { | ||
return fmt.Errorf("failed registering notblank validator: %v", err) | ||
} | ||
|
||
// Provides better validation error messages. | ||
enLocale := en.New() | ||
uni := ut.New(enLocale, enLocale) | ||
|
||
tr, ok := uni.GetTranslator("en") | ||
if !ok { | ||
return errors.New("unable to lookup en translator") | ||
} | ||
|
||
if err := en_translations.RegisterDefaultTranslations(v, tr); err != nil { | ||
return fmt.Errorf("failed registering translations: %v", err) | ||
} | ||
|
||
if err := v.RegisterTranslation("nosuchlabel", tr, func(ut ut.Translator) error { | ||
return ut.Add("nosuchlabel", "unknown label value", true) // see universal-translator for details | ||
}, func(ut ut.Translator, fe validator.FieldError) string { | ||
t, _ := ut.T("nosuchlabel", fe.Field()) | ||
return t | ||
}); err != nil { | ||
return fmt.Errorf("failed registering nosuchlabel: %v", err) | ||
} | ||
|
||
v.RegisterStructValidation(metricValidation, metric{}) | ||
|
||
if err := v.Struct(&out); err != nil { | ||
if verr, ok := err.(validator.ValidationErrors); ok { | ||
m := verr.Translate(tr) | ||
buf := strings.Builder{} | ||
buf.WriteString("error validating struct:\n") | ||
for k, v := range m { | ||
buf.WriteString(fmt.Sprintf("\t%v: %v\n", k, v)) | ||
} | ||
return errors.New(buf.String()) | ||
} | ||
return fmt.Errorf("unknown validation error: %v", err) | ||
} | ||
|
||
// Set metric data interface. | ||
for k, v := range out.Metrics { | ||
v.Data = v.YmlData.MetricData | ||
v.YmlData = nil | ||
out.Metrics[k] = v | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// metricValidation validates metric structs. | ||
func metricValidation(sl validator.StructLevel) { | ||
// Make sure that the labels are valid. | ||
md := sl.Top().Interface().(*metadata) | ||
cur := sl.Current().Interface().(metric) | ||
|
||
for _, l := range cur.Labels { | ||
if _, ok := md.Labels[l]; !ok { | ||
sl.ReportError(cur.Labels, fmt.Sprintf("Labels[%s]", string(l)), "Labels", "nosuchlabel", "") | ||
} | ||
} | ||
} |
Oops, something went wrong.