Skip to content

Commit

Permalink
Metric metadata generation (open-telemetry#1546)
Browse files Browse the repository at this point in the history
* 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
jrcamp authored Sep 11, 2020
1 parent e886a01 commit 498bd31
Show file tree
Hide file tree
Showing 35 changed files with 1,403 additions and 153 deletions.
3 changes: 3 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ coverage:
default:
enabled: yes
target: 95%

ignore:
- **/*/metadata/generated_metrics.go
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ benchmark:
.PHONY: test-with-cover
test-with-cover:
@echo Verifying that all packages have test files to count in coverage
@internal/buildscripts/check-test-files.sh $(subst go.opentelemetry.io/collector/,./,$(ALL_PKGS))
@internal/buildscripts/check-test-files.sh $(subst go.opentelemetry.io/collector,.,$(ALL_PKGS))
@echo pre-compiling tests
@time go test -i $(ALL_PKGS)
$(GO_ACC) $(ALL_PKGS)
Expand Down Expand Up @@ -156,6 +156,7 @@ install-tools:

.PHONY: otelcol
otelcol:
go generate ./...
GO111MODULE=on CGO_ENABLED=0 go build -o ./bin/otelcol_$(GOOS)_$(GOARCH)$(EXTENSION) $(BUILD_INFO) ./cmd/otelcol

.PHONY: run
Expand Down
74 changes: 74 additions & 0 deletions cmd/mdatagen/lint.go
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
}
61 changes: 61 additions & 0 deletions cmd/mdatagen/lint_test.go
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)
}
})
}
}
163 changes: 163 additions & 0 deletions cmd/mdatagen/loader.go
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", "")
}
}
}
Loading

0 comments on commit 498bd31

Please sign in to comment.