From 4b340ca452b57327834191442cdf86715128b310 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 22 Dec 2023 08:18:19 -0800 Subject: [PATCH] [mdatagen] copy files from contrib to core (#9172) This is to allow us to use it in core as well --------- Signed-off-by: Alex Boten --- .chloggen/codeboten_mdatagen.yaml | 25 ++ .golangci.yml | 1 + Makefile | 5 +- cmd/mdatagen/Makefile | 1 + cmd/mdatagen/README.md | 56 +++ cmd/mdatagen/doc.go | 7 + cmd/mdatagen/documentation.md | 96 ++++ cmd/mdatagen/embeded_templates.go | 13 + cmd/mdatagen/embeded_templates_test.go | 47 ++ cmd/mdatagen/go.mod | 77 ++++ cmd/mdatagen/go.sum | 102 +++++ .../internal/metadata/generated_config.go | 122 +++++ .../metadata/generated_config_test.go | 145 ++++++ .../internal/metadata/generated_metrics.go | 425 ++++++++++++++++++ .../metadata/generated_metrics_test.go | 215 +++++++++ .../internal/metadata/generated_resource.go | 92 ++++ .../metadata/generated_resource_test.go | 82 ++++ .../internal/metadata/generated_status.go | 25 ++ .../internal/metadata/testdata/config.yaml | 55 +++ cmd/mdatagen/lint.go | 63 +++ cmd/mdatagen/lint_test.go | 50 +++ cmd/mdatagen/loader.go | 308 +++++++++++++ cmd/mdatagen/loader_test.go | 282 ++++++++++++ cmd/mdatagen/main.go | 292 ++++++++++++ cmd/mdatagen/main_test.go | 399 ++++++++++++++++ cmd/mdatagen/metadata-sample.yaml | 136 ++++++ cmd/mdatagen/metadata-schema.yaml | 112 +++++ cmd/mdatagen/metadata.yaml | 6 + cmd/mdatagen/metricdata.go | 182 ++++++++ cmd/mdatagen/metricdata_test.go | 26 ++ cmd/mdatagen/statusdata.go | 59 +++ cmd/mdatagen/statusdata_test.go | 59 +++ cmd/mdatagen/templates/component_test.go.tmpl | 340 ++++++++++++++ cmd/mdatagen/templates/config.go.tmpl | 103 +++++ cmd/mdatagen/templates/config_test.go.tmpl | 131 ++++++ cmd/mdatagen/templates/documentation.md.tmpl | 98 ++++ cmd/mdatagen/templates/metrics.go.tmpl | 319 +++++++++++++ cmd/mdatagen/templates/metrics_test.go.tmpl | 174 +++++++ cmd/mdatagen/templates/readme.md.tmpl | 55 +++ cmd/mdatagen/templates/resource.go.tmpl | 51 +++ cmd/mdatagen/templates/resource_test.go.tmpl | 65 +++ cmd/mdatagen/templates/status.go.tmpl | 26 ++ .../templates/testdata/config.yaml.tmpl | 31 ++ cmd/mdatagen/testdata/invalid.yaml | 1 + .../testdata/invalid_aggregation.yaml | 22 + cmd/mdatagen/testdata/invalid_class.yaml | 8 + cmd/mdatagen/testdata/invalid_input_type.yaml | 20 + cmd/mdatagen/testdata/invalid_stability.yaml | 7 + .../testdata/invalid_stability_component.yaml | 7 + cmd/mdatagen/testdata/invalid_type_attr.yaml | 24 + cmd/mdatagen/testdata/invalid_type_rattr.yaml | 19 + cmd/mdatagen/testdata/metrics_and_type.yaml | 19 + cmd/mdatagen/testdata/no_aggregation.yaml | 22 + cmd/mdatagen/testdata/no_class.yaml | 7 + .../testdata/no_description_attr.yaml | 33 ++ .../testdata/no_description_rattr.yaml | 12 + cmd/mdatagen/testdata/no_enabled.yaml | 18 + .../testdata/no_metric_description.yaml | 22 + cmd/mdatagen/testdata/no_metric_type.yaml | 13 + cmd/mdatagen/testdata/no_metric_unit.yaml | 22 + cmd/mdatagen/testdata/no_monotonic.yaml | 22 + cmd/mdatagen/testdata/no_stability.yaml | 4 + .../testdata/no_stability_component.yaml | 6 + cmd/mdatagen/testdata/no_status.yaml | 1 + cmd/mdatagen/testdata/no_type.yaml | 6 + cmd/mdatagen/testdata/no_type_attr.yaml | 23 + cmd/mdatagen/testdata/no_type_rattr.yaml | 18 + cmd/mdatagen/testdata/no_value_type.yaml | 22 + cmd/mdatagen/testdata/parent.yaml | 3 + .../testdata/readme_with_cmd_class.md | 14 + .../testdata/readme_with_multiple_signals.md | 15 + cmd/mdatagen/testdata/readme_with_status.md | 14 + .../testdata/readme_with_status_codeowners.md | 15 + ...dme_with_status_codeowners_and_emeritus.md | 16 + .../testdata/readme_with_status_extension.md | 14 + cmd/mdatagen/testdata/readme_with_warnings.md | 16 + .../testdata/readme_without_status.md | 3 + .../testdata/resource_attributes_only.yaml | 17 + cmd/mdatagen/testdata/status_only.yaml | 6 + cmd/mdatagen/testdata/two_metric_types.yaml | 21 + .../testdata/unknown_metric_attribute.yaml | 19 + cmd/mdatagen/testdata/unknown_value_type.yaml | 18 + cmd/mdatagen/testdata/unused_attribute.yaml | 29 ++ cmd/mdatagen/third_party/golint/LICENSE | 27 ++ cmd/mdatagen/third_party/golint/golint.go | 51 +++ cmd/mdatagen/validate.go | 194 ++++++++ cmd/mdatagen/validate_test.go | 153 +++++++ versions.yaml | 1 + 88 files changed, 5981 insertions(+), 1 deletion(-) create mode 100755 .chloggen/codeboten_mdatagen.yaml create mode 100644 cmd/mdatagen/Makefile create mode 100644 cmd/mdatagen/README.md create mode 100644 cmd/mdatagen/doc.go create mode 100644 cmd/mdatagen/documentation.md create mode 100644 cmd/mdatagen/embeded_templates.go create mode 100644 cmd/mdatagen/embeded_templates_test.go create mode 100644 cmd/mdatagen/go.mod create mode 100644 cmd/mdatagen/go.sum create mode 100644 cmd/mdatagen/internal/metadata/generated_config.go create mode 100644 cmd/mdatagen/internal/metadata/generated_config_test.go create mode 100644 cmd/mdatagen/internal/metadata/generated_metrics.go create mode 100644 cmd/mdatagen/internal/metadata/generated_metrics_test.go create mode 100644 cmd/mdatagen/internal/metadata/generated_resource.go create mode 100644 cmd/mdatagen/internal/metadata/generated_resource_test.go create mode 100644 cmd/mdatagen/internal/metadata/generated_status.go create mode 100644 cmd/mdatagen/internal/metadata/testdata/config.yaml create mode 100644 cmd/mdatagen/lint.go create mode 100644 cmd/mdatagen/lint_test.go create mode 100644 cmd/mdatagen/loader.go create mode 100644 cmd/mdatagen/loader_test.go create mode 100644 cmd/mdatagen/main.go create mode 100644 cmd/mdatagen/main_test.go create mode 100644 cmd/mdatagen/metadata-sample.yaml create mode 100644 cmd/mdatagen/metadata-schema.yaml create mode 100644 cmd/mdatagen/metadata.yaml create mode 100644 cmd/mdatagen/metricdata.go create mode 100644 cmd/mdatagen/metricdata_test.go create mode 100644 cmd/mdatagen/statusdata.go create mode 100644 cmd/mdatagen/statusdata_test.go create mode 100644 cmd/mdatagen/templates/component_test.go.tmpl create mode 100644 cmd/mdatagen/templates/config.go.tmpl create mode 100644 cmd/mdatagen/templates/config_test.go.tmpl create mode 100644 cmd/mdatagen/templates/documentation.md.tmpl create mode 100644 cmd/mdatagen/templates/metrics.go.tmpl create mode 100644 cmd/mdatagen/templates/metrics_test.go.tmpl create mode 100644 cmd/mdatagen/templates/readme.md.tmpl create mode 100644 cmd/mdatagen/templates/resource.go.tmpl create mode 100644 cmd/mdatagen/templates/resource_test.go.tmpl create mode 100644 cmd/mdatagen/templates/status.go.tmpl create mode 100644 cmd/mdatagen/templates/testdata/config.yaml.tmpl create mode 100644 cmd/mdatagen/testdata/invalid.yaml create mode 100644 cmd/mdatagen/testdata/invalid_aggregation.yaml create mode 100644 cmd/mdatagen/testdata/invalid_class.yaml create mode 100644 cmd/mdatagen/testdata/invalid_input_type.yaml create mode 100644 cmd/mdatagen/testdata/invalid_stability.yaml create mode 100644 cmd/mdatagen/testdata/invalid_stability_component.yaml create mode 100644 cmd/mdatagen/testdata/invalid_type_attr.yaml create mode 100644 cmd/mdatagen/testdata/invalid_type_rattr.yaml create mode 100644 cmd/mdatagen/testdata/metrics_and_type.yaml create mode 100644 cmd/mdatagen/testdata/no_aggregation.yaml create mode 100644 cmd/mdatagen/testdata/no_class.yaml create mode 100644 cmd/mdatagen/testdata/no_description_attr.yaml create mode 100644 cmd/mdatagen/testdata/no_description_rattr.yaml create mode 100644 cmd/mdatagen/testdata/no_enabled.yaml create mode 100644 cmd/mdatagen/testdata/no_metric_description.yaml create mode 100644 cmd/mdatagen/testdata/no_metric_type.yaml create mode 100644 cmd/mdatagen/testdata/no_metric_unit.yaml create mode 100644 cmd/mdatagen/testdata/no_monotonic.yaml create mode 100644 cmd/mdatagen/testdata/no_stability.yaml create mode 100644 cmd/mdatagen/testdata/no_stability_component.yaml create mode 100644 cmd/mdatagen/testdata/no_status.yaml create mode 100644 cmd/mdatagen/testdata/no_type.yaml create mode 100644 cmd/mdatagen/testdata/no_type_attr.yaml create mode 100644 cmd/mdatagen/testdata/no_type_rattr.yaml create mode 100644 cmd/mdatagen/testdata/no_value_type.yaml create mode 100644 cmd/mdatagen/testdata/parent.yaml create mode 100644 cmd/mdatagen/testdata/readme_with_cmd_class.md create mode 100644 cmd/mdatagen/testdata/readme_with_multiple_signals.md create mode 100644 cmd/mdatagen/testdata/readme_with_status.md create mode 100644 cmd/mdatagen/testdata/readme_with_status_codeowners.md create mode 100644 cmd/mdatagen/testdata/readme_with_status_codeowners_and_emeritus.md create mode 100644 cmd/mdatagen/testdata/readme_with_status_extension.md create mode 100644 cmd/mdatagen/testdata/readme_with_warnings.md create mode 100644 cmd/mdatagen/testdata/readme_without_status.md create mode 100644 cmd/mdatagen/testdata/resource_attributes_only.yaml create mode 100644 cmd/mdatagen/testdata/status_only.yaml create mode 100644 cmd/mdatagen/testdata/two_metric_types.yaml create mode 100644 cmd/mdatagen/testdata/unknown_metric_attribute.yaml create mode 100644 cmd/mdatagen/testdata/unknown_value_type.yaml create mode 100644 cmd/mdatagen/testdata/unused_attribute.yaml create mode 100644 cmd/mdatagen/third_party/golint/LICENSE create mode 100644 cmd/mdatagen/third_party/golint/golint.go create mode 100644 cmd/mdatagen/validate.go create mode 100644 cmd/mdatagen/validate_test.go diff --git a/.chloggen/codeboten_mdatagen.yaml b/.chloggen/codeboten_mdatagen.yaml new file mode 100755 index 00000000000..d0fb2c9c265 --- /dev/null +++ b/.chloggen/codeboten_mdatagen.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: mdatagen + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: move component from contrib to core + +# One or more tracking issues or pull requests related to the change +issues: [9172] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 65589afbc2f..30c69ed9444 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,6 +17,7 @@ run: # default value is empty list, but default dirs are skipped independently # from this option's value (see skip-dirs-use-default). skip-dirs: + - third_party # default is true. Enables skipping of directories: # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ diff --git a/Makefile b/Makefile index dc7884c89bb..925cb18a53a 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ include ./Makefile.Common # This is the code that we want to run lint, etc. ALL_SRC := $(shell find . -name '*.go' \ -not -path './internal/tools/*' \ + -not -path '*/third_party/*' \ -not -path './pdata/internal/data/protogen/*' \ -not -path './service/internal/zpages/tmplgen/*' \ -type f | sort) @@ -57,7 +58,7 @@ gotest-with-cover: .PHONY: goporto goporto: $(PORTO) - $(PORTO) -w --include-internal ./ + $(PORTO) -w --include-internal --skip-dirs "^cmd/mdatagen/third_party$$" ./ .PHONY: golint golint: @@ -77,7 +78,9 @@ gotidy: .PHONY: gogenerate gogenerate: + cd cmd/mdatagen && $(GOCMD) install . @$(MAKE) for-all-target TARGET="generate" + $(MAKE) fmt .PHONY: addlicense addlicense: $(ADDLICENSE) diff --git a/cmd/mdatagen/Makefile b/cmd/mdatagen/Makefile new file mode 100644 index 00000000000..ded7a36092d --- /dev/null +++ b/cmd/mdatagen/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/cmd/mdatagen/README.md b/cmd/mdatagen/README.md new file mode 100644 index 00000000000..b2affa6c780 --- /dev/null +++ b/cmd/mdatagen/README.md @@ -0,0 +1,56 @@ +# Metadata Generator + +Every component's documentation should include a brief description of the component and guidance on how to use it. +There is also some information about the component (or metadata) that should be included to help end-users understand the current state of the component and whether it is right for their use case. +Examples of this metadata about a component are: + +* its stability level +* the distributions containing it +* the types of pipelines it supports +* metrics emitted in the case of a scraping receiver + +The metadata generator defines a schema for specifying this information to ensure it is complete and well-formed. +The metadata generator is then able to ingest the metadata, validate it against the schema and produce documentation in a standardized format. +An example of how this generated documentation looks can be found in [documentation.md](./documentation.md). + +## Using the Metadata Generator + +In order for a component to benefit from the metadata generator (`mdatagen`) these requirements need to be met: +1. A `metadata.yaml` file containing the metadata needs to be included in the component +2. The component should declare a `go:generate mdatagen` directive which tells `mdatagen` what to generate + +As an example, here is a minimal `metadata.yaml` for the [OTLP receiver](https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver/otlpreceiver): +```yaml +type: otlp +status: + class: receiver + stability: + beta: [logs] + stable: [metrics, traces] +``` + +Detailed information about the schema of `metadata.yaml` can be found in [metadata-schema.yaml](./metadata-schema.yaml). + +The `go:generate mdatagen` directive is usually defined in a `doc.go` file in the same package as the component, for example: +```go +//go:generate mdatagen metadata.yaml + +package main +``` + +Below are some more examples that can be used for reference: + +* The ElasticSearch receiver has an extensive [metadata.yaml](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/elasticsearchreceiver/metadata.yaml) +* The host metrics receiver has internal subcomponents, each with their own `metadata.yaml` and `doc.go`. See [cpuscraper](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/hostmetricsreceiver/internal/scraper/cpuscraper) for example. + +You can run `cd cmd/mdatagen && $(GOCMD) install .` to install the `mdatagen` tool in `GOBIN` and then run `mdatagen metadata.yaml` to generate documentation for a specific component or you can run `make generate` to generate documentation for all components. + +## Contributing to the Metadata Generator + +The code for generating the documentation can be found in [loader.go](./loader.go) and the templates for rendering the documentation can be found in [templates](./templates). +When making updates to the metadata generator or introducing support for new functionality: + +1. Ensure the [metadata-schema.yaml](./metadata-schema.yaml) and [./metadata.yaml](metadata.yaml) files reflect the changes. +2. Run `make mdatagen-test`. +3. Make sure all tests are passing including [generated tests](./internal/metadata/generated_metrics_test.go). +4. Run `make generate`. diff --git a/cmd/mdatagen/doc.go b/cmd/mdatagen/doc.go new file mode 100644 index 00000000000..8f1fc6a7176 --- /dev/null +++ b/cmd/mdatagen/doc.go @@ -0,0 +1,7 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Generate a test metrics builder from a sample metrics set covering all configuration options. +//go:generate mdatagen metadata-sample.yaml + +package main diff --git a/cmd/mdatagen/documentation.md b/cmd/mdatagen/documentation.md new file mode 100644 index 00000000000..1955e081535 --- /dev/null +++ b/cmd/mdatagen/documentation.md @@ -0,0 +1,96 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# file + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### default.metric + +Monotonic cumulative sum int metric enabled by default. + +The metric will be become optional soon. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| s | Sum | Int | Cumulative | true | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| string_attr | Attribute with any string value. | Any Str | +| state | Integer attribute with overridden name. | Any Int | +| enum_attr | Attribute with a known set of string values. | Str: ``red``, ``green``, ``blue`` | +| slice_attr | Attribute with a slice value. | Any Slice | +| map_attr | Attribute with a map value. | Any Map | + +### default.metric.to_be_removed + +[DEPRECATED] Non-monotonic delta sum double metric enabled by default. + +The metric will be will be removed soon. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| s | Sum | Double | Delta | false | + +## Optional Metrics + +The following metrics are not emitted by default. Each of them can be enabled by applying the following configuration: + +```yaml +metrics: + : + enabled: true +``` + +### optional.metric + +[DEPRECATED] Gauge double metric disabled by default. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Double | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| string_attr | Attribute with any string value. | Any Str | +| boolean_attr | Attribute with a boolean value. | Any Bool | + +### optional.metric.empty_unit + +[DEPRECATED] Gauge double metric disabled by default. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Double | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| string_attr | Attribute with any string value. | Any Str | +| boolean_attr | Attribute with a boolean value. | Any Bool | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| map.resource.attr | Resource attribute with a map value. | Any Map | true | +| optional.resource.attr | Explicitly disabled ResourceAttribute. | Any Str | false | +| slice.resource.attr | Resource attribute with a slice value. | Any Slice | true | +| string.enum.resource.attr | Resource attribute with a known set of string values. | Str: ``one``, ``two`` | true | +| string.resource.attr | Resource attribute with any string value. | Any Str | true | +| string.resource.attr_disable_warning | Resource attribute with any string value. | Any Str | true | +| string.resource.attr_remove_warning | Resource attribute with any string value. | Any Str | false | +| string.resource.attr_to_be_removed | Resource attribute with any string value. | Any Str | true | diff --git a/cmd/mdatagen/embeded_templates.go b/cmd/mdatagen/embeded_templates.go new file mode 100644 index 00000000000..dace6c43f32 --- /dev/null +++ b/cmd/mdatagen/embeded_templates.go @@ -0,0 +1,13 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "embed" + +// templateFS ensures that the files needed +// to generate metadata as an embedded filesystem since +// `go get` doesn't require these files to be downloaded. +// +//go:embed templates/*.tmpl templates/testdata/*.tmpl +var templateFS embed.FS diff --git a/cmd/mdatagen/embeded_templates_test.go b/cmd/mdatagen/embeded_templates_test.go new file mode 100644 index 00000000000..b52850da395 --- /dev/null +++ b/cmd/mdatagen/embeded_templates_test.go @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "io/fs" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEnsureTemplatesLoaded(t *testing.T) { + t.Parallel() + + const ( + rootDir = "templates" + ) + + var ( + templateFiles = map[string]struct{}{ + path.Join(rootDir, "component_test.go.tmpl"): {}, + path.Join(rootDir, "documentation.md.tmpl"): {}, + path.Join(rootDir, "metrics.go.tmpl"): {}, + path.Join(rootDir, "metrics_test.go.tmpl"): {}, + path.Join(rootDir, "resource.go.tmpl"): {}, + path.Join(rootDir, "resource_test.go.tmpl"): {}, + path.Join(rootDir, "config.go.tmpl"): {}, + path.Join(rootDir, "config_test.go.tmpl"): {}, + path.Join(rootDir, "readme.md.tmpl"): {}, + path.Join(rootDir, "status.go.tmpl"): {}, + path.Join(rootDir, "testdata", "config.yaml.tmpl"): {}, + } + count = 0 + ) + assert.NoError(t, fs.WalkDir(templateFS, ".", func(path string, d fs.DirEntry, err error) error { + if d != nil && d.IsDir() { + return nil + } + count++ + assert.Contains(t, templateFiles, path) + return nil + })) + assert.Equal(t, len(templateFiles), count, "Must match the expected number of calls") + +} diff --git a/cmd/mdatagen/go.mod b/cmd/mdatagen/go.mod new file mode 100644 index 00000000000..610f2dec11d --- /dev/null +++ b/cmd/mdatagen/go.mod @@ -0,0 +1,77 @@ +module go.opentelemetry.io/collector/cmd/mdatagen + +go 1.20 + +require ( + github.com/google/go-cmp v0.6.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/collector/component v0.91.0 + go.opentelemetry.io/collector/confmap v0.91.0 + go.opentelemetry.io/collector/pdata v1.0.0 + go.opentelemetry.io/collector/receiver v0.91.0 + go.opentelemetry.io/collector/semconv v0.91.0 + go.opentelemetry.io/otel/metric v1.21.0 + go.opentelemetry.io/otel/trace v1.21.0 + go.uber.org/multierr v1.11.0 + go.uber.org/zap v1.26.0 + golang.org/x/text v0.14.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.0.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.91.0 // indirect + go.opentelemetry.io/collector/consumer v0.91.0 // indirect + go.opentelemetry.io/collector/featuregate v1.0.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sys v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/grpc v1.60.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +retract ( + v0.76.2 + v0.76.1 + v0.65.0 +) + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector => ../.. + +replace go.opentelemetry.io/collector/config/configretry => ../../config/configretry + +replace go.opentelemetry.io/collector/processor => ../../processor + +replace go.opentelemetry.io/collector/config/configtelemetry => ../../config/configtelemetry + +replace go.opentelemetry.io/collector/confmap => ../../confmap + +replace go.opentelemetry.io/collector/consumer => ../../consumer + +replace go.opentelemetry.io/collector/pdata => ../../pdata + +replace go.opentelemetry.io/collector/extension => ../../extension + +replace go.opentelemetry.io/collector/exporter => ../../exporter + +replace go.opentelemetry.io/collector/semconv => ../../semconv + +replace go.opentelemetry.io/collector/receiver => ../../receiver diff --git a/cmd/mdatagen/go.sum b/cmd/mdatagen/go.sum new file mode 100644 index 00000000000..b99c2617e81 --- /dev/null +++ b/cmd/mdatagen/go.sum @@ -0,0 +1,102 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= +github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/mdatagen/internal/metadata/generated_config.go b/cmd/mdatagen/internal/metadata/generated_config.go new file mode 100644 index 00000000000..ac4e46cd178 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_config.go @@ -0,0 +1,122 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import "go.opentelemetry.io/collector/confmap" + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms, confmap.WithErrorUnused()) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for file metrics. +type MetricsConfig struct { + DefaultMetric MetricConfig `mapstructure:"default.metric"` + DefaultMetricToBeRemoved MetricConfig `mapstructure:"default.metric.to_be_removed"` + OptionalMetric MetricConfig `mapstructure:"optional.metric"` + OptionalMetricEmptyUnit MetricConfig `mapstructure:"optional.metric.empty_unit"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + DefaultMetric: MetricConfig{ + Enabled: true, + }, + DefaultMetricToBeRemoved: MetricConfig{ + Enabled: true, + }, + OptionalMetric: MetricConfig{ + Enabled: false, + }, + OptionalMetricEmptyUnit: MetricConfig{ + Enabled: false, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac, confmap.WithErrorUnused()) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for file resource attributes. +type ResourceAttributesConfig struct { + MapResourceAttr ResourceAttributeConfig `mapstructure:"map.resource.attr"` + OptionalResourceAttr ResourceAttributeConfig `mapstructure:"optional.resource.attr"` + SliceResourceAttr ResourceAttributeConfig `mapstructure:"slice.resource.attr"` + StringEnumResourceAttr ResourceAttributeConfig `mapstructure:"string.enum.resource.attr"` + StringResourceAttr ResourceAttributeConfig `mapstructure:"string.resource.attr"` + StringResourceAttrDisableWarning ResourceAttributeConfig `mapstructure:"string.resource.attr_disable_warning"` + StringResourceAttrRemoveWarning ResourceAttributeConfig `mapstructure:"string.resource.attr_remove_warning"` + StringResourceAttrToBeRemoved ResourceAttributeConfig `mapstructure:"string.resource.attr_to_be_removed"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + MapResourceAttr: ResourceAttributeConfig{ + Enabled: true, + }, + OptionalResourceAttr: ResourceAttributeConfig{ + Enabled: false, + }, + SliceResourceAttr: ResourceAttributeConfig{ + Enabled: true, + }, + StringEnumResourceAttr: ResourceAttributeConfig{ + Enabled: true, + }, + StringResourceAttr: ResourceAttributeConfig{ + Enabled: true, + }, + StringResourceAttrDisableWarning: ResourceAttributeConfig{ + Enabled: true, + }, + StringResourceAttrRemoveWarning: ResourceAttributeConfig{ + Enabled: false, + }, + StringResourceAttrToBeRemoved: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for file metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), + } +} diff --git a/cmd/mdatagen/internal/metadata/generated_config_test.go b/cmd/mdatagen/internal/metadata/generated_config_test.go new file mode 100644 index 00000000000..bfb90940386 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_config_test.go @@ -0,0 +1,145 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + DefaultMetric: MetricConfig{Enabled: true}, + DefaultMetricToBeRemoved: MetricConfig{Enabled: true}, + OptionalMetric: MetricConfig{Enabled: true}, + OptionalMetricEmptyUnit: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + MapResourceAttr: ResourceAttributeConfig{Enabled: true}, + OptionalResourceAttr: ResourceAttributeConfig{Enabled: true}, + SliceResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringEnumResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrDisableWarning: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrRemoveWarning: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrToBeRemoved: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + DefaultMetric: MetricConfig{Enabled: false}, + DefaultMetricToBeRemoved: MetricConfig{Enabled: false}, + OptionalMetric: MetricConfig{Enabled: false}, + OptionalMetricEmptyUnit: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + MapResourceAttr: ResourceAttributeConfig{Enabled: false}, + OptionalResourceAttr: ResourceAttributeConfig{Enabled: false}, + SliceResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringEnumResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrDisableWarning: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrRemoveWarning: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrToBeRemoved: ResourceAttributeConfig{Enabled: false}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})); diff != "" { + t.Errorf("Config mismatch (-expected +actual):\n%s", diff) + } + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, component.UnmarshalConfig(sub, &cfg)) + return cfg +} + +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + MapResourceAttr: ResourceAttributeConfig{Enabled: true}, + OptionalResourceAttr: ResourceAttributeConfig{Enabled: true}, + SliceResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringEnumResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringResourceAttr: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrDisableWarning: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrRemoveWarning: ResourceAttributeConfig{Enabled: true}, + StringResourceAttrToBeRemoved: ResourceAttributeConfig{Enabled: true}, + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + MapResourceAttr: ResourceAttributeConfig{Enabled: false}, + OptionalResourceAttr: ResourceAttributeConfig{Enabled: false}, + SliceResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringEnumResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringResourceAttr: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrDisableWarning: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrRemoveWarning: ResourceAttributeConfig{Enabled: false}, + StringResourceAttrToBeRemoved: ResourceAttributeConfig{Enabled: false}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})); diff != "" { + t.Errorf("Config mismatch (-expected +actual):\n%s", diff) + } + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, component.UnmarshalConfig(sub, &cfg)) + return cfg +} diff --git a/cmd/mdatagen/internal/metadata/generated_metrics.go b/cmd/mdatagen/internal/metadata/generated_metrics.go new file mode 100644 index 00000000000..a52195c69df --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_metrics.go @@ -0,0 +1,425 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" + conventions "go.opentelemetry.io/collector/semconv/v1.9.0" +) + +// AttributeEnumAttr specifies the a value enum_attr attribute. +type AttributeEnumAttr int + +const ( + _ AttributeEnumAttr = iota + AttributeEnumAttrRed + AttributeEnumAttrGreen + AttributeEnumAttrBlue +) + +// String returns the string representation of the AttributeEnumAttr. +func (av AttributeEnumAttr) String() string { + switch av { + case AttributeEnumAttrRed: + return "red" + case AttributeEnumAttrGreen: + return "green" + case AttributeEnumAttrBlue: + return "blue" + } + return "" +} + +// MapAttributeEnumAttr is a helper map of string to AttributeEnumAttr attribute value. +var MapAttributeEnumAttr = map[string]AttributeEnumAttr{ + "red": AttributeEnumAttrRed, + "green": AttributeEnumAttrGreen, + "blue": AttributeEnumAttrBlue, +} + +type metricDefaultMetric struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills default.metric metric with initial data. +func (m *metricDefaultMetric) init() { + m.data.SetName("default.metric") + m.data.SetDescription("Monotonic cumulative sum int metric enabled by default.") + m.data.SetUnit("s") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(true) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + m.data.Sum().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricDefaultMetric) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, stringAttrAttributeValue string, overriddenIntAttrAttributeValue int64, enumAttrAttributeValue string, sliceAttrAttributeValue []any, mapAttrAttributeValue map[string]any) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) + dp.Attributes().PutStr("string_attr", stringAttrAttributeValue) + dp.Attributes().PutInt("state", overriddenIntAttrAttributeValue) + dp.Attributes().PutStr("enum_attr", enumAttrAttributeValue) + dp.Attributes().PutEmptySlice("slice_attr").FromRaw(sliceAttrAttributeValue) + dp.Attributes().PutEmptyMap("map_attr").FromRaw(mapAttrAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricDefaultMetric) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricDefaultMetric) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricDefaultMetric(cfg MetricConfig) metricDefaultMetric { + m := metricDefaultMetric{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricDefaultMetricToBeRemoved struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills default.metric.to_be_removed metric with initial data. +func (m *metricDefaultMetricToBeRemoved) init() { + m.data.SetName("default.metric.to_be_removed") + m.data.SetDescription("[DEPRECATED] Non-monotonic delta sum double metric enabled by default.") + m.data.SetUnit("s") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityDelta) +} + +func (m *metricDefaultMetricToBeRemoved) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricDefaultMetricToBeRemoved) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricDefaultMetricToBeRemoved) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricDefaultMetricToBeRemoved(cfg MetricConfig) metricDefaultMetricToBeRemoved { + m := metricDefaultMetricToBeRemoved{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricOptionalMetric struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills optional.metric metric with initial data. +func (m *metricOptionalMetric) init() { + m.data.SetName("optional.metric") + m.data.SetDescription("[DEPRECATED] Gauge double metric disabled by default.") + m.data.SetUnit("1") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricOptionalMetric) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64, stringAttrAttributeValue string, booleanAttrAttributeValue bool) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) + dp.Attributes().PutStr("string_attr", stringAttrAttributeValue) + dp.Attributes().PutBool("boolean_attr", booleanAttrAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricOptionalMetric) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricOptionalMetric) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricOptionalMetric(cfg MetricConfig) metricOptionalMetric { + m := metricOptionalMetric{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricOptionalMetricEmptyUnit struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills optional.metric.empty_unit metric with initial data. +func (m *metricOptionalMetricEmptyUnit) init() { + m.data.SetName("optional.metric.empty_unit") + m.data.SetDescription("[DEPRECATED] Gauge double metric disabled by default.") + m.data.SetUnit("") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricOptionalMetricEmptyUnit) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64, stringAttrAttributeValue string, booleanAttrAttributeValue bool) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) + dp.Attributes().PutStr("string_attr", stringAttrAttributeValue) + dp.Attributes().PutBool("boolean_attr", booleanAttrAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricOptionalMetricEmptyUnit) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricOptionalMetricEmptyUnit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricOptionalMetricEmptyUnit(cfg MetricConfig) metricOptionalMetricEmptyUnit { + m := metricOptionalMetricEmptyUnit{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + metricDefaultMetric metricDefaultMetric + metricDefaultMetricToBeRemoved metricDefaultMetricToBeRemoved + metricOptionalMetric metricOptionalMetric + metricOptionalMetricEmptyUnit metricOptionalMetricEmptyUnit +} + +// metricBuilderOption applies changes to default metrics builder. +type metricBuilderOption func(*MetricsBuilder) + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) metricBuilderOption { + return func(mb *MetricsBuilder) { + mb.startTime = startTime + } +} + +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSettings, options ...metricBuilderOption) *MetricsBuilder { + if !mbc.Metrics.DefaultMetric.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `default.metric`: This metric will be disabled by default soon.") + } + if mbc.Metrics.DefaultMetricToBeRemoved.Enabled { + settings.Logger.Warn("[WARNING] `default.metric.to_be_removed` should not be enabled: This metric is deprecated and will be removed soon.") + } + if mbc.Metrics.OptionalMetric.enabledSetByUser { + settings.Logger.Warn("[WARNING] `optional.metric` should not be configured: This metric is deprecated and will be removed soon.") + } + if mbc.Metrics.OptionalMetricEmptyUnit.enabledSetByUser { + settings.Logger.Warn("[WARNING] `optional.metric.empty_unit` should not be configured: This metric is deprecated and will be removed soon.") + } + if !mbc.ResourceAttributes.StringResourceAttrDisableWarning.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `string.resource.attr_disable_warning`: This resource_attribute will be disabled by default soon.") + } + if mbc.ResourceAttributes.StringResourceAttrRemoveWarning.enabledSetByUser { + settings.Logger.Warn("[WARNING] `string.resource.attr_remove_warning` should not be configured: This resource_attribute is deprecated and will be removed soon.") + } + if mbc.ResourceAttributes.StringResourceAttrToBeRemoved.Enabled { + settings.Logger.Warn("[WARNING] `string.resource.attr_to_be_removed` should not be enabled: This resource_attribute is deprecated and will be removed soon.") + } + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricDefaultMetric: newMetricDefaultMetric(mbc.Metrics.DefaultMetric), + metricDefaultMetricToBeRemoved: newMetricDefaultMetricToBeRemoved(mbc.Metrics.DefaultMetricToBeRemoved), + metricOptionalMetric: newMetricOptionalMetric(mbc.Metrics.OptionalMetric), + metricOptionalMetricEmptyUnit: newMetricOptionalMetricEmptyUnit(mbc.Metrics.OptionalMetricEmptyUnit), + } + for _, op := range options { + op(mb) + } + return mb +} + +// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. +func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { + return NewResourceBuilder(mb.config.ResourceAttributes) +} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption func(pmetric.ResourceMetrics) + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + } +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + } +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(rmo ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + rm.SetSchemaUrl(conventions.SchemaURL) + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName("otelcol") + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricDefaultMetric.emit(ils.Metrics()) + mb.metricDefaultMetricToBeRemoved.emit(ils.Metrics()) + mb.metricOptionalMetric.emit(ils.Metrics()) + mb.metricOptionalMetricEmptyUnit.emit(ils.Metrics()) + + for _, op := range rmo { + op(rm) + } + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(rmo ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(rmo...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordDefaultMetricDataPoint adds a data point to default.metric metric. +func (mb *MetricsBuilder) RecordDefaultMetricDataPoint(ts pcommon.Timestamp, val int64, stringAttrAttributeValue string, overriddenIntAttrAttributeValue int64, enumAttrAttributeValue AttributeEnumAttr, sliceAttrAttributeValue []any, mapAttrAttributeValue map[string]any) { + mb.metricDefaultMetric.recordDataPoint(mb.startTime, ts, val, stringAttrAttributeValue, overriddenIntAttrAttributeValue, enumAttrAttributeValue.String(), sliceAttrAttributeValue, mapAttrAttributeValue) +} + +// RecordDefaultMetricToBeRemovedDataPoint adds a data point to default.metric.to_be_removed metric. +func (mb *MetricsBuilder) RecordDefaultMetricToBeRemovedDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricDefaultMetricToBeRemoved.recordDataPoint(mb.startTime, ts, val) +} + +// RecordOptionalMetricDataPoint adds a data point to optional.metric metric. +func (mb *MetricsBuilder) RecordOptionalMetricDataPoint(ts pcommon.Timestamp, val float64, stringAttrAttributeValue string, booleanAttrAttributeValue bool) { + mb.metricOptionalMetric.recordDataPoint(mb.startTime, ts, val, stringAttrAttributeValue, booleanAttrAttributeValue) +} + +// RecordOptionalMetricEmptyUnitDataPoint adds a data point to optional.metric.empty_unit metric. +func (mb *MetricsBuilder) RecordOptionalMetricEmptyUnitDataPoint(ts pcommon.Timestamp, val float64, stringAttrAttributeValue string, booleanAttrAttributeValue bool) { + mb.metricOptionalMetricEmptyUnit.recordDataPoint(mb.startTime, ts, val, stringAttrAttributeValue, booleanAttrAttributeValue) +} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...metricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op(mb) + } +} diff --git a/cmd/mdatagen/internal/metadata/generated_metrics_test.go b/cmd/mdatagen/internal/metadata/generated_metrics_test.go new file mode 100644 index 00000000000..4860ac1ba2b --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_metrics_test.go @@ -0,0 +1,215 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +type testConfigCollection int + +const ( + testSetDefault testConfigCollection = iota + testSetAll + testSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + configSet testConfigCollection + }{ + { + name: "default", + configSet: testSetDefault, + }, + { + name: "all_set", + configSet: testSetAll, + }, + { + name: "none_set", + configSet: testSetNone, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := receivertest.NewNopCreateSettings() + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, test.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + if test.configSet == testSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `default.metric`: This metric will be disabled by default soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetDefault || test.configSet == testSetAll { + assert.Equal(t, "[WARNING] `default.metric.to_be_removed` should not be enabled: This metric is deprecated and will be removed soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetAll || test.configSet == testSetNone { + assert.Equal(t, "[WARNING] `optional.metric` should not be configured: This metric is deprecated and will be removed soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetAll || test.configSet == testSetNone { + assert.Equal(t, "[WARNING] `optional.metric.empty_unit` should not be configured: This metric is deprecated and will be removed soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `string.resource.attr_disable_warning`: This resource_attribute will be disabled by default soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetAll || test.configSet == testSetNone { + assert.Equal(t, "[WARNING] `string.resource.attr_remove_warning` should not be configured: This resource_attribute is deprecated and will be removed soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.configSet == testSetDefault || test.configSet == testSetAll { + assert.Equal(t, "[WARNING] `string.resource.attr_to_be_removed` should not be enabled: This resource_attribute is deprecated and will be removed soon.", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordDefaultMetricDataPoint(ts, 1, "string_attr-val", 19, AttributeEnumAttrRed, []any{"slice_attr-item1", "slice_attr-item2"}, map[string]any{"key1": "map_attr-val1", "key2": "map_attr-val2"}) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordDefaultMetricToBeRemovedDataPoint(ts, 1) + + allMetricsCount++ + mb.RecordOptionalMetricDataPoint(ts, 1, "string_attr-val", true) + + allMetricsCount++ + mb.RecordOptionalMetricEmptyUnitDataPoint(ts, 1, "string_attr-val", true) + + rb := mb.NewResourceBuilder() + rb.SetMapResourceAttr(map[string]any{"key1": "map.resource.attr-val1", "key2": "map.resource.attr-val2"}) + rb.SetOptionalResourceAttr("optional.resource.attr-val") + rb.SetSliceResourceAttr([]any{"slice.resource.attr-item1", "slice.resource.attr-item2"}) + rb.SetStringEnumResourceAttrOne() + rb.SetStringResourceAttr("string.resource.attr-val") + rb.SetStringResourceAttrDisableWarning("string.resource.attr_disable_warning-val") + rb.SetStringResourceAttrRemoveWarning("string.resource.attr_remove_warning-val") + rb.SetStringResourceAttrToBeRemoved("string.resource.attr_to_be_removed-val") + res := rb.Emit() + metrics := mb.Emit(WithResource(res)) + + if test.configSet == testSetNone { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if test.configSet == testSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if test.configSet == testSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "default.metric": + assert.False(t, validatedMetrics["default.metric"], "Found a duplicate in the metrics slice: default.metric") + validatedMetrics["default.metric"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "Monotonic cumulative sum int metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, "s", ms.At(i).Unit()) + assert.Equal(t, true, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("string_attr") + assert.True(t, ok) + assert.EqualValues(t, "string_attr-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("state") + assert.True(t, ok) + assert.EqualValues(t, 19, attrVal.Int()) + attrVal, ok = dp.Attributes().Get("enum_attr") + assert.True(t, ok) + assert.EqualValues(t, "red", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("slice_attr") + assert.True(t, ok) + assert.EqualValues(t, []any{"slice_attr-item1", "slice_attr-item2"}, attrVal.Slice().AsRaw()) + attrVal, ok = dp.Attributes().Get("map_attr") + assert.True(t, ok) + assert.EqualValues(t, map[string]any{"key1": "map_attr-val1", "key2": "map_attr-val2"}, attrVal.Map().AsRaw()) + case "default.metric.to_be_removed": + assert.False(t, validatedMetrics["default.metric.to_be_removed"], "Found a duplicate in the metrics slice: default.metric.to_be_removed") + validatedMetrics["default.metric.to_be_removed"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "[DEPRECATED] Non-monotonic delta sum double metric enabled by default.", ms.At(i).Description()) + assert.Equal(t, "s", ms.At(i).Unit()) + assert.Equal(t, false, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityDelta, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + case "optional.metric": + assert.False(t, validatedMetrics["optional.metric"], "Found a duplicate in the metrics slice: optional.metric") + validatedMetrics["optional.metric"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "[DEPRECATED] Gauge double metric disabled by default.", ms.At(i).Description()) + assert.Equal(t, "1", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + attrVal, ok := dp.Attributes().Get("string_attr") + assert.True(t, ok) + assert.EqualValues(t, "string_attr-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("boolean_attr") + assert.True(t, ok) + assert.EqualValues(t, true, attrVal.Bool()) + case "optional.metric.empty_unit": + assert.False(t, validatedMetrics["optional.metric.empty_unit"], "Found a duplicate in the metrics slice: optional.metric.empty_unit") + validatedMetrics["optional.metric.empty_unit"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "[DEPRECATED] Gauge double metric disabled by default.", ms.At(i).Description()) + assert.Equal(t, "", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + attrVal, ok := dp.Attributes().Get("string_attr") + assert.True(t, ok) + assert.EqualValues(t, "string_attr-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("boolean_attr") + assert.True(t, ok) + assert.EqualValues(t, true, attrVal.Bool()) + } + } + }) + } +} diff --git a/cmd/mdatagen/internal/metadata/generated_resource.go b/cmd/mdatagen/internal/metadata/generated_resource.go new file mode 100644 index 00000000000..da69b3c0841 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_resource.go @@ -0,0 +1,92 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +// SetMapResourceAttr sets provided value as "map.resource.attr" attribute. +func (rb *ResourceBuilder) SetMapResourceAttr(val map[string]any) { + if rb.config.MapResourceAttr.Enabled { + rb.res.Attributes().PutEmptyMap("map.resource.attr").FromRaw(val) + } +} + +// SetOptionalResourceAttr sets provided value as "optional.resource.attr" attribute. +func (rb *ResourceBuilder) SetOptionalResourceAttr(val string) { + if rb.config.OptionalResourceAttr.Enabled { + rb.res.Attributes().PutStr("optional.resource.attr", val) + } +} + +// SetSliceResourceAttr sets provided value as "slice.resource.attr" attribute. +func (rb *ResourceBuilder) SetSliceResourceAttr(val []any) { + if rb.config.SliceResourceAttr.Enabled { + rb.res.Attributes().PutEmptySlice("slice.resource.attr").FromRaw(val) + } +} + +// SetStringEnumResourceAttrOne sets "string.enum.resource.attr=one" attribute. +func (rb *ResourceBuilder) SetStringEnumResourceAttrOne() { + if rb.config.StringEnumResourceAttr.Enabled { + rb.res.Attributes().PutStr("string.enum.resource.attr", "one") + } +} + +// SetStringEnumResourceAttrTwo sets "string.enum.resource.attr=two" attribute. +func (rb *ResourceBuilder) SetStringEnumResourceAttrTwo() { + if rb.config.StringEnumResourceAttr.Enabled { + rb.res.Attributes().PutStr("string.enum.resource.attr", "two") + } +} + +// SetStringResourceAttr sets provided value as "string.resource.attr" attribute. +func (rb *ResourceBuilder) SetStringResourceAttr(val string) { + if rb.config.StringResourceAttr.Enabled { + rb.res.Attributes().PutStr("string.resource.attr", val) + } +} + +// SetStringResourceAttrDisableWarning sets provided value as "string.resource.attr_disable_warning" attribute. +func (rb *ResourceBuilder) SetStringResourceAttrDisableWarning(val string) { + if rb.config.StringResourceAttrDisableWarning.Enabled { + rb.res.Attributes().PutStr("string.resource.attr_disable_warning", val) + } +} + +// SetStringResourceAttrRemoveWarning sets provided value as "string.resource.attr_remove_warning" attribute. +func (rb *ResourceBuilder) SetStringResourceAttrRemoveWarning(val string) { + if rb.config.StringResourceAttrRemoveWarning.Enabled { + rb.res.Attributes().PutStr("string.resource.attr_remove_warning", val) + } +} + +// SetStringResourceAttrToBeRemoved sets provided value as "string.resource.attr_to_be_removed" attribute. +func (rb *ResourceBuilder) SetStringResourceAttrToBeRemoved(val string) { + if rb.config.StringResourceAttrToBeRemoved.Enabled { + rb.res.Attributes().PutStr("string.resource.attr_to_be_removed", val) + } +} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/cmd/mdatagen/internal/metadata/generated_resource_test.go b/cmd/mdatagen/internal/metadata/generated_resource_test.go new file mode 100644 index 00000000000..c0bde40a087 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_resource_test.go @@ -0,0 +1,82 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBuilder(t *testing.T) { + for _, test := range []string{"default", "all_set", "none_set"} { + t.Run(test, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, test) + rb := NewResourceBuilder(cfg) + rb.SetMapResourceAttr(map[string]any{"key1": "map.resource.attr-val1", "key2": "map.resource.attr-val2"}) + rb.SetOptionalResourceAttr("optional.resource.attr-val") + rb.SetSliceResourceAttr([]any{"slice.resource.attr-item1", "slice.resource.attr-item2"}) + rb.SetStringEnumResourceAttrOne() + rb.SetStringResourceAttr("string.resource.attr-val") + rb.SetStringResourceAttrDisableWarning("string.resource.attr_disable_warning-val") + rb.SetStringResourceAttrRemoveWarning("string.resource.attr_remove_warning-val") + rb.SetStringResourceAttrToBeRemoved("string.resource.attr_to_be_removed-val") + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch test { + case "default": + assert.Equal(t, 6, res.Attributes().Len()) + case "all_set": + assert.Equal(t, 8, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", test) + } + + val, ok := res.Attributes().Get("map.resource.attr") + assert.True(t, ok) + if ok { + assert.EqualValues(t, map[string]any{"key1": "map.resource.attr-val1", "key2": "map.resource.attr-val2"}, val.Map().AsRaw()) + } + val, ok = res.Attributes().Get("optional.resource.attr") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, "optional.resource.attr-val", val.Str()) + } + val, ok = res.Attributes().Get("slice.resource.attr") + assert.True(t, ok) + if ok { + assert.EqualValues(t, []any{"slice.resource.attr-item1", "slice.resource.attr-item2"}, val.Slice().AsRaw()) + } + val, ok = res.Attributes().Get("string.enum.resource.attr") + assert.True(t, ok) + if ok { + assert.EqualValues(t, "one", val.Str()) + } + val, ok = res.Attributes().Get("string.resource.attr") + assert.True(t, ok) + if ok { + assert.EqualValues(t, "string.resource.attr-val", val.Str()) + } + val, ok = res.Attributes().Get("string.resource.attr_disable_warning") + assert.True(t, ok) + if ok { + assert.EqualValues(t, "string.resource.attr_disable_warning-val", val.Str()) + } + val, ok = res.Attributes().Get("string.resource.attr_remove_warning") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, "string.resource.attr_remove_warning-val", val.Str()) + } + val, ok = res.Attributes().Get("string.resource.attr_to_be_removed") + assert.True(t, ok) + if ok { + assert.EqualValues(t, "string.resource.attr_to_be_removed-val", val.Str()) + } + }) + } +} diff --git a/cmd/mdatagen/internal/metadata/generated_status.go b/cmd/mdatagen/internal/metadata/generated_status.go new file mode 100644 index 00000000000..02b54824865 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/generated_status.go @@ -0,0 +1,25 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + + "go.opentelemetry.io/collector/component" +) + +const ( + Type = "file" + TracesStability = component.StabilityLevelBeta + LogsStability = component.StabilityLevelDevelopment + MetricsStability = component.StabilityLevelStable +) + +func Meter(settings component.TelemetrySettings) metric.Meter { + return settings.MeterProvider.Meter("otelcol") +} + +func Tracer(settings component.TelemetrySettings) trace.Tracer { + return settings.TracerProvider.Tracer("otelcol") +} diff --git a/cmd/mdatagen/internal/metadata/testdata/config.yaml b/cmd/mdatagen/internal/metadata/testdata/config.yaml new file mode 100644 index 00000000000..8283dc2dd70 --- /dev/null +++ b/cmd/mdatagen/internal/metadata/testdata/config.yaml @@ -0,0 +1,55 @@ +default: +all_set: + metrics: + default.metric: + enabled: true + default.metric.to_be_removed: + enabled: true + optional.metric: + enabled: true + optional.metric.empty_unit: + enabled: true + resource_attributes: + map.resource.attr: + enabled: true + optional.resource.attr: + enabled: true + slice.resource.attr: + enabled: true + string.enum.resource.attr: + enabled: true + string.resource.attr: + enabled: true + string.resource.attr_disable_warning: + enabled: true + string.resource.attr_remove_warning: + enabled: true + string.resource.attr_to_be_removed: + enabled: true +none_set: + metrics: + default.metric: + enabled: false + default.metric.to_be_removed: + enabled: false + optional.metric: + enabled: false + optional.metric.empty_unit: + enabled: false + resource_attributes: + map.resource.attr: + enabled: false + optional.resource.attr: + enabled: false + slice.resource.attr: + enabled: false + string.enum.resource.attr: + enabled: false + string.resource.attr: + enabled: false + string.resource.attr_disable_warning: + enabled: false + string.resource.attr_remove_warning: + enabled: false + string.resource.attr_to_be_removed: + enabled: false diff --git a/cmd/mdatagen/lint.go b/cmd/mdatagen/lint.go new file mode 100644 index 00000000000..f169ac4fdbb --- /dev/null +++ b/cmd/mdatagen/lint.go @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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) // nolint SA1019 + 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 +} diff --git a/cmd/mdatagen/lint_test.go b/cmd/mdatagen/lint_test.go new file mode 100644 index 00000000000..5c470f302d7 --- /dev/null +++ b/cmd/mdatagen/lint_test.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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) + } + }) + } +} diff --git a/cmd/mdatagen/loader.go b/cmd/mdatagen/loader.go new file mode 100644 index 00000000000..701c008138b --- /dev/null +++ b/cmd/mdatagen/loader.go @@ -0,0 +1,308 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/provider/fileprovider" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +type metricName string + +func (mn metricName) Render() (string, error) { + return formatIdentifier(string(mn), true) +} + +func (mn metricName) RenderUnexported() (string, error) { + return formatIdentifier(string(mn), false) +} + +type attributeName string + +func (mn attributeName) Render() (string, error) { + return formatIdentifier(string(mn), true) +} + +func (mn attributeName) RenderUnexported() (string, error) { + return formatIdentifier(string(mn), false) +} + +// ValueType defines an attribute value type. +type ValueType struct { + // ValueType is type of the attribute value. + ValueType pcommon.ValueType +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (mvt *ValueType) UnmarshalText(text []byte) error { + switch vtStr := string(text); vtStr { + case "string": + mvt.ValueType = pcommon.ValueTypeStr + case "int": + mvt.ValueType = pcommon.ValueTypeInt + case "double": + mvt.ValueType = pcommon.ValueTypeDouble + case "bool": + mvt.ValueType = pcommon.ValueTypeBool + case "bytes": + mvt.ValueType = pcommon.ValueTypeBytes + case "slice": + mvt.ValueType = pcommon.ValueTypeSlice + case "map": + mvt.ValueType = pcommon.ValueTypeMap + default: + return fmt.Errorf("invalid type: %q", vtStr) + } + return nil +} + +// String returns capitalized name of the ValueType. +func (mvt ValueType) String() string { + return strings.Title(strings.ToLower(mvt.ValueType.String())) // nolint SA1019 +} + +// Primitive returns name of primitive type for the ValueType. +func (mvt ValueType) Primitive() string { + switch mvt.ValueType { + case pcommon.ValueTypeStr: + return "string" + case pcommon.ValueTypeInt: + return "int64" + case pcommon.ValueTypeDouble: + return "float64" + case pcommon.ValueTypeBool: + return "bool" + case pcommon.ValueTypeBytes: + return "[]byte" + case pcommon.ValueTypeSlice: + return "[]any" + case pcommon.ValueTypeMap: + return "map[string]any" + case pcommon.ValueTypeEmpty: + return "" + default: + return "" + } +} + +type metric struct { + // Enabled defines whether the metric is enabled by default. + Enabled bool `mapstructure:"enabled"` + + // Warnings that will be shown to user under specified conditions. + Warnings warnings `mapstructure:"warnings"` + + // Description of the metric. + Description string `mapstructure:"description"` + + // ExtendedDocumentation of the metric. If specified, this will + // be appended to the description used in generated documentation. + ExtendedDocumentation string `mapstructure:"extended_documentation"` + + // Unit of the metric. + Unit *string `mapstructure:"unit"` + + // Sum stores metadata for sum metric type + Sum *sum `mapstructure:"sum,omitempty"` + // Gauge stores metadata for gauge metric type + Gauge *gauge `mapstructure:"gauge,omitempty"` + + // Attributes is the list of attributes that the metric emits. + Attributes []attributeName `mapstructure:"attributes"` +} + +func (m *metric) Unmarshal(parser *confmap.Conf) error { + if !parser.IsSet("enabled") { + return errors.New("missing required field: `enabled`") + } + err := parser.Unmarshal(m, confmap.WithErrorUnused()) + if err != nil { + return err + } + return nil +} +func (m metric) Data() MetricData { + if m.Sum != nil { + return m.Sum + } + if m.Gauge != nil { + return m.Gauge + } + return nil +} + +type warnings struct { + // A warning that will be displayed if the field is enabled in user config. + IfEnabled string `mapstructure:"if_enabled"` + // A warning that will be displayed if `enabled` field is not set explicitly in user config. + IfEnabledNotSet string `mapstructure:"if_enabled_not_set"` + // A warning that will be displayed if the field is configured by user in any way. + IfConfigured string `mapstructure:"if_configured"` +} + +type attribute struct { + // Description describes the purpose of the attribute. + Description string `mapstructure:"description"` + // NameOverride can be used to override the attribute name. + NameOverride string `mapstructure:"name_override"` + // Enabled defines whether the attribute is enabled by default. + Enabled bool `mapstructure:"enabled"` + // Enum can optionally describe the set of values to which the attribute can belong. + Enum []string `mapstructure:"enum"` + // Type is an attribute type. + Type ValueType `mapstructure:"type"` + // FullName is the attribute name populated from the map key. + FullName attributeName `mapstructure:"-"` + // Warnings that will be shown to user under specified conditions. + Warnings warnings `mapstructure:"warnings"` +} + +// Name returns actual name of the attribute that is set on the metric after applying NameOverride. +func (a attribute) Name() attributeName { + if a.NameOverride != "" { + return attributeName(a.NameOverride) + } + return a.FullName +} + +func (a attribute) TestValue() string { + if a.Enum != nil { + return fmt.Sprintf(`"%s"`, a.Enum[0]) + } + switch a.Type.ValueType { + case pcommon.ValueTypeEmpty: + return "" + case pcommon.ValueTypeStr: + return fmt.Sprintf(`"%s-val"`, a.FullName) + case pcommon.ValueTypeInt: + return fmt.Sprintf("%d", len(a.FullName)) + case pcommon.ValueTypeDouble: + return fmt.Sprintf("%f", 0.1+float64(len(a.FullName))) + case pcommon.ValueTypeBool: + return fmt.Sprintf("%t", len(a.FullName)%2 == 0) + case pcommon.ValueTypeMap: + return fmt.Sprintf(`map[string]any{"key1": "%s-val1", "key2": "%s-val2"}`, a.FullName, a.FullName) + case pcommon.ValueTypeSlice: + return fmt.Sprintf(`[]any{"%s-item1", "%s-item2"}`, a.FullName, a.FullName) + case pcommon.ValueTypeBytes: + return fmt.Sprintf(`bytes("%s-val")`, a.FullName) + } + return "" +} + +type tests struct { + Config any `mapstructure:"config"` + SkipLifecycle bool `mapstructure:"skip_lifecycle"` + ExpectConsumerError bool `mapstructure:"expect_consumer_error"` +} + +type metadata struct { + // Type of the component. + Type string `mapstructure:"type"` + // Type of the parent component (applicable to subcomponents). + Parent string `mapstructure:"parent"` + // Status information for the component. + Status *Status `mapstructure:"status"` + // SemConvVersion is a version number of OpenTelemetry semantic conventions applied to the scraped metrics. + SemConvVersion string `mapstructure:"sem_conv_version"` + // ResourceAttributes that can be emitted by the component. + ResourceAttributes map[attributeName]attribute `mapstructure:"resource_attributes"` + // Attributes emitted by one or more metrics. + Attributes map[attributeName]attribute `mapstructure:"attributes"` + // Metrics that can be emitted by the component. + Metrics map[metricName]metric `mapstructure:"metrics"` + // ScopeName of the metrics emitted by the component. + ScopeName string `mapstructure:"-"` + // ShortFolderName is the shortened folder name of the component, removing class if present + ShortFolderName string `mapstructure:"-"` + + Tests *tests `mapstructure:"tests"` +} + +func setAttributesFullName(attrs map[attributeName]attribute) { + for k, v := range attrs { + v.FullName = k + attrs[k] = v + } +} + +type templateContext struct { + metadata + // Package name for generated code. + Package string +} + +func loadMetadata(filePath string) (metadata, error) { + cp, err := fileprovider.New().Retrieve(context.Background(), "file:"+filePath, nil) + if err != nil { + return metadata{}, err + } + + conf, err := cp.AsConf() + if err != nil { + return metadata{}, err + } + + md := metadata{ScopeName: scopeName(filePath), ShortFolderName: shortFolderName(filePath)} + if err := conf.Unmarshal(&md, confmap.WithErrorUnused()); err != nil { + return md, err + } + + if err := md.Validate(); err != nil { + return md, err + } + + setAttributesFullName(md.Attributes) + setAttributesFullName(md.ResourceAttributes) + + return md, nil +} + +var componentTypes = map[string]func(string) string{ + "connector": func(in string) string { return strings.TrimSuffix(in, "connector") }, + "exporter": func(in string) string { return strings.TrimSuffix(in, "exporter") }, + "extension": func(in string) string { return strings.TrimSuffix(in, "extension") }, + "processor": func(in string) string { return strings.TrimSuffix(in, "processor") }, + "scraper": func(in string) string { return strings.TrimSuffix(in, "scraper") }, + "receiver": func(in string) string { return in }, +} + +func shortFolderName(filePath string) string { + parentFolder := filepath.Base(filepath.Dir(filePath)) + for cType := range componentTypes { + if strings.HasSuffix(parentFolder, cType) { + return strings.TrimSuffix(parentFolder, cType) + } + } + return parentFolder +} + +func scopeName(filePath string) string { + sn := "otelcol" + dirs := strings.Split(filepath.Dir(filePath), string(os.PathSeparator)) + for _, dir := range dirs { + // skip directory names for component types + if _, ok := componentTypes[dir]; ok { + continue + } + // note here that the only component that receives a different + // treatment is receivers. this is to prevent breaking backwards + // compatibility for anyone that's using the generated metrics w/ + // scope names today. + for cType, normalizeFunc := range componentTypes { + if strings.HasSuffix(dir, cType) { + sn += "/" + normalizeFunc(dir) + } + } + } + return sn +} diff --git a/cmd/mdatagen/loader_test.go b/cmd/mdatagen/loader_test.go new file mode 100644 index 00000000000..3cb037adae1 --- /dev/null +++ b/cmd/mdatagen/loader_test.go @@ -0,0 +1,282 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +func Test_loadMetadata(t *testing.T) { + tests := []struct { + name string + want metadata + wantErr string + }{ + { + name: "metadata-sample.yaml", + want: metadata{ + Type: "file", + SemConvVersion: "1.9.0", + Status: &Status{ + Class: "receiver", + Stability: map[string][]string{ + "development": {"logs"}, + "beta": {"traces"}, + "stable": {"metrics"}, + }, + Distributions: []string{"contrib"}, + Warnings: []string{"Any additional information that should be brought to the consumer's attention"}, + }, + ResourceAttributes: map[attributeName]attribute{ + "string.resource.attr": { + Description: "Resource attribute with any string value.", + Enabled: true, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string.resource.attr", + }, + "string.enum.resource.attr": { + Description: "Resource attribute with a known set of string values.", + Enabled: true, + Enum: []string{"one", "two"}, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string.enum.resource.attr", + }, + "optional.resource.attr": { + Description: "Explicitly disabled ResourceAttribute.", + Enabled: false, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "optional.resource.attr", + }, + "slice.resource.attr": { + Description: "Resource attribute with a slice value.", + Enabled: true, + Type: ValueType{ + ValueType: pcommon.ValueTypeSlice, + }, + FullName: "slice.resource.attr", + }, + "map.resource.attr": { + Description: "Resource attribute with a map value.", + Enabled: true, + Type: ValueType{ + ValueType: pcommon.ValueTypeMap, + }, + FullName: "map.resource.attr", + }, + "string.resource.attr_disable_warning": { + Description: "Resource attribute with any string value.", + Warnings: warnings{ + IfEnabledNotSet: "This resource_attribute will be disabled by default soon.", + }, + Enabled: true, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string.resource.attr_disable_warning", + }, + "string.resource.attr_remove_warning": { + Description: "Resource attribute with any string value.", + Warnings: warnings{ + IfConfigured: "This resource_attribute is deprecated and will be removed soon.", + }, + Enabled: false, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string.resource.attr_remove_warning", + }, + "string.resource.attr_to_be_removed": { + Description: "Resource attribute with any string value.", + Warnings: warnings{ + IfEnabled: "This resource_attribute is deprecated and will be removed soon.", + }, + Enabled: true, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string.resource.attr_to_be_removed", + }, + }, + + Attributes: map[attributeName]attribute{ + "enum_attr": { + Description: "Attribute with a known set of string values.", + NameOverride: "", + Enum: []string{"red", "green", "blue"}, + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "enum_attr", + }, + "string_attr": { + Description: "Attribute with any string value.", + NameOverride: "", + Type: ValueType{ + ValueType: pcommon.ValueTypeStr, + }, + FullName: "string_attr", + }, + "overridden_int_attr": { + Description: "Integer attribute with overridden name.", + NameOverride: "state", + Type: ValueType{ + ValueType: pcommon.ValueTypeInt, + }, + FullName: "overridden_int_attr", + }, + "boolean_attr": { + Description: "Attribute with a boolean value.", + Type: ValueType{ + ValueType: pcommon.ValueTypeBool, + }, + FullName: "boolean_attr", + }, + "slice_attr": { + Description: "Attribute with a slice value.", + Type: ValueType{ + ValueType: pcommon.ValueTypeSlice, + }, + FullName: "slice_attr", + }, + "map_attr": { + Description: "Attribute with a map value.", + Type: ValueType{ + ValueType: pcommon.ValueTypeMap, + }, + FullName: "map_attr", + }, + }, + Metrics: map[metricName]metric{ + "default.metric": { + Enabled: true, + Description: "Monotonic cumulative sum int metric enabled by default.", + ExtendedDocumentation: "The metric will be become optional soon.", + Warnings: warnings{ + IfEnabledNotSet: "This metric will be disabled by default soon.", + }, + Unit: strPtr("s"), + Sum: &sum{ + MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeInt}, + AggregationTemporality: AggregationTemporality{Aggregation: pmetric.AggregationTemporalityCumulative}, + Mono: Mono{Monotonic: true}, + }, + Attributes: []attributeName{"string_attr", "overridden_int_attr", "enum_attr", "slice_attr", "map_attr"}, + }, + "optional.metric": { + Enabled: false, + Description: "[DEPRECATED] Gauge double metric disabled by default.", + Warnings: warnings{ + IfConfigured: "This metric is deprecated and will be removed soon.", + }, + Unit: strPtr("1"), + Gauge: &gauge{ + MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeDouble}, + }, + Attributes: []attributeName{"string_attr", "boolean_attr"}, + }, + "optional.metric.empty_unit": { + Enabled: false, + Description: "[DEPRECATED] Gauge double metric disabled by default.", + Warnings: warnings{ + IfConfigured: "This metric is deprecated and will be removed soon.", + }, + Unit: strPtr(""), + Gauge: &gauge{ + MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeDouble}, + }, + Attributes: []attributeName{"string_attr", "boolean_attr"}, + }, + + "default.metric.to_be_removed": { + Enabled: true, + Description: "[DEPRECATED] Non-monotonic delta sum double metric enabled by default.", + ExtendedDocumentation: "The metric will be will be removed soon.", + Warnings: warnings{ + IfEnabled: "This metric is deprecated and will be removed soon.", + }, + Unit: strPtr("s"), + Sum: &sum{ + MetricValueType: MetricValueType{pmetric.NumberDataPointValueTypeDouble}, + AggregationTemporality: AggregationTemporality{Aggregation: pmetric.AggregationTemporalityDelta}, + Mono: Mono{Monotonic: false}, + }, + }, + }, + ScopeName: "otelcol", + ShortFolderName: ".", + }, + }, + { + name: "testdata/parent.yaml", + want: metadata{ + Type: "subcomponent", + Parent: "parentComponent", + ScopeName: "otelcol", + ShortFolderName: "testdata", + }, + }, + { + name: "testdata/invalid_type_rattr.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'resource_attributes[string.resource.attr].type': invalid type: \"invalidtype\"", + }, + { + name: "testdata/no_enabled.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': missing required field: `enabled`", + }, + { + name: "testdata/no_value_type.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n" + + "* error decoding 'sum': missing required field: `value_type`", + }, + { + name: "testdata/unknown_value_type.yaml", + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'value_type': invalid value_type: \"unknown\"", + }, + { + name: "testdata/no_aggregation.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': missing required field: `aggregation_temporality`", + }, + { + name: "testdata/invalid_aggregation.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'aggregation_temporality': invalid aggregation: \"invalidaggregation\"", + }, + { + name: "testdata/invalid_type_attr.yaml", + want: metadata{}, + wantErr: "1 error(s) decoding:\n\n* error decoding 'attributes[used_attr].type': invalid type: \"invalidtype\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := loadMetadata(tt.name) + if tt.wantErr != "" { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/cmd/mdatagen/main.go b/cmd/mdatagen/main.go new file mode 100644 index 00000000000..fc1c732dce5 --- /dev/null +++ b/cmd/mdatagen/main.go @@ -0,0 +1,292 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "go/format" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +const ( + statusStart = "" + statusEnd = "" +) + +func main() { + flag.Parse() + yml := flag.Arg(0) + if err := run(yml); err != nil { + log.Fatal(err) + } +} + +func run(ymlPath string) error { + if ymlPath == "" { + return errors.New("argument must be metadata.yaml file") + } + ymlPath, err := filepath.Abs(ymlPath) + if err != nil { + return fmt.Errorf("failed to get absolute path for %v: %w", ymlPath, err) + } + + ymlDir := filepath.Dir(ymlPath) + + md, err := loadMetadata(ymlPath) + if err != nil { + return fmt.Errorf("failed loading %v: %w", ymlPath, err) + } + + tmplDir := "templates" + + codeDir := filepath.Join(ymlDir, "internal", "metadata") + if err = os.MkdirAll(codeDir, 0700); err != nil { + return fmt.Errorf("unable to create output directory %q: %w", codeDir, err) + } + if md.Status != nil { + if md.Status.Class != "cmd" && md.Status.Class != "pkg" { + if err = generateFile(filepath.Join(tmplDir, "status.go.tmpl"), + filepath.Join(codeDir, "generated_status.go"), md, "metadata"); err != nil { + return err + } + } + + if _, err = os.Stat(filepath.Join(ymlDir, "README.md")); err == nil { + if err = inlineReplace( + filepath.Join(tmplDir, "readme.md.tmpl"), + filepath.Join(ymlDir, "README.md"), + md, statusStart, statusEnd); err != nil { + return err + } + } + } + + if md.Tests != nil { + if err = generateFile(filepath.Join(tmplDir, "component_test.go.tmpl"), + filepath.Join(ymlDir, "generated_component_test.go"), md, md.ShortFolderName+md.Status.Class); err != nil { + return err + } + } + + if len(md.Metrics) == 0 && len(md.ResourceAttributes) == 0 { + return nil + } + + if err = os.MkdirAll(filepath.Join(codeDir, "testdata"), 0700); err != nil { + return fmt.Errorf("unable to create output directory %q: %w", filepath.Join(codeDir, "testdata"), err) + } + if err = generateFile(filepath.Join(tmplDir, "testdata", "config.yaml.tmpl"), + filepath.Join(codeDir, "testdata", "config.yaml"), md, "metadata"); err != nil { + return err + } + + if err = generateFile(filepath.Join(tmplDir, "config.go.tmpl"), + filepath.Join(codeDir, "generated_config.go"), md, "metadata"); err != nil { + return err + } + if err = generateFile(filepath.Join(tmplDir, "config_test.go.tmpl"), + filepath.Join(codeDir, "generated_config_test.go"), md, "metadata"); err != nil { + return err + } + + if len(md.ResourceAttributes) > 0 { + if err = generateFile(filepath.Join(tmplDir, "resource.go.tmpl"), + filepath.Join(codeDir, "generated_resource.go"), md, "metadata"); err != nil { + return err + } + if err = generateFile(filepath.Join(tmplDir, "resource_test.go.tmpl"), + filepath.Join(codeDir, "generated_resource_test.go"), md, "metadata"); err != nil { + return err + } + } + + if len(md.Metrics) == 0 { + return nil + } + + if err = generateFile(filepath.Join(tmplDir, "metrics.go.tmpl"), + filepath.Join(codeDir, "generated_metrics.go"), md, "metadata"); err != nil { + return err + } + if err = generateFile(filepath.Join(tmplDir, "metrics_test.go.tmpl"), + filepath.Join(codeDir, "generated_metrics_test.go"), md, "metadata"); err != nil { + return err + } + + return generateFile(filepath.Join(tmplDir, "documentation.md.tmpl"), filepath.Join(ymlDir, "documentation.md"), md, "metadata") +} + +func templatize(tmplFile string, md metadata) *template.Template { + return template.Must( + template. + New(filepath.Base(tmplFile)). + Option("missingkey=error"). + Funcs(map[string]any{ + "publicVar": func(s string) (string, error) { + return formatIdentifier(s, true) + }, + "attributeInfo": func(an attributeName) attribute { + return md.Attributes[an] + }, + "metricInfo": func(mn metricName) metric { + return md.Metrics[mn] + }, + "parseImportsRequired": func(metrics map[metricName]metric) bool { + for _, m := range metrics { + if m.Data().HasMetricInputType() { + return true + } + } + return false + }, + "stringsJoin": strings.Join, + "stringsSplit": strings.Split, + "userLinks": func(elems []string) []string { + result := make([]string, len(elems)) + for i, elem := range elems { + if elem == "open-telemetry/collector-approvers" { + result[i] = "[@open-telemetry/collector-approvers](https://github.com/orgs/open-telemetry/teams/collector-approvers)" + } else { + result[i] = fmt.Sprintf("[@%s](https://www.github.com/%s)", elem, elem) + } + } + return result + }, + "casesTitle": cases.Title(language.English).String, + "toCamelCase": func(s string) string { + caser := cases.Title(language.English).String + parts := strings.Split(s, "_") + result := "" + for _, part := range parts { + result += caser(part) + } + return result + }, + "inc": func(i int) int { return i + 1 }, + "distroURL": func(name string) string { + return distros[name] + }, + "isExporter": func() bool { + return md.Status.Class == "exporter" + }, + "isProcessor": func() bool { + return md.Status.Class == "processor" + }, + "isReceiver": func() bool { + return md.Status.Class == "receiver" + }, + "isExtension": func() bool { + return md.Status.Class == "extension" + }, + "skipLifecycle": func() bool { + return md.Tests.SkipLifecycle + }, + "supportsLogs": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "logs" { + return true + } + } + } + return false + }, + "supportsMetrics": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "metrics" { + return true + } + } + } + return false + }, + "supportsTraces": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "traces" { + return true + } + } + } + return false + }, + "expectConsumerError": func() bool { + return md.Tests.ExpectConsumerError + }, + // ParseFS delegates the parsing of the files to `Glob` + // which uses the `\` as a special character. + // Meaning on windows based machines, the `\` needs to be replaced + // with a `/` for it to find the file. + }).ParseFS(templateFS, strings.ReplaceAll(tmplFile, "\\", "/"))) +} + +func inlineReplace(tmplFile string, outputFile string, md metadata, start string, end string) error { + var readmeContents []byte + var err error + if readmeContents, err = os.ReadFile(outputFile); err != nil { // nolint: gosec + return err + } + + var re = regexp.MustCompile(fmt.Sprintf("%s[\\s\\S]*%s", start, end)) + if !re.Match(readmeContents) { + return nil + } + + tmpl := templatize(tmplFile, md) + buf := bytes.Buffer{} + + if err := tmpl.Execute(&buf, templateContext{metadata: md, Package: "metadata"}); err != nil { + return fmt.Errorf("failed executing template: %w", err) + } + + result := buf.String() + + s := re.ReplaceAllString(string(readmeContents), result) + if err := os.WriteFile(outputFile, []byte(s), 0600); err != nil { + return fmt.Errorf("failed writing %q: %w", outputFile, err) + } + + return nil +} + +func generateFile(tmplFile string, outputFile string, md metadata, goPackage string) error { + tmpl := templatize(tmplFile, md) + buf := bytes.Buffer{} + + if err := tmpl.Execute(&buf, templateContext{metadata: md, Package: goPackage}); err != nil { + return fmt.Errorf("failed executing template: %w", err) + } + + if err := os.Remove(outputFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("unable to remove genererated file %q: %w", outputFile, err) + } + + result := buf.Bytes() + var formatErr error + if strings.HasSuffix(outputFile, ".go") { + if formatted, err := format.Source(buf.Bytes()); err == nil { + result = formatted + } else { + formatErr = fmt.Errorf("failed formatting %s:%w", outputFile, err) + } + } + + if err := os.WriteFile(outputFile, result, 0600); err != nil { + return fmt.Errorf("failed writing %q: %w", outputFile, err) + } + + return formatErr +} diff --git a/cmd/mdatagen/main_test.go b/cmd/mdatagen/main_test.go new file mode 100644 index 00000000000..56f4413aa49 --- /dev/null +++ b/cmd/mdatagen/main_test.go @@ -0,0 +1,399 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + md "go.opentelemetry.io/collector/cmd/mdatagen/internal/metadata" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +func Test_runContents(t *testing.T) { + tests := []struct { + yml string + wantMetricsGenerated bool + wantConfigGenerated bool + wantStatusGenerated bool + wantErr bool + }{ + { + yml: "invalid.yaml", + wantErr: true, + }, + { + yml: "metrics_and_type.yaml", + wantMetricsGenerated: true, + wantConfigGenerated: true, + wantStatusGenerated: true, + }, + { + yml: "resource_attributes_only.yaml", + wantConfigGenerated: true, + wantStatusGenerated: true, + }, + { + yml: "status_only.yaml", + wantStatusGenerated: true, + }, + } + for _, tt := range tests { + t.Run(tt.yml, func(t *testing.T) { + tmpdir := t.TempDir() + ymlContent, err := os.ReadFile(filepath.Join("testdata", tt.yml)) + require.NoError(t, err) + metadataFile := filepath.Join(tmpdir, "metadata.yaml") + require.NoError(t, os.WriteFile(metadataFile, ymlContent, 0600)) + require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "README.md"), []byte(` + +foo +`), 0600)) + + err = run(metadataFile) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + if tt.wantMetricsGenerated { + require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_metrics.go")) + require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_metrics_test.go")) + require.FileExists(t, filepath.Join(tmpdir, "documentation.md")) + } else { + require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_metrics.go")) + require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_metrics_test.go")) + require.NoFileExists(t, filepath.Join(tmpdir, "documentation.md")) + } + + if tt.wantConfigGenerated { + require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_config.go")) + require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_config_test.go")) + } else { + require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_config.go")) + require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_config_test.go")) + } + + if tt.wantStatusGenerated { + require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_status.go")) + contents, err := os.ReadFile(filepath.Join(tmpdir, "README.md")) // nolint: gosec + require.NoError(t, err) + require.NotContains(t, string(contents), "foo") + } else { + require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_status.go")) + contents, err := os.ReadFile(filepath.Join(tmpdir, "README.md")) // nolint: gosec + require.NoError(t, err) + require.Contains(t, string(contents), "foo") + } + }) + } +} + +func Test_run(t *testing.T) { + type args struct { + ymlPath string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "no argument", + args: args{""}, + wantErr: true, + }, + { + name: "no such file", + args: args{"/no/such/file"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := run(tt.args.ymlPath); (err != nil) != tt.wantErr { + t.Errorf("run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_inlineReplace(t *testing.T) { + tests := []struct { + name string + markdown string + outputFile string + componentClass string + warnings []string + stability map[string][]string + distros []string + codeowners *Codeowners + }{ + { + name: "readme with empty status", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_status.md", + componentClass: "receiver", + distros: []string{"contrib"}, + }, + { + name: "readme with status for extension", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_status_extension.md", + componentClass: "extension", + distros: []string{"contrib"}, + }, + { + name: "readme with status with codeowners and emeritus", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_status_codeowners_and_emeritus.md", + componentClass: "receiver", + distros: []string{"contrib"}, + codeowners: &Codeowners{ + Active: []string{"foo"}, + Emeritus: []string{"bar"}, + }, + }, + { + name: "readme with status with codeowners", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_status_codeowners.md", + componentClass: "receiver", + distros: []string{"contrib"}, + codeowners: &Codeowners{ + Active: []string{"foo"}, + }, + }, + { + name: "readme with status table", + markdown: `# Some component + + +| Status | | +| ------------------------ |-----------| + + +Some info about a component +`, + outputFile: "readme_with_status.md", + componentClass: "receiver", + distros: []string{"contrib"}, + }, + { + name: "readme with no status", + markdown: `# Some component + +Some info about a component +`, + outputFile: "readme_without_status.md", + distros: []string{"contrib"}, + }, + { + name: "component with warnings", + markdown: `# Some component + + + + +Some info about a component +### warnings +Some warning there. +`, + outputFile: "readme_with_warnings.md", + warnings: []string{"warning1"}, + distros: []string{"contrib"}, + }, + { + name: "readme with multiple signals", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_multiple_signals.md", + stability: map[string][]string{"beta": {"metrics"}, "alpha": {"logs"}}, + distros: []string{"contrib"}, + }, + { + name: "readme with cmd class", + markdown: `# Some component + + + + +Some info about a component +`, + outputFile: "readme_with_cmd_class.md", + stability: map[string][]string{"beta": {"metrics"}, "alpha": {"logs"}}, + componentClass: "cmd", + distros: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stability := map[string][]string{"beta": {"metrics"}} + if len(tt.stability) > 0 { + stability = tt.stability + } + md := metadata{ + Type: "foo", + ShortFolderName: "foo", + Status: &Status{ + Stability: stability, + Distributions: tt.distros, + Class: tt.componentClass, + Warnings: tt.warnings, + Codeowners: tt.codeowners, + }, + } + tmpdir := t.TempDir() + + readmeFile := filepath.Join(tmpdir, "README.md") + require.NoError(t, os.WriteFile(readmeFile, []byte(tt.markdown), 0600)) + + err := inlineReplace("templates/readme.md.tmpl", readmeFile, md, statusStart, statusEnd) + require.NoError(t, err) + + require.FileExists(t, filepath.Join(tmpdir, "README.md")) + got, err := os.ReadFile(filepath.Join(tmpdir, "README.md")) // nolint: gosec + require.NoError(t, err) + got = bytes.ReplaceAll(got, []byte("\r\n"), []byte("\n")) + expected, err := os.ReadFile(filepath.Join("testdata", tt.outputFile)) + require.NoError(t, err) + expected = bytes.ReplaceAll(expected, []byte("\r\n"), []byte("\n")) + fmt.Println(string(got)) + fmt.Println(string(expected)) + require.Equal(t, string(expected), string(got)) + }) + } +} + +func TestGenerateStatusMetadata(t *testing.T) { + tests := []struct { + name string + output string + md metadata + expected string + }{ + { + name: "foo component with beta status", + md: metadata{ + Type: "foo", + Status: &Status{ + Stability: map[string][]string{"beta": {"metrics"}}, + Distributions: []string{"contrib"}, + Class: "receiver", + }, + }, + expected: `// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" +) + +const ( + Type = "foo" + MetricsStability = component.StabilityLevelBeta +) + +func Meter(settings component.TelemetrySettings) metric.Meter { + return settings.MeterProvider.Meter("") +} + +func Tracer(settings component.TelemetrySettings) trace.Tracer { + return settings.TracerProvider.Tracer("") +} +`, + }, + { + name: "foo component with alpha status", + md: metadata{ + Type: "foo", + Status: &Status{ + Stability: map[string][]string{"alpha": {"metrics"}}, + Distributions: []string{"contrib"}, + Class: "receiver", + }, + }, + expected: `// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" +) + +const ( + Type = "foo" + MetricsStability = component.StabilityLevelAlpha +) + +func Meter(settings component.TelemetrySettings) metric.Meter { + return settings.MeterProvider.Meter("") +} + +func Tracer(settings component.TelemetrySettings) trace.Tracer { + return settings.TracerProvider.Tracer("") +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := t.TempDir() + err := generateFile("templates/status.go.tmpl", + filepath.Join(tmpdir, "generated_status.go"), tt.md, "metadata") + require.NoError(t, err) + actual, err := os.ReadFile(filepath.Join(tmpdir, "generated_status.go")) // nolint: gosec + require.NoError(t, err) + require.Equal(t, tt.expected, string(actual)) + }) + } +} + +// TestGenerated verifies that the internal/metadata API is generated correctly. +func TestGenerated(t *testing.T) { + mb := md.NewMetricsBuilder(md.DefaultMetricsBuilderConfig(), receivertest.NewNopCreateSettings()) + m := mb.Emit() + require.Equal(t, 0, m.ResourceMetrics().Len()) +} diff --git a/cmd/mdatagen/metadata-sample.yaml b/cmd/mdatagen/metadata-sample.yaml new file mode 100644 index 00000000000..b9a5f8f6c90 --- /dev/null +++ b/cmd/mdatagen/metadata-sample.yaml @@ -0,0 +1,136 @@ +# Sample metric metadata file with all available configurations. + +type: file + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +resource_attributes: + string.resource.attr: + description: Resource attribute with any string value. + type: string + enabled: true + + string.enum.resource.attr: + description: Resource attribute with a known set of string values. + type: string + enum: [one, two] + enabled: true + + optional.resource.attr: + description: Explicitly disabled ResourceAttribute. + type: string + enabled: false + + slice.resource.attr: + description: Resource attribute with a slice value. + type: slice + enabled: true + + map.resource.attr: + description: Resource attribute with a map value. + type: map + enabled: true + + string.resource.attr_disable_warning: + description: Resource attribute with any string value. + type: string + enabled: true + warnings: + if_enabled_not_set: This resource_attribute will be disabled by default soon. + + string.resource.attr_remove_warning: + description: Resource attribute with any string value. + type: string + enabled: false + warnings: + if_configured: This resource_attribute is deprecated and will be removed soon. + + string.resource.attr_to_be_removed: + description: Resource attribute with any string value. + type: string + enabled: true + warnings: + if_enabled: This resource_attribute is deprecated and will be removed soon. + +attributes: + string_attr: + description: Attribute with any string value. + type: string + + overridden_int_attr: + name_override: state + description: Integer attribute with overridden name. + type: int + + enum_attr: + description: Attribute with a known set of string values. + type: string + enum: [red, green, blue] + + boolean_attr: + description: Attribute with a boolean value. + type: bool + + slice_attr: + description: Attribute with a slice value. + type: slice + + map_attr: + description: Attribute with a map value. + type: map + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + monotonic: true + aggregation_temporality: cumulative + attributes: [string_attr, overridden_int_attr, enum_attr, slice_attr, map_attr] + warnings: + if_enabled_not_set: This metric will be disabled by default soon. + + optional.metric: + enabled: false + description: "[DEPRECATED] Gauge double metric disabled by default." + unit: 1 + gauge: + value_type: double + attributes: [string_attr, boolean_attr] + warnings: + if_configured: This metric is deprecated and will be removed soon. + + optional.metric.empty_unit: + enabled: false + description: "[DEPRECATED] Gauge double metric disabled by default." + unit: "" + gauge: + value_type: double + attributes: [string_attr, boolean_attr] + warnings: + if_configured: This metric is deprecated and will be removed soon. + + default.metric.to_be_removed: + enabled: true + description: "[DEPRECATED] Non-monotonic delta sum double metric enabled by default." + extended_documentation: The metric will be will be removed soon. + unit: s + sum: + value_type: double + monotonic: false + aggregation_temporality: delta + warnings: + if_enabled: This metric is deprecated and will be removed soon. diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml new file mode 100644 index 00000000000..60d165c616b --- /dev/null +++ b/cmd/mdatagen/metadata-schema.yaml @@ -0,0 +1,112 @@ +# Required: The type of the component - Usually the name. The type and class combined uniquely identify the component (eg. receiver/otlp) or subcomponent (eg. receiver/hostmetricsreceiver/cpu) +type: + +# Required for subcomponents: The type of the parent component. +parent: string + +# Required for components (Optional for subcomponents): A high-level view of the development status and use of this component +status: + # Required: The class of the component (For example receiver) + class: + # Required: The stability of the component - See https://github.com/open-telemetry/opentelemetry-collector#stability-levels + stability: + development: [] + alpha: [] + beta: [] + stable: [] + deprecated: [] + unmaintained: [] + # Optional: The distributions that this component is bundled with (For example core or contrib). See statusdata.go for a list of common distros. + distributions: [string] + # Optional: A list of warnings that should be brought to the attention of users looking to use this component + warnings: [string] + # Optional: Metadata related to codeowners of the component + codeowners: + active: [string] + emeritus: [string] + +# Optional: OTel Semantic Conventions version that will be associated with the scraped metrics. +# This attribute should be set for metrics compliant with OTel Semantic Conventions. +sem_conv_version: 1.9.0 + +# Optional: map of resource attribute definitions with the key being the attribute name. +resource_attributes: + : + # Required: whether the resource attribute is added the emitted metrics by default. + enabled: bool + # Required: description of the attribute. + description: + # Optional: array of attribute values if they are static values (currently, only string type is supported). + enum: [string] + # Required: attribute value type. + type: + # Optional: warnings that will be shown to user under specified conditions. + warnings: + # A warning that will be displayed if the resource_attribute is enabled in user config. + # Should be used for deprecated default resource_attributes that will be removed soon. + if_enabled: + # A warning that will be displayed if `enabled` field is not set explicitly in user config. + # Should be used for resource_attributes that will be turned from default to optional or vice versa. + if_enabled_not_set: + # A warning that will be displayed if the resource_attribute is configured by user in any way. + # Should be used for deprecated optional resource_attributes that will be removed soon. + if_configured: + + +# Optional: map of attribute definitions with the key being the attribute name and value +# being described below. +attributes: + : + # Optional: this field can be used to override the actual attribute name defined by the key. + # It should be used if multiple metrics have different attributes with the same name. + name_override: + # Required: description of the attribute. + description: + # Optional: array of attribute values if they are static values (currently, only string type is supported). + enum: [string] + # Required: attribute value type. + type: + +# Optional: map of metric names with the key being the metric name and value +# being described below. +metrics: + : + # Required: whether the metric is collected by default. + enabled: bool + # Required: metric description. + description: + # Optional: extended documentation of the metric. + extended_documentation: + # Optional: warnings that will be shown to user under specified conditions. + warnings: + # A warning that will be displayed if the metric is enabled in user config. + # Should be used for deprecated default metrics that will be removed soon. + if_enabled: + # A warning that will be displayed if `enabled` field is not set explicitly in user config. + # Should be used for metrics that will be turned from default to optional or vice versa. + if_enabled_not_set: + # A warning that will be displayed if the metrics is configured by user in any way. + # Should be used for deprecated optional metrics that will be removed soon. + if_configured: + # Required: metric unit as defined by https://ucum.org/ucum.html. + unit: + # Required: metric type with its settings. + : + # Required for sum and gauge metrics: type of number data point values. + value_type: + # Required for sum metric: whether the metric is monotonic (no negative delta values). + monotonic: bool + # Required for sum metric: whether reported values incorporate previous measurements + # (cumulative) or not (delta). + aggregation_temporality: + # Optional: Indicates the type the metric needs to be parsed from. If set, the generated + # functions will parse the value from string to value_type. + input_type: string + # Optional: array of attributes that were defined in the attributes section that are emitted by this metric. + attributes: [string] + +# Lifecycle tests generated for this component. +tests: + config: # {} by default, specific testing configuration for lifecycle tests. + skip_lifecycle: false # false by default + expect_consumer_error: true # false by default diff --git a/cmd/mdatagen/metadata.yaml b/cmd/mdatagen/metadata.yaml new file mode 100644 index 00000000000..65bb097a962 --- /dev/null +++ b/cmd/mdatagen/metadata.yaml @@ -0,0 +1,6 @@ +type: mdatagen + +status: + class: cmd + codeowners: + active: [dmitryax] \ No newline at end of file diff --git a/cmd/mdatagen/metricdata.go b/cmd/mdatagen/metricdata.go new file mode 100644 index 00000000000..c3448f1722c --- /dev/null +++ b/cmd/mdatagen/metricdata.go @@ -0,0 +1,182 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +var ( + _ MetricData = &gauge{} + _ MetricData = &sum{} +) + +// MetricData is generic interface for all metric datatypes. +type MetricData interface { + Type() string + HasMonotonic() bool + HasAggregated() bool + HasMetricInputType() bool +} + +// AggregationTemporality defines a metric aggregation type. +type AggregationTemporality struct { + // Aggregation describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + Aggregation pmetric.AggregationTemporality +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (agg *AggregationTemporality) UnmarshalText(text []byte) error { + switch vtStr := string(text); vtStr { + case "cumulative": + agg.Aggregation = pmetric.AggregationTemporalityCumulative + case "delta": + agg.Aggregation = pmetric.AggregationTemporalityDelta + default: + return fmt.Errorf("invalid aggregation: %q", vtStr) + } + return nil +} + +// String returns string representation of the aggregation temporality. +func (agg *AggregationTemporality) String() string { + return agg.Aggregation.String() +} + +// Mono defines the metric monotonicity. +type Mono struct { + // Monotonic is true if the sum is monotonic. + Monotonic bool `mapstructure:"monotonic"` +} + +// MetricInputType defines the metric input value type +type MetricInputType struct { + // InputType is the type the metric needs to be parsed from, options are "string" + InputType string `mapstructure:"input_type"` +} + +func (mit MetricInputType) HasMetricInputType() bool { + return mit.InputType != "" +} + +// Type returns name of the datapoint type. +func (mit MetricInputType) String() string { + return mit.InputType +} + +// MetricValueType defines the metric number type. +type MetricValueType struct { + // ValueType is type of the metric number, options are "double", "int". + ValueType pmetric.NumberDataPointValueType +} + +func (mvt *MetricValueType) Unmarshal(parser *confmap.Conf) error { + if !parser.IsSet("value_type") { + return errors.New("missing required field: `value_type`") + } + return nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (mvt *MetricValueType) UnmarshalText(text []byte) error { + switch vtStr := string(text); vtStr { + case "int": + mvt.ValueType = pmetric.NumberDataPointValueTypeInt + case "double": + mvt.ValueType = pmetric.NumberDataPointValueTypeDouble + default: + return fmt.Errorf("invalid value_type: %q", vtStr) + } + return nil +} + +// Type returns name of the datapoint type. +func (mvt MetricValueType) String() string { + return mvt.ValueType.String() +} + +// BasicType returns name of a golang basic type for the datapoint type. +func (mvt MetricValueType) BasicType() string { + switch mvt.ValueType { + case pmetric.NumberDataPointValueTypeInt: + return "int64" + case pmetric.NumberDataPointValueTypeDouble: + return "float64" + case pmetric.NumberDataPointValueTypeEmpty: + return "" + default: + return "" + } +} + +type gauge struct { + MetricValueType `mapstructure:"value_type"` + MetricInputType `mapstructure:",squash"` +} + +// Unmarshal is a custom unmarshaler for gauge. Needed mostly to avoid MetricValueType.Unmarshal inheritance. +func (d *gauge) Unmarshal(parser *confmap.Conf) error { + if err := d.MetricValueType.Unmarshal(parser); err != nil { + return err + } + return parser.Unmarshal(d, confmap.WithErrorUnused()) +} + +func (d gauge) Type() string { + return "Gauge" +} + +func (d gauge) HasMonotonic() bool { + return false +} + +func (d gauge) HasAggregated() bool { + return false +} + +type sum struct { + AggregationTemporality `mapstructure:"aggregation_temporality"` + Mono `mapstructure:",squash"` + MetricValueType `mapstructure:"value_type"` + MetricInputType `mapstructure:",squash"` +} + +// Unmarshal is a custom unmarshaler for sum. Needed mostly to avoid MetricValueType.Unmarshal inheritance. +func (d *sum) Unmarshal(parser *confmap.Conf) error { + if !parser.IsSet("aggregation_temporality") { + return errors.New("missing required field: `aggregation_temporality`") + } + if err := d.MetricValueType.Unmarshal(parser); err != nil { + return err + } + return parser.Unmarshal(d, confmap.WithErrorUnused()) +} + +// TODO: Currently, this func will not be called because of https://github.com/open-telemetry/opentelemetry-collector/issues/6671. Uncomment function and +// add a test case to Test_loadMetadata for file no_monotonic.yaml once the issue is solved. +// +// Unmarshal is a custom unmarshaler for Mono. +// func (m *Mono) Unmarshal(parser *confmap.Conf) error { +// if !parser.IsSet("monotonic") { +// return errors.New("missing required field: `monotonic`") +// } +// return parser.Unmarshal(m, confmap.WithErrorUnused()) +// } + +func (d sum) Type() string { + return "Sum" +} + +func (d sum) HasMonotonic() bool { + return true +} + +func (d sum) HasAggregated() bool { + return true +} diff --git a/cmd/mdatagen/metricdata_test.go b/cmd/mdatagen/metricdata_test.go new file mode 100644 index 00000000000..10b7f46613e --- /dev/null +++ b/cmd/mdatagen/metricdata_test.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMetricData(t *testing.T) { + for _, arg := range []struct { + metricData MetricData + typ string + hasAggregated bool + hasMonotonic bool + }{ + {&gauge{}, "Gauge", false, false}, + {&sum{}, "Sum", true, true}, + } { + assert.Equal(t, arg.typ, arg.metricData.Type()) + assert.Equal(t, arg.hasAggregated, arg.metricData.HasAggregated()) + assert.Equal(t, arg.hasMonotonic, arg.metricData.HasMonotonic()) + } +} diff --git a/cmd/mdatagen/statusdata.go b/cmd/mdatagen/statusdata.go new file mode 100644 index 00000000000..9ecf63d4257 --- /dev/null +++ b/cmd/mdatagen/statusdata.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "sort" +) + +// distros is a collection of distributions that can be referenced in the metadata.yaml files. +// The rules below apply to every distribution added to this list: +// - The distribution must be open source. +// - The link must point to a publicly accessible repository. +var distros = map[string]string{ + "core": "https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol", + "contrib": "https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib", + "aws": "https://github.com/aws-observability/aws-otel-collector", + "grafana": "https://github.com/grafana/agent", + "observiq": "https://github.com/observIQ/observiq-otel-collector", + "redhat": "https://github.com/os-observability/redhat-opentelemetry-collector", + "splunk": "https://github.com/signalfx/splunk-otel-collector", + "sumo": "https://github.com/SumoLogic/sumologic-otel-collector", + "liatrio": "https://github.com/liatrio/liatrio-otel-collector", +} + +type Codeowners struct { + // Active codeowners + Active []string `mapstructure:"active"` + // Emeritus codeowners + Emeritus []string `mapstructure:"emeritus"` +} + +type Status struct { + Stability map[string][]string `mapstructure:"stability"` + Distributions []string `mapstructure:"distributions"` + Class string `mapstructure:"class"` + Warnings []string `mapstructure:"warnings"` + Codeowners *Codeowners `mapstructure:"codeowners"` +} + +func (s *Status) SortedDistributions() []string { + sorted := s.Distributions + sort.Slice(sorted, func(i, j int) bool { + if s.Distributions[i] == "core" { + return true + } + if s.Distributions[i] == "contrib" { + return s.Distributions[j] != "core" + } + if s.Distributions[j] == "core" { + return false + } + if s.Distributions[j] == "contrib" { + return s.Distributions[i] == "core" + } + return s.Distributions[i] < s.Distributions[j] + }) + return sorted +} diff --git a/cmd/mdatagen/statusdata_test.go b/cmd/mdatagen/statusdata_test.go new file mode 100644 index 00000000000..947065b1d6f --- /dev/null +++ b/cmd/mdatagen/statusdata_test.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortedDistributions(t *testing.T) { + tests := []struct { + name string + s Status + result []string + }{ + { + "all combined", + Status{Distributions: []string{"arm", "contrib", "core", "foo", "bar"}}, + []string{"core", "contrib", "arm", "bar", "foo"}, + }, + { + "core only", + Status{Distributions: []string{"core"}}, + []string{"core"}, + }, + { + "core and contrib only", + Status{Distributions: []string{"core", "contrib"}}, + []string{"core", "contrib"}, + }, + { + "core and contrib reversed", + Status{Distributions: []string{"contrib", "core"}}, + []string{"core", "contrib"}, + }, + { + "neither core nor contrib", + Status{Distributions: []string{"foo", "bar"}}, + []string{"bar", "foo"}, + }, + { + "no core, contrib, something else", + Status{Distributions: []string{"foo", "contrib", "bar"}}, + []string{"contrib", "bar", "foo"}, + }, + { + "core, no contrib, something else", + Status{Distributions: []string{"foo", "core", "bar"}}, + []string{"core", "bar", "foo"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.result, test.s.SortedDistributions()) + }) + } +} diff --git a/cmd/mdatagen/templates/component_test.go.tmpl b/cmd/mdatagen/templates/component_test.go.tmpl new file mode 100644 index 00000000000..94739cb73f9 --- /dev/null +++ b/cmd/mdatagen/templates/component_test.go.tmpl @@ -0,0 +1,340 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" +{{ if isExporter }} + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exportertest" +{{ end }} +{{ if isProcessor }} + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/processor" + "go.opentelemetry.io/collector/processor/processortest" +{{ end }} +{{ if isReceiver }} + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/receivertest" +{{ end }} +{{ if isExtension }} + "go.opentelemetry.io/collector/extension/extensiontest" +{{ end }} + "go.opentelemetry.io/collector/confmap/confmaptest" +{{ if or (isExporter) (isProcessor) }} + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata" +{{ end }} +) + +// assertNoErrorHost implements a component.Host that asserts that there were no errors. +type assertNoErrorHost struct { + component.Host + *testing.T +} + +var _ component.Host = (*assertNoErrorHost)(nil) + +// newAssertNoErrorHost returns a new instance of assertNoErrorHost. +func newAssertNoErrorHost(t *testing.T) component.Host { + return &assertNoErrorHost{ + componenttest.NewNopHost(), + t, + } +} + +func (aneh *assertNoErrorHost) ReportFatalError(err error) { + assert.NoError(aneh, err) +} + + +{{ if isExporter }} +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct{ + name string + createFn func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) + }{ +{{ if supportsLogs }} + { + name: "logs", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsExporter(ctx, set, cfg) + }, + }, +{{ end }} +{{ if supportsMetrics }} + { + name: "metrics", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsExporter(ctx, set, cfg) + }, + }, +{{ end }} +{{ if supportsTraces }} + { + name: "traces", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesExporter(ctx, set, cfg) + }, + }, +{{ end }} + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name + "-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name + "-lifecycle", func(t *testing.T) { + {{ if skipLifecycle }} + // TODO support lifecycle + t.SkipNow() + {{ end }} + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + err = c.Start(context.Background(), host) + require.NoError(t, err) + assert.NotPanics(t, func() { + switch e := c.(type) { + case exporter.Logs: + logs := testdata.GenerateLogsManyLogRecordsSameResource(2) + if !e.Capabilities().MutatesData { + logs.MarkReadOnly() + } + err = e.ConsumeLogs(context.Background(), logs) + case exporter.Metrics: + metrics := testdata.GenerateMetricsTwoMetrics() + if !e.Capabilities().MutatesData { + metrics.MarkReadOnly() + } + err = e.ConsumeMetrics(context.Background(), metrics) + case exporter.Traces: + traces := testdata.GenerateTracesTwoSpansSameResource() + if !e.Capabilities().MutatesData { + traces.MarkReadOnly() + } + err = e.ConsumeTraces(context.Background(), traces) + } + }) + {{ if not expectConsumerError }} + assert.NoError(t, err) + {{ end }} + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} +{{ end }} + +{{ if isProcessor }} +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct{ + name string + createFn func(ctx context.Context, set processor.CreateSettings, cfg component.Config) (component.Component, error) + }{ +{{ if supportsLogs }} + { + name: "logs", + createFn: func(ctx context.Context, set processor.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsProcessor(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} +{{ if supportsMetrics }} + { + name: "metrics", + createFn: func(ctx context.Context, set processor.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsProcessor(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} +{{ if supportsTraces }} + { + name: "traces", + createFn: func(ctx context.Context, set processor.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesProcessor(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name + "-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), processortest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name + "-lifecycle", func(t *testing.T) { + {{ if skipLifecycle }} + // TODO support lifecycle + t.SkipNow() + {{ end }} + c, err := test.createFn(context.Background(), processortest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + err = c.Start(context.Background(), host) + require.NoError(t, err) + assert.NotPanics(t, func() { + switch e := c.(type) { + case processor.Logs: + logs := testdata.GenerateLogsManyLogRecordsSameResource(2) + if !e.Capabilities().MutatesData { + logs.MarkReadOnly() + } + err = e.ConsumeLogs(context.Background(), logs) + case processor.Metrics: + metrics := testdata.GenerateMetricsTwoMetrics() + if !e.Capabilities().MutatesData { + metrics.MarkReadOnly() + } + err = e.ConsumeMetrics(context.Background(), metrics) + case processor.Traces: + traces := testdata.GenerateTracesTwoSpansSameResource() + if !e.Capabilities().MutatesData { + traces.MarkReadOnly() + } + err = e.ConsumeTraces(context.Background(), traces) + } + }) + assert.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} +{{ end }} + +{{ if isReceiver }} +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct{ + name string + createFn func(ctx context.Context, set receiver.CreateSettings, cfg component.Config) (component.Component, error) + }{ +{{ if supportsLogs }} + { + name: "logs", + createFn: func(ctx context.Context, set receiver.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsReceiver(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} +{{ if supportsMetrics }} + { + name: "metrics", + createFn: func(ctx context.Context, set receiver.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsReceiver(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} +{{ if supportsTraces }} + { + name: "traces", + createFn: func(ctx context.Context, set receiver.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesReceiver(ctx, set, cfg, consumertest.NewNop()) + }, + }, +{{ end }} + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name + "-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), receivertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name + "-lifecycle", func(t *testing.T) { + {{ if skipLifecycle }} + // TODO support lifecycle + t.SkipNow() + {{ end }} + firstRcvr, err := test.createFn(context.Background(), receivertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + require.NoError(t, err) + require.NoError(t, firstRcvr.Start(context.Background(), host)) + require.NoError(t, firstRcvr.Shutdown(context.Background())) + secondRcvr, err := test.createFn(context.Background(), receivertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + require.NoError(t, secondRcvr.Start(context.Background(), host)) + require.NoError(t, secondRcvr.Shutdown(context.Background())) + }) + } +} +{{ end }} + +{{ if isExtension }} +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + t.Run("shutdown", func(t *testing.T) { + e, err := factory.CreateExtension(context.Background(), extensiontest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = e.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run("lifecycle", func(t *testing.T) { + {{ if skipLifecycle }} + // TODO support lifecycle + t.SkipNow() + {{ end }} + firstExt, err := factory.CreateExtension(context.Background(), extensiontest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + require.NoError(t, firstExt.Start(context.Background(), newAssertNoErrorHost(t))) + require.NoError(t, firstExt.Shutdown(context.Background())) + + secondExt, err := factory.CreateExtension(context.Background(), extensiontest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + require.NoError(t, secondExt.Start(context.Background(), newAssertNoErrorHost(t))) + require.NoError(t, secondExt.Shutdown(context.Background())) + }) +} +{{ end }} diff --git a/cmd/mdatagen/templates/config.go.tmpl b/cmd/mdatagen/templates/config.go.tmpl new file mode 100644 index 00000000000..02f69788334 --- /dev/null +++ b/cmd/mdatagen/templates/config.go.tmpl @@ -0,0 +1,103 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +{{ if or .Metrics .ResourceAttributes -}} +import "go.opentelemetry.io/collector/confmap" +{{- end }} + +{{ if .Metrics -}} + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms, confmap.WithErrorUnused()) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for {{ .Type }} metrics. +type MetricsConfig struct { + {{- range $name, $metric := .Metrics }} + {{ $name.Render }} MetricConfig `mapstructure:"{{ $name }}"` + {{- end }} +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + {{- range $name, $metric := .Metrics }} + {{ $name.Render }}: MetricConfig{ + Enabled: {{ $metric.Enabled }}, + }, + {{- end }} + } +} +{{- end }} + +{{ if .ResourceAttributes -}} +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac, confmap.WithErrorUnused()) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for {{ .Type }} resource attributes. +type ResourceAttributesConfig struct { + {{- range $name, $attr := .ResourceAttributes }} + {{ $name.Render }} ResourceAttributeConfig `mapstructure:"{{ $name }}"` + {{- end }} +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + {{- range $name, $attr := .ResourceAttributes }} + {{ $name.Render }}: ResourceAttributeConfig { + Enabled: {{ $attr.Enabled }}, + }, + {{- end }} + } +} +{{- end }} + +{{ if .Metrics -}} +// MetricsBuilderConfig is a configuration for {{ .Type }} metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + {{- if .ResourceAttributes }} + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` + {{- end }} +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig { + Metrics: DefaultMetricsConfig(), + {{- if .ResourceAttributes }} + ResourceAttributes: DefaultResourceAttributesConfig(), + {{- end }} + } +} +{{- end }} diff --git a/cmd/mdatagen/templates/config_test.go.tmpl b/cmd/mdatagen/templates/config_test.go.tmpl new file mode 100644 index 00000000000..3f6a636bc6e --- /dev/null +++ b/cmd/mdatagen/templates/config_test.go.tmpl @@ -0,0 +1,131 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +{{ if .Metrics }} +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + {{- range $name, $_ := .Metrics }} + {{ $name.Render }}: MetricConfig{Enabled: true}, + {{- end }} + }, + {{- if .ResourceAttributes }} + ResourceAttributes: ResourceAttributesConfig{ + {{- range $name, $_ := .ResourceAttributes }} + {{ $name.Render }}: ResourceAttributeConfig{Enabled: true}, + {{- end }} + }, + {{- end }} + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + {{- range $name, $_ := .Metrics }} + {{ $name.Render }}: MetricConfig{Enabled: false}, + {{- end }} + }, + {{- if .ResourceAttributes }} + ResourceAttributes: ResourceAttributesConfig{ + {{- range $name, $_ := .ResourceAttributes }} + {{ $name.Render }}: ResourceAttributeConfig{Enabled: false}, + {{- end }} + }, + {{- end }} + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{} + {{- if .ResourceAttributes }}, ResourceAttributeConfig{}{{ end }})); diff != "" { + t.Errorf("Config mismatch (-expected +actual):\n%s", diff) + } + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, component.UnmarshalConfig(sub, &cfg)) + return cfg +} +{{- end }} + +{{ if .ResourceAttributes -}} +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + {{- range $name, $_ := .ResourceAttributes }} + {{ $name.Render }}: ResourceAttributeConfig{Enabled: true}, + {{- end }} + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + {{- range $name, $_ := .ResourceAttributes }} + {{ $name.Render }}: ResourceAttributeConfig{Enabled: false}, + {{- end }} + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})); diff != "" { + t.Errorf("Config mismatch (-expected +actual):\n%s", diff) + } + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, component.UnmarshalConfig(sub, &cfg)) + return cfg +} +{{- end }} diff --git a/cmd/mdatagen/templates/documentation.md.tmpl b/cmd/mdatagen/templates/documentation.md.tmpl new file mode 100644 index 00000000000..eda8dc9fffe --- /dev/null +++ b/cmd/mdatagen/templates/documentation.md.tmpl @@ -0,0 +1,98 @@ +{{- define "metric-documenation" -}} +{{- $metricName := . }} +{{- $metric := $metricName | metricInfo -}} + +### {{ $metricName }} + +{{ $metric.Description }} + +{{- if $metric.ExtendedDocumentation }} + +{{ $metric.ExtendedDocumentation }} + +{{- end }} + +| Unit | Metric Type | Value Type |{{ if $metric.Data.HasAggregated }} Aggregation Temporality |{{ end }}{{ if $metric.Data.HasMonotonic }} Monotonic |{{ end }} +| ---- | ----------- | ---------- |{{ if $metric.Data.HasAggregated }} ----------------------- |{{ end }}{{ if $metric.Data.HasMonotonic }} --------- |{{ end }} +| {{ $metric.Unit }} | {{ $metric.Data.Type }} | {{ $metric.Data.MetricValueType }} | +{{- if $metric.Data.HasAggregated }} {{ $metric.Data.AggregationTemporality }} |{{ end }} +{{- if $metric.Data.HasMonotonic }} {{ $metric.Data.Monotonic }} |{{ end }} + +{{- if $metric.Attributes }} + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +{{- range $metric.Attributes }} +{{- $attribute := . | attributeInfo }} +| {{ $attribute.Name }} | {{ $attribute.Description }} | +{{- if $attribute.Enum }} {{ $attribute.Type }}: ``{{ stringsJoin $attribute.Enum "``, ``" }}``{{ else }} Any {{ $attribute.Type }}{{ end }} | +{{- end }} + +{{- end }} + +{{- end -}} + +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# {{ .Type }} + +{{- if .Parent }} + +**Parent Component:** {{ .Parent }} +{{- end }} + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +{{- range $metricName, $metric := .Metrics }} +{{- if $metric.Enabled }} + +{{ template "metric-documenation" $metricName }} + +{{- end }} +{{- end }} + +{{- $optionalMetricSeen := false }} +{{- range $metricName, $metric := .Metrics }} +{{- if not $metric.Enabled }} +{{- if not $optionalMetricSeen }} + +## Optional Metrics + +The following metrics are not emitted by default. Each of them can be enabled by applying the following configuration: + +```yaml +metrics: + : + enabled: true +``` + +{{- end }} +{{- $optionalMetricSeen = true }} + +{{ template "metric-documenation" $metricName }} + +{{- end }} +{{- end }} + +{{- if .ResourceAttributes }} + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +{{- range $attributeName, $attribute := .ResourceAttributes }} +| {{ $attributeName }} | {{ $attribute.Description }} | +{{- if $attribute.Enum }} {{ $attribute.Type }}: ``{{ stringsJoin $attribute.Enum "``, ``" }}``{{ else }} Any {{ $attribute.Type }}{{ end }} | {{ $attribute.Enabled }} | +{{- end }} + +{{- end }} diff --git a/cmd/mdatagen/templates/metrics.go.tmpl b/cmd/mdatagen/templates/metrics.go.tmpl new file mode 100644 index 00000000000..c76623c40fc --- /dev/null +++ b/cmd/mdatagen/templates/metrics.go.tmpl @@ -0,0 +1,319 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + {{- if .Metrics | parseImportsRequired }} + "strconv" + "fmt" + {{- end }} + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" + {{- if .SemConvVersion }} + conventions "go.opentelemetry.io/collector/semconv/v{{ .SemConvVersion }}" + {{- end }} +) + +{{ range $name, $info := .Attributes }} +{{- if $info.Enum -}} +// Attribute{{ $name.Render }} specifies the a value {{ $name }} attribute. +type Attribute{{ $name.Render }} int + +const ( + _ Attribute{{ $name.Render }} = iota + {{- range $info.Enum }} + Attribute{{ $name.Render }}{{ . | publicVar }} + {{- end }} +) + +// String returns the string representation of the Attribute{{ $name.Render }}. +func (av Attribute{{ $name.Render }}) String() string { + switch av { + {{- range $info.Enum }} + case Attribute{{ $name.Render }}{{ . | publicVar }}: + return "{{ . }}" + {{- end }} + } + return "" +} + +// MapAttribute{{ $name.Render }} is a helper map of string to Attribute{{ $name.Render }} attribute value. +var MapAttribute{{ $name.Render }} = map[string]Attribute{{ $name.Render }}{ + {{- range $info.Enum }} + "{{ . }}": Attribute{{ $name.Render }}{{ . | publicVar }}, + {{- end }} +} + +{{ end }} +{{- end }} + +{{ range $name, $metric := .Metrics -}} +type metric{{ $name.Render }} struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills {{ $name }} metric with initial data. +func (m *metric{{ $name.Render }}) init() { + m.data.SetName("{{ $name }}") + m.data.SetDescription("{{ $metric.Description }}") + m.data.SetUnit("{{ $metric.Unit }}") + m.data.SetEmpty{{ $metric.Data.Type }}() + {{- if $metric.Data.HasMonotonic }} + m.data.{{ $metric.Data.Type }}().SetIsMonotonic({{ $metric.Data.Monotonic }}) + {{- end }} + {{- if $metric.Data.HasAggregated }} + m.data.{{ $metric.Data.Type }}().SetAggregationTemporality(pmetric.AggregationTemporality{{ $metric.Data.AggregationTemporality }}) + {{- end }} + {{- if $metric.Attributes }} + m.data.{{ $metric.Data.Type }}().DataPoints().EnsureCapacity(m.capacity) + {{- end }} +} + +func (m *metric{{ $name.Render }}) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val {{ $metric.Data.MetricValueType.BasicType }} +{{- range $metric.Attributes -}}, {{ .RenderUnexported }}AttributeValue {{ (attributeInfo .).Type.Primitive }}{{ end }}) { + if !m.config.Enabled { + return + } + dp := m.data.{{ $metric.Data.Type }}().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.Set{{ $metric.Data.MetricValueType }}Value(val) + {{- range $metric.Attributes }} + {{- if eq (attributeInfo .).Type.Primitive "[]byte" }} + dp.Attributes().PutEmptyBytes("{{ (attributeInfo .).Name }}").FromRaw({{ .RenderUnexported }}AttributeValue) + {{- else if eq (attributeInfo .).Type.Primitive "[]any" }} + dp.Attributes().PutEmptySlice("{{ (attributeInfo .).Name }}").FromRaw({{ .RenderUnexported }}AttributeValue) + {{- else if eq (attributeInfo .).Type.Primitive "map[string]any" }} + dp.Attributes().PutEmptyMap("{{ (attributeInfo .).Name }}").FromRaw({{ .RenderUnexported }}AttributeValue) + {{- else }} + dp.Attributes().Put{{ (attributeInfo .).Type }}("{{ (attributeInfo .).Name }}", {{ .RenderUnexported }}AttributeValue) + {{- end }} + {{- end }} +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metric{{ $name.Render }}) updateCapacity() { + if m.data.{{ $metric.Data.Type }}().DataPoints().Len() > m.capacity { + m.capacity = m.data.{{ $metric.Data.Type }}().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metric{{ $name.Render }}) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.{{ $metric.Data.Type }}().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetric{{ $name.Render }}(cfg MetricConfig) metric{{ $name.Render }} { + m := metric{{ $name.Render }}{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +{{ end -}} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + {{- range $name, $metric := .Metrics }} + metric{{ $name.Render }} metric{{ $name.Render }} + {{- end }} +} + +// metricBuilderOption applies changes to default metrics builder. +type metricBuilderOption func(*MetricsBuilder) + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) metricBuilderOption { + return func(mb *MetricsBuilder) { + mb.startTime = startTime + } +} + +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSettings, options ...metricBuilderOption) *MetricsBuilder { + {{- range $name, $metric := .Metrics }} + {{- if $metric.Warnings.IfEnabled }} + if mbc.Metrics.{{ $name.Render }}.Enabled { + settings.Logger.Warn("[WARNING] `{{ $name }}` should not be enabled: {{ $metric.Warnings.IfEnabled }}") + } + {{- end }} + {{- if $metric.Warnings.IfEnabledNotSet }} + if !mbc.Metrics.{{ $name.Render }}.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `{{ $name }}`: {{ $metric.Warnings.IfEnabledNotSet }}") + } + {{- end }} + {{- if $metric.Warnings.IfConfigured }} + if mbc.Metrics.{{ $name.Render }}.enabledSetByUser { + settings.Logger.Warn("[WARNING] `{{ $name }}` should not be configured: {{ $metric.Warnings.IfConfigured }}") + } + {{- end }} + {{- end }} + {{- range $name, $attr := .ResourceAttributes }} + {{- if $attr.Warnings.IfEnabled }} + if mbc.ResourceAttributes.{{ $name.Render }}.Enabled { + settings.Logger.Warn("[WARNING] `{{ $name }}` should not be enabled: {{ $attr.Warnings.IfEnabled }}") + } + {{- end }} + {{- if $attr.Warnings.IfEnabledNotSet }} + if !mbc.ResourceAttributes.{{ $name.Render }}.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `{{ $name }}`: {{ $attr.Warnings.IfEnabledNotSet }}") + } + {{- end }} + {{- if $attr.Warnings.IfConfigured }} + if mbc.ResourceAttributes.{{ $name.Render }}.enabledSetByUser { + settings.Logger.Warn("[WARNING] `{{ $name }}` should not be configured: {{ $attr.Warnings.IfConfigured }}") + } + {{- end }} + {{- end }} + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + {{- range $name, $metric := .Metrics }} + metric{{ $name.Render }}: newMetric{{ $name.Render }}(mbc.Metrics.{{ $name.Render }}), + {{- end }} + } + for _, op := range options { + op(mb) + } + return mb +} + +{{- if .ResourceAttributes }} +// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. +func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { + return NewResourceBuilder(mb.config.ResourceAttributes) +} +{{- end }} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption func(pmetric.ResourceMetrics) + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + } +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + } +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(rmo ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + {{- if .SemConvVersion }} + rm.SetSchemaUrl(conventions.SchemaURL) + {{- end }} + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName("{{ .ScopeName }}") + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + {{- range $name, $metric := .Metrics }} + mb.metric{{- $name.Render }}.emit(ils.Metrics()) + {{- end }} + + for _, op := range rmo { + op(rm) + } + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(rmo ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(rmo...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +{{ range $name, $metric := .Metrics -}} +// Record{{ $name.Render }}DataPoint adds a data point to {{ $name }} metric. +func (mb *MetricsBuilder) Record{{ $name.Render }}DataPoint(ts pcommon.Timestamp + {{- if $metric.Data.HasMetricInputType }}, inputVal {{ $metric.Data.MetricInputType.String }} + {{- else }}, val {{ $metric.Data.MetricValueType.BasicType }} + {{- end }} + {{- range $metric.Attributes -}} + , {{ .RenderUnexported }}AttributeValue {{ if (attributeInfo .).Enum }}Attribute{{ .Render }}{{ else }}{{ (attributeInfo .).Type.Primitive }}{{ end }} + {{- end }}) + {{- if $metric.Data.HasMetricInputType }} error{{ end }} { + {{- if $metric.Data.HasMetricInputType }} + {{- if eq $metric.Data.MetricValueType.BasicType "float64" }} + val, err := strconv.ParseFloat(inputVal, 64) + {{- else if eq $metric.Data.MetricValueType.BasicType "int64" }} + val, err := strconv.ParseInt(inputVal, 10, 64) + {{- end }} + if err != nil { + return fmt.Errorf("failed to parse {{ $metric.Data.MetricValueType.BasicType }} for {{ $name.Render }}, value was %s: %w", inputVal, err) + } + {{- end }} + mb.metric{{ $name.Render }}.recordDataPoint(mb.startTime, ts, val + {{- range $metric.Attributes -}} + , {{ .RenderUnexported }}AttributeValue{{ if (attributeInfo .).Enum }}.String(){{ end }} + {{- end }}) + {{- if $metric.Data.HasMetricInputType }} + return nil + {{- end }} +} +{{ end }} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...metricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op(mb) + } +} diff --git a/cmd/mdatagen/templates/metrics_test.go.tmpl b/cmd/mdatagen/templates/metrics_test.go.tmpl new file mode 100644 index 00000000000..bf7b09d05a9 --- /dev/null +++ b/cmd/mdatagen/templates/metrics_test.go.tmpl @@ -0,0 +1,174 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + + +type testConfigCollection int + +const ( + testSetDefault testConfigCollection = iota + testSetAll + testSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + configSet testConfigCollection + }{ + { + name: "default", + configSet: testSetDefault, + }, + { + name: "all_set", + configSet: testSetAll, + }, + { + name: "none_set", + configSet: testSetNone, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := receivertest.NewNopCreateSettings() + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, test.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + {{- range $name, $metric := .Metrics }} + {{- if and $metric.Enabled $metric.Warnings.IfEnabled }} + if test.configSet == testSetDefault || test.configSet == testSetAll { + assert.Equal(t, "[WARNING] `{{ $name }}` should not be enabled: {{ $metric.Warnings.IfEnabled }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- if $metric.Warnings.IfEnabledNotSet }} + if test.configSet == testSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `{{ $name }}`: {{ $metric.Warnings.IfEnabledNotSet }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- if $metric.Warnings.IfConfigured }} + if test.configSet == testSetAll || test.configSet == testSetNone { + assert.Equal(t, "[WARNING] `{{ $name }}` should not be configured: {{ $metric.Warnings.IfConfigured }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- end }} + {{- range $name, $attr := .ResourceAttributes }} + {{- if and $attr.Enabled $attr.Warnings.IfEnabled }} + if test.configSet == testSetDefault || test.configSet == testSetAll { + assert.Equal(t, "[WARNING] `{{ $name }}` should not be enabled: {{ $attr.Warnings.IfEnabled }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- if $attr.Warnings.IfEnabledNotSet }} + if test.configSet == testSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `{{ $name }}`: {{ $attr.Warnings.IfEnabledNotSet }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- if $attr.Warnings.IfConfigured }} + if test.configSet == testSetAll || test.configSet == testSetNone { + assert.Equal(t, "[WARNING] `{{ $name }}` should not be configured: {{ $attr.Warnings.IfConfigured }}", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + {{- end }} + {{- end }} + + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + {{- range $name, $metric := .Metrics }} + + {{ if $metric.Enabled }}defaultMetricsCount++{{ end }} + allMetricsCount++ + mb.Record{{ $name.Render }}DataPoint(ts, {{ if $metric.Data.HasMetricInputType }}"1"{{ else }}1{{ end }} + {{- range $metric.Attributes -}} + , {{ if (attributeInfo .).Enum }}Attribute{{ .Render }}{{ (index (attributeInfo .).Enum 0) | publicVar }}{{ else }}{{ (attributeInfo .).TestValue }}{{ end }} + {{- end }}) + {{- end }} + + {{ if .ResourceAttributes }} + rb := mb.NewResourceBuilder() + {{- range $name, $attr := .ResourceAttributes }} + {{- if $attr.Enum }} + rb.Set{{ $attr.Name.Render }}{{ index $attr.Enum 0 | publicVar }}() + {{- else }} + rb.Set{{ $attr.Name.Render }}({{ $attr.TestValue }}) + {{- end }} + {{- end }} + res := rb.Emit() + {{- else }} + res := pcommon.NewResource() + {{- end }} + metrics := mb.Emit(WithResource(res)) + + if test.configSet == testSetNone { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if test.configSet == testSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if test.configSet == testSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + {{- range $name, $metric := .Metrics }} + case "{{ $name }}": + assert.False(t, validatedMetrics["{{ $name }}"], "Found a duplicate in the metrics slice: {{ $name }}") + validatedMetrics["{{ $name }}"] = true + assert.Equal(t, pmetric.MetricType{{ $metric.Data.Type }}, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).{{ $metric.Data.Type }}().DataPoints().Len()) + assert.Equal(t, "{{ $metric.Description }}", ms.At(i).Description()) + assert.Equal(t, "{{ $metric.Unit }}", ms.At(i).Unit()) + {{- if $metric.Data.HasMonotonic }} + assert.Equal(t, {{ $metric.Data.Monotonic }}, ms.At(i).{{ $metric.Data.Type }}().IsMonotonic()) + {{- end }} + {{- if $metric.Data.HasAggregated }} + assert.Equal(t, pmetric.AggregationTemporality{{ $metric.Data.AggregationTemporality }}, ms.At(i).{{ $metric.Data.Type }}().AggregationTemporality()) + {{- end }} + dp := ms.At(i).{{ $metric.Data.Type }}().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueType{{ $metric.Data.MetricValueType }}, dp.ValueType()) + assert.Equal(t, {{ $metric.Data.MetricValueType.BasicType }}(1), dp.{{ $metric.Data.MetricValueType }}Value()) + + {{- range $i, $attr := $metric.Attributes }} + attrVal, ok {{ if eq $i 0 }}:{{ end }}= dp.Attributes().Get("{{ (attributeInfo $attr).Name }}") + assert.True(t, ok) + assert.EqualValues(t, {{ (attributeInfo $attr).TestValue }}, attrVal.{{ (attributeInfo $attr).Type }}() + {{- if or (eq (attributeInfo $attr).Type.String "Slice") (eq (attributeInfo $attr).Type.String "Map")}}.AsRaw(){{ end }}) + {{- end }} + {{- end }} + } + } + }) + } +} diff --git a/cmd/mdatagen/templates/readme.md.tmpl b/cmd/mdatagen/templates/readme.md.tmpl new file mode 100644 index 00000000000..364d3ad484d --- /dev/null +++ b/cmd/mdatagen/templates/readme.md.tmpl @@ -0,0 +1,55 @@ + +{{- if len .Status.Stability }} +| Status | | +| ------------- |-----------| +{{- $class := .Status.Class }} +{{- $shortName := .ShortFolderName }} +{{- if ne $class "connector" }} +{{- $idx := 0 }} +{{- range $stability, $value := .Status.Stability }} +| {{ if not $idx }}Stability{{ else }} {{ end }} | [{{ $stability }}]{{ if ne $class "extension" }}: {{ stringsJoin $value ", " }} {{ end }} | +{{- $idx = inc $idx }} +{{- end }} +{{- end}} +{{- if and (ne $class "cmd") (ne $class "pkg") }} +| Distributions | [{{ stringsJoin .Status.SortedDistributions "], [" }}] | +{{- end }} +{{- if .Status.Warnings }} +| Warnings | [{{ stringsJoin .Status.Warnings ", " }}](#warnings) | +{{- end }} +{{- if ne $class "" }} +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3A{{ $class }}%2F{{ $shortName }}%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3A{{ $class }}%2F{{ $shortName }}) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3A{{ $class }}%2F{{ $shortName }}%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3A{{ $class }}%2F{{ $shortName }}) | +{{- end }} +{{- if .Status.Codeowners }} +{{- $codeowners := userLinks .Status.Codeowners.Active }} +{{- $emeritus := userLinks .Status.Codeowners.Emeritus }} +| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | {{ stringsJoin $codeowners ", " }} | +{{- if $emeritus }} +| Emeritus | {{ stringsJoin $emeritus ", " }} | +{{- end }} +{{- end }} +{{range $stability, $val := .Status.Stability}} +[{{ $stability }}]: https://github.com/open-telemetry/opentelemetry-collector#{{ $stability }} +{{- end }} +{{- range .Status.SortedDistributions }} +[{{.}}]: {{ distroURL . }} +{{- end }} +{{- if eq $class "connector"}} + +## Supported Pipeline Types + +| [Exporter Pipeline Type] | [Receiver Pipeline Type] | [Stability Level] | +| ------------------------ | ------------------------ | ----------------- | +{{- range $stability, $pipelines := .Status.Stability }} +{{- range $pipeline := $pipelines }} +{{- $parts := stringsSplit $pipeline "_to_" }} +| {{index $parts 0}} | {{index $parts 1}} | [{{$stability}}] | +{{- end }} +{{- end }} + +[Exporter Pipeline Type]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/connector/README.md#exporter-pipeline-type +[Receiver Pipeline Type]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/connector/README.md#receiver-pipeline-type +[Stability Level]: https://github.com/open-telemetry/opentelemetry-collector#stability-levels +{{- end }} +{{- end }} + \ No newline at end of file diff --git a/cmd/mdatagen/templates/resource.go.tmpl b/cmd/mdatagen/templates/resource.go.tmpl new file mode 100644 index 00000000000..aa6b8e8b685 --- /dev/null +++ b/cmd/mdatagen/templates/resource.go.tmpl @@ -0,0 +1,51 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +{{- range $name, $attr := .ResourceAttributes }} +{{- range $attr.Enum }} +// Set{{ $name.Render }}{{ . | publicVar }} sets "{{ $name }}={{ . }}" attribute. +func (rb *ResourceBuilder) Set{{ $name.Render }}{{ . | publicVar }}() { + if rb.config.{{ $name.Render }}.Enabled { + rb.res.Attributes().PutStr("{{ $name }}", "{{ . }}") + } +} +{{- else }} +// Set{{ $name.Render }} sets provided value as "{{ $name }}" attribute. +func (rb *ResourceBuilder) Set{{ $name.Render }}(val {{ $attr.Type.Primitive }}) { + if rb.config.{{ $name.Render }}.Enabled { + {{- if or (eq $attr.Type.String "Bytes") (eq $attr.Type.String "Slice") (eq $attr.Type.String "Map") }} + rb.res.Attributes().PutEmpty{{ $attr.Type }}("{{ $name }}").FromRaw(val) + {{- else }} + rb.res.Attributes().Put{{ $attr.Type }}("{{ $name }}", val) + {{- end }} + } +} +{{- end }} +{{ end }} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/cmd/mdatagen/templates/resource_test.go.tmpl b/cmd/mdatagen/templates/resource_test.go.tmpl new file mode 100644 index 00000000000..7c1422e3d3b --- /dev/null +++ b/cmd/mdatagen/templates/resource_test.go.tmpl @@ -0,0 +1,65 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +{{- $enabledAttrCount := 0 }} +{{- range $_, $attr := .ResourceAttributes }} +{{- if $attr.Enabled }} +{{- $enabledAttrCount = inc $enabledAttrCount }} +{{- end }} +{{- end }} + +func TestResourceBuilder(t *testing.T) { + for _, test := range []string{"default", "all_set", "none_set"} { + t.Run(test, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, test) + rb := NewResourceBuilder(cfg) + {{- range $name, $attr := .ResourceAttributes }} + {{- if $attr.Enum }} + rb.Set{{ $name.Render }}{{ index $attr.Enum 0 | publicVar }}() + {{- else }} + rb.Set{{ $name.Render }}({{ $attr.TestValue }}) + {{- end }} + {{- end }} + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch test { + case "default": + assert.Equal(t, {{ $enabledAttrCount }}, res.Attributes().Len()) + case "all_set": + assert.Equal(t, {{ len .ResourceAttributes }}, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", test) + } + + {{ $assignSign := ":=" }} + {{- range $name, $attr := .ResourceAttributes }} + val, ok {{ $assignSign }} res.Attributes().Get("{{ $name }}") + {{- if $attr.Enabled }} + assert.True(t, ok) + {{- else }} + assert.Equal(t, test == "all_set", ok) + {{- end }} + if ok { + assert.EqualValues(t, {{ $attr.TestValue }}, val.{{ $attr.Type }}() + {{- if or (eq $attr.Type.String "Bytes") (eq $attr.Type.String "Slice") (eq $attr.Type.String "Map") -}} + .AsRaw() + {{- end -}} + ) + } + {{- $assignSign = "=" }} + {{- end }} + }) + } +} diff --git a/cmd/mdatagen/templates/status.go.tmpl b/cmd/mdatagen/templates/status.go.tmpl new file mode 100644 index 00000000000..654e6b1518a --- /dev/null +++ b/cmd/mdatagen/templates/status.go.tmpl @@ -0,0 +1,26 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" +) + +const ( + Type = "{{ .Type }}" + {{- range $stability, $signals := .Status.Stability }} + {{- range $signal := $signals }} + {{ toCamelCase $signal }}Stability = component.StabilityLevel{{ casesTitle $stability }} + {{- end }} + {{- end }} +) + +func Meter(settings component.TelemetrySettings) metric.Meter { + return settings.MeterProvider.Meter("{{ .ScopeName }}") +} + +func Tracer(settings component.TelemetrySettings) trace.Tracer { + return settings.TracerProvider.Tracer("{{ .ScopeName }}") +} \ No newline at end of file diff --git a/cmd/mdatagen/templates/testdata/config.yaml.tmpl b/cmd/mdatagen/templates/testdata/config.yaml.tmpl new file mode 100644 index 00000000000..8b32773ec45 --- /dev/null +++ b/cmd/mdatagen/templates/testdata/config.yaml.tmpl @@ -0,0 +1,31 @@ +default: +all_set: + {{- if .Metrics }} + metrics: + {{- range $name, $_ := .Metrics }} + {{ $name }}: + enabled: true + {{- end }} + {{- end }} + {{- if .ResourceAttributes }} + resource_attributes: + {{- range $name, $_ := .ResourceAttributes }} + {{ $name }}: + enabled: true + {{- end }} + {{- end }} +none_set: + {{- if .Metrics }} + metrics: + {{- range $name, $_ := .Metrics }} + {{ $name }}: + enabled: false + {{- end }} + {{- end }} + {{- if .ResourceAttributes }} + resource_attributes: + {{- range $name, $_ := .ResourceAttributes }} + {{ $name }}: + enabled: false + {{- end }} + {{- end }} diff --git a/cmd/mdatagen/testdata/invalid.yaml b/cmd/mdatagen/testdata/invalid.yaml new file mode 100644 index 00000000000..e466dcbd8e8 --- /dev/null +++ b/cmd/mdatagen/testdata/invalid.yaml @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/cmd/mdatagen/testdata/invalid_aggregation.yaml b/cmd/mdatagen/testdata/invalid_aggregation.yaml new file mode 100644 index 00000000000..3165d1a7101 --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_aggregation.yaml @@ -0,0 +1,22 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + monotonic: true + aggregation_temporality: invalidaggregation diff --git a/cmd/mdatagen/testdata/invalid_class.yaml b/cmd/mdatagen/testdata/invalid_class.yaml new file mode 100644 index 00000000000..6fcd9b9d7e7 --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_class.yaml @@ -0,0 +1,8 @@ +type: test + +status: + class: incorrectclass + stability: + development: [logs] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/invalid_input_type.yaml b/cmd/mdatagen/testdata/invalid_input_type.yaml new file mode 100644 index 00000000000..9e28b42120d --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_input_type.yaml @@ -0,0 +1,20 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + sum: + value_type: double + monotonic: true + aggregation_temporality: cumulative + input_type: double + attributes: diff --git a/cmd/mdatagen/testdata/invalid_stability.yaml b/cmd/mdatagen/testdata/invalid_stability.yaml new file mode 100644 index 00000000000..259aade2445 --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_stability.yaml @@ -0,0 +1,7 @@ +type: file +status: + class: receiver + stability: + incorrectstability: [logs] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/invalid_stability_component.yaml b/cmd/mdatagen/testdata/invalid_stability_component.yaml new file mode 100644 index 00000000000..5aa538865fa --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_stability_component.yaml @@ -0,0 +1,7 @@ +type: file +status: + class: receiver + stability: + development: [incorrectcomponent] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/invalid_type_attr.yaml b/cmd/mdatagen/testdata/invalid_type_attr.yaml new file mode 100644 index 00000000000..ed951be6d6d --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_type_attr.yaml @@ -0,0 +1,24 @@ +type: metricreceiver + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +attributes: + used_attr: + description: Used attribute. + type: invalidtype + +metrics: + metric: + enabled: true + description: Metric. + unit: 1 + gauge: + value_type: double + attributes: [used_attr] diff --git a/cmd/mdatagen/testdata/invalid_type_rattr.yaml b/cmd/mdatagen/testdata/invalid_type_rattr.yaml new file mode 100644 index 00000000000..90e9c33dae3 --- /dev/null +++ b/cmd/mdatagen/testdata/invalid_type_rattr.yaml @@ -0,0 +1,19 @@ +type: file + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +resource_attributes: + string.resource.attr: + description: Resource attribute with any string value. + type: invalidtype + enabled: true diff --git a/cmd/mdatagen/testdata/metrics_and_type.yaml b/cmd/mdatagen/testdata/metrics_and_type.yaml new file mode 100644 index 00000000000..81d66bde9c1 --- /dev/null +++ b/cmd/mdatagen/testdata/metrics_and_type.yaml @@ -0,0 +1,19 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +metrics: + metric: + enabled: true + description: Description. + unit: s + gauge: + value_type: double diff --git a/cmd/mdatagen/testdata/no_aggregation.yaml b/cmd/mdatagen/testdata/no_aggregation.yaml new file mode 100644 index 00000000000..02c14b6b1e7 --- /dev/null +++ b/cmd/mdatagen/testdata/no_aggregation.yaml @@ -0,0 +1,22 @@ +type: file + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + monotonic: false \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_class.yaml b/cmd/mdatagen/testdata/no_class.yaml new file mode 100644 index 00000000000..8d33f9c019a --- /dev/null +++ b/cmd/mdatagen/testdata/no_class.yaml @@ -0,0 +1,7 @@ +type: test + +status: + stability: + development: [logs] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_description_attr.yaml b/cmd/mdatagen/testdata/no_description_attr.yaml new file mode 100644 index 00000000000..1942f74e111 --- /dev/null +++ b/cmd/mdatagen/testdata/no_description_attr.yaml @@ -0,0 +1,33 @@ +# Sample metric metadata file with all available configurations. + +type: file + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +attributes: + string_attr: + type: string + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + monotonic: true + aggregation_temporality: cumulative + attributes: [string_attr] + warnings: + if_enabled_not_set: This metric will be disabled by default soon. diff --git a/cmd/mdatagen/testdata/no_description_rattr.yaml b/cmd/mdatagen/testdata/no_description_rattr.yaml new file mode 100644 index 00000000000..d489eaa7cb6 --- /dev/null +++ b/cmd/mdatagen/testdata/no_description_rattr.yaml @@ -0,0 +1,12 @@ +type: file +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +resource_attributes: + string.resource.attr: + type: string + enabled: true diff --git a/cmd/mdatagen/testdata/no_enabled.yaml b/cmd/mdatagen/testdata/no_enabled.yaml new file mode 100644 index 00000000000..ab1dfafed32 --- /dev/null +++ b/cmd/mdatagen/testdata/no_enabled.yaml @@ -0,0 +1,18 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +metrics: + system.cpu.time: + description: Total CPU seconds broken down by different states. + unit: s + sum: + value_type: double + monotonic: true + aggregation_temporality: cumulative + attributes: diff --git a/cmd/mdatagen/testdata/no_metric_description.yaml b/cmd/mdatagen/testdata/no_metric_description.yaml new file mode 100644 index 00000000000..de9cbd42087 --- /dev/null +++ b/cmd/mdatagen/testdata/no_metric_description.yaml @@ -0,0 +1,22 @@ +type: file + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + + +metrics: + default.metric: + enabled: true + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + monotonic: true + aggregation_temporality: cumulative diff --git a/cmd/mdatagen/testdata/no_metric_type.yaml b/cmd/mdatagen/testdata/no_metric_type.yaml new file mode 100644 index 00000000000..1f0e0283f2a --- /dev/null +++ b/cmd/mdatagen/testdata/no_metric_type.yaml @@ -0,0 +1,13 @@ +type: metricreceiver +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + attributes: diff --git a/cmd/mdatagen/testdata/no_metric_unit.yaml b/cmd/mdatagen/testdata/no_metric_unit.yaml new file mode 100644 index 00000000000..cc5c49a7c9a --- /dev/null +++ b/cmd/mdatagen/testdata/no_metric_unit.yaml @@ -0,0 +1,22 @@ +type: file + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + sum: + value_type: int + monotonic: true + aggregation_temporality: cumulative diff --git a/cmd/mdatagen/testdata/no_monotonic.yaml b/cmd/mdatagen/testdata/no_monotonic.yaml new file mode 100644 index 00000000000..2f99cecab58 --- /dev/null +++ b/cmd/mdatagen/testdata/no_monotonic.yaml @@ -0,0 +1,22 @@ +type: file + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + + +metrics: + default.metric: + enabled: true + description: Monotonic cumulative sum int metric enabled by default. + extended_documentation: The metric will be become optional soon. + unit: s + sum: + value_type: int + aggregation_temporality: cumulative diff --git a/cmd/mdatagen/testdata/no_stability.yaml b/cmd/mdatagen/testdata/no_stability.yaml new file mode 100644 index 00000000000..29b00be6ffa --- /dev/null +++ b/cmd/mdatagen/testdata/no_stability.yaml @@ -0,0 +1,4 @@ +type: test + +status: + class: receiver \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_stability_component.yaml b/cmd/mdatagen/testdata/no_stability_component.yaml new file mode 100644 index 00000000000..9c0631e8bec --- /dev/null +++ b/cmd/mdatagen/testdata/no_stability_component.yaml @@ -0,0 +1,6 @@ +type: file +status: + class: receiver + stability: + beta: + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_status.yaml b/cmd/mdatagen/testdata/no_status.yaml new file mode 100644 index 00000000000..75db25c3b05 --- /dev/null +++ b/cmd/mdatagen/testdata/no_status.yaml @@ -0,0 +1 @@ +type: test \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_type.yaml b/cmd/mdatagen/testdata/no_type.yaml new file mode 100644 index 00000000000..72d92e5dee8 --- /dev/null +++ b/cmd/mdatagen/testdata/no_type.yaml @@ -0,0 +1,6 @@ +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_type_attr.yaml b/cmd/mdatagen/testdata/no_type_attr.yaml new file mode 100644 index 00000000000..914a9820bda --- /dev/null +++ b/cmd/mdatagen/testdata/no_type_attr.yaml @@ -0,0 +1,23 @@ +type: metricreceiver + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +attributes: + used_attr: + description: Used attribute. + +metrics: + metric: + enabled: true + description: Metric. + unit: 1 + gauge: + value_type: double + attributes: [used_attr] diff --git a/cmd/mdatagen/testdata/no_type_rattr.yaml b/cmd/mdatagen/testdata/no_type_rattr.yaml new file mode 100644 index 00000000000..e6961b1801e --- /dev/null +++ b/cmd/mdatagen/testdata/no_type_rattr.yaml @@ -0,0 +1,18 @@ +type: file + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +resource_attributes: + string.resource.attr: + description: Resource attribute with any string value. + enabled: true \ No newline at end of file diff --git a/cmd/mdatagen/testdata/no_value_type.yaml b/cmd/mdatagen/testdata/no_value_type.yaml new file mode 100644 index 00000000000..046f457b510 --- /dev/null +++ b/cmd/mdatagen/testdata/no_value_type.yaml @@ -0,0 +1,22 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + + +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + sum: + monotonic: true + aggregation_temporality: cumulative + attributes: diff --git a/cmd/mdatagen/testdata/parent.yaml b/cmd/mdatagen/testdata/parent.yaml new file mode 100644 index 00000000000..1f8fc15ba17 --- /dev/null +++ b/cmd/mdatagen/testdata/parent.yaml @@ -0,0 +1,3 @@ +type: subcomponent + +parent: parentComponent \ No newline at end of file diff --git a/cmd/mdatagen/testdata/readme_with_cmd_class.md b/cmd/mdatagen/testdata/readme_with_cmd_class.md new file mode 100644 index 00000000000..71a8ad7598c --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_cmd_class.md @@ -0,0 +1,14 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [alpha]: logs | +| | [beta]: metrics | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Acmd%2Ffoo%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Acmd%2Ffoo) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Acmd%2Ffoo%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Acmd%2Ffoo) | + +[alpha]: https://github.com/open-telemetry/opentelemetry-collector#alpha +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_multiple_signals.md b/cmd/mdatagen/testdata/readme_with_multiple_signals.md new file mode 100644 index 00000000000..8de1ed6221a --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_multiple_signals.md @@ -0,0 +1,15 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [alpha]: logs | +| | [beta]: metrics | +| Distributions | [contrib] | + +[alpha]: https://github.com/open-telemetry/opentelemetry-collector#alpha +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_status.md b/cmd/mdatagen/testdata/readme_with_status.md new file mode 100644 index 00000000000..92e0b1f4258 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_status.md @@ -0,0 +1,14 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [beta]: metrics | +| Distributions | [contrib] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Ffoo%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Ffoo) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Ffoo%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Ffoo) | + +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_status_codeowners.md b/cmd/mdatagen/testdata/readme_with_status_codeowners.md new file mode 100644 index 00000000000..f350b593360 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_status_codeowners.md @@ -0,0 +1,15 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [beta]: metrics | +| Distributions | [contrib] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Ffoo%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Ffoo) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Ffoo%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Ffoo) | +| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@foo](https://www.github.com/foo) | + +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_status_codeowners_and_emeritus.md b/cmd/mdatagen/testdata/readme_with_status_codeowners_and_emeritus.md new file mode 100644 index 00000000000..203de2f3dd0 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_status_codeowners_and_emeritus.md @@ -0,0 +1,16 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [beta]: metrics | +| Distributions | [contrib] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Ffoo%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Ffoo) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Ffoo%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Ffoo) | +| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@foo](https://www.github.com/foo) | +| Emeritus | [@bar](https://www.github.com/bar) | + +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_status_extension.md b/cmd/mdatagen/testdata/readme_with_status_extension.md new file mode 100644 index 00000000000..1c454afe9a9 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_status_extension.md @@ -0,0 +1,14 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [beta] | +| Distributions | [contrib] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Aextension%2Ffoo%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Aextension%2Ffoo) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Aextension%2Ffoo%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Aextension%2Ffoo) | + +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component diff --git a/cmd/mdatagen/testdata/readme_with_warnings.md b/cmd/mdatagen/testdata/readme_with_warnings.md new file mode 100644 index 00000000000..6cf1afa3866 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_with_warnings.md @@ -0,0 +1,16 @@ +# Some component + + +| Status | | +| ------------- |-----------| +| Stability | [beta]: metrics | +| Distributions | [contrib] | +| Warnings | [warning1](#warnings) | + +[beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +Some info about a component +### warnings +Some warning there. diff --git a/cmd/mdatagen/testdata/readme_without_status.md b/cmd/mdatagen/testdata/readme_without_status.md new file mode 100644 index 00000000000..be610f8c087 --- /dev/null +++ b/cmd/mdatagen/testdata/readme_without_status.md @@ -0,0 +1,3 @@ +# Some component + +Some info about a component diff --git a/cmd/mdatagen/testdata/resource_attributes_only.yaml b/cmd/mdatagen/testdata/resource_attributes_only.yaml new file mode 100644 index 00000000000..05e031bcf45 --- /dev/null +++ b/cmd/mdatagen/testdata/resource_attributes_only.yaml @@ -0,0 +1,17 @@ +type: test + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + distributions: [contrib] + warnings: + - Any additional information that should be brought to the consumer's attention + +resource_attributes: + res.attr1: + description: Resource attribute 1. + type: string + enabled: true diff --git a/cmd/mdatagen/testdata/status_only.yaml b/cmd/mdatagen/testdata/status_only.yaml new file mode 100644 index 00000000000..2365b43f1d9 --- /dev/null +++ b/cmd/mdatagen/testdata/status_only.yaml @@ -0,0 +1,6 @@ +type: metricreceiver +status: + class: exporter + stability: + beta: [traces, metrics, logs] + distributions: [contrib] diff --git a/cmd/mdatagen/testdata/two_metric_types.yaml b/cmd/mdatagen/testdata/two_metric_types.yaml new file mode 100644 index 00000000000..572eab2f3bc --- /dev/null +++ b/cmd/mdatagen/testdata/two_metric_types.yaml @@ -0,0 +1,21 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + gauge: + value_type: double + sum: + value_type: double + monotonic: true + aggregation_temporality: cumulative + attributes: diff --git a/cmd/mdatagen/testdata/unknown_metric_attribute.yaml b/cmd/mdatagen/testdata/unknown_metric_attribute.yaml new file mode 100644 index 00000000000..d86b5afe401 --- /dev/null +++ b/cmd/mdatagen/testdata/unknown_metric_attribute.yaml @@ -0,0 +1,19 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + sum: + value_type: double + monotonic: true + aggregation_temporality: cumulative + attributes: [missing] diff --git a/cmd/mdatagen/testdata/unknown_value_type.yaml b/cmd/mdatagen/testdata/unknown_value_type.yaml new file mode 100644 index 00000000000..1a4890045d8 --- /dev/null +++ b/cmd/mdatagen/testdata/unknown_value_type.yaml @@ -0,0 +1,18 @@ +type: metricreceiver + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +metrics: + system.cpu.time: + enabled: true + description: Total CPU seconds broken down by different states. + unit: s + sum: + value_type: unknown + monotonic: true + aggregation_temporality: cumulative diff --git a/cmd/mdatagen/testdata/unused_attribute.yaml b/cmd/mdatagen/testdata/unused_attribute.yaml new file mode 100644 index 00000000000..cc0eb08f767 --- /dev/null +++ b/cmd/mdatagen/testdata/unused_attribute.yaml @@ -0,0 +1,29 @@ +type: metricreceiver + +sem_conv_version: 1.9.0 + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] + +attributes: + used_attr: + description: Used attribute. + type: string + + unused_attr: + name_override: state + description: Unused attribute. + type: string + +metrics: + metric: + enabled: true + description: Metric. + unit: 1 + gauge: + value_type: double + attributes: [used_attr] diff --git a/cmd/mdatagen/third_party/golint/LICENSE b/cmd/mdatagen/third_party/golint/LICENSE new file mode 100644 index 00000000000..65d761bc9f2 --- /dev/null +++ b/cmd/mdatagen/third_party/golint/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/mdatagen/third_party/golint/golint.go b/cmd/mdatagen/third_party/golint/golint.go new file mode 100644 index 00000000000..f4898aaebd5 --- /dev/null +++ b/cmd/mdatagen/third_party/golint/golint.go @@ -0,0 +1,51 @@ +// Copyright (c) 2013 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd. + +package golint + +// See https://github.com/golang/lint/blob/d0100b6bd8b389f0385611eb39152c4d7c3a7905/lint.go#L771 + +// Acronyms is a list of known acronyms that should not be formatted when linting. +var Acronyms = map[string]bool{ + "ACL": true, + "API": true, + "ASCII": true, + "CPU": true, + "CSS": true, + "DNS": true, + "EOF": true, + "GUID": true, + "HTML": true, + "HTTP": true, + "HTTPS": true, + "ID": true, + "IP": true, + "JSON": true, + "LHS": true, + "QPS": true, + "RAM": true, + "RHS": true, + "RPC": true, + "SLA": true, + "SMTP": true, + "SQL": true, + "SSH": true, + "TCP": true, + "TLS": true, + "TTL": true, + "UDP": true, + "UI": true, + "UID": true, + "UUID": true, + "URI": true, + "URL": true, + "UTF8": true, + "VM": true, + "XML": true, + "XMPP": true, + "XSRF": true, + "XSS": true, +} diff --git a/cmd/mdatagen/validate.go b/cmd/mdatagen/validate.go new file mode 100644 index 00000000000..aac3e3a6898 --- /dev/null +++ b/cmd/mdatagen/validate.go @@ -0,0 +1,194 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + + "go.uber.org/multierr" + + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func (md *metadata) Validate() error { + var errs error + if err := md.validateType(); err != nil { + errs = multierr.Append(errs, err) + } + if err := md.validateStatus(); err != nil { + errs = multierr.Append(errs, err) + } + if err := md.validateResourceAttributes(); err != nil { + errs = multierr.Append(errs, err) + } + if err := md.validateMetrics(); err != nil { + errs = multierr.Append(errs, err) + } + return errs +} + +func (md *metadata) validateType() error { + if md.Type == "" { + return errors.New("missing type") + } + return nil +} + +func (md *metadata) validateStatus() error { + if md.Parent != "" && md.Status == nil { + // status is not required for subcomponents. + return nil + } + + var errs error + if md.Status == nil { + return errors.New("missing status") + } + if err := md.Status.validateClass(); err != nil { + errs = multierr.Append(errs, err) + } + if md.Parent == "" { + if err := md.Status.validateStability(); err != nil { + errs = multierr.Append(errs, err) + } + } + return errs +} + +func (s *Status) validateClass() error { + if s.Class == "" { + return errors.New("missing class") + } + if s.Class != "receiver" && s.Class != "processor" && s.Class != "exporter" && s.Class != "connector" && s.Class != "extension" && s.Class != "cmd" && s.Class != "pkg" { + return fmt.Errorf("invalid class: %v", s.Class) + } + return nil +} + +func (s *Status) validateStability() error { + var errs error + if len(s.Stability) == 0 { + return errors.New("missing stability") + } + for stability, component := range s.Stability { + if stability != "development" && stability != "alpha" && stability != "beta" && stability != "stable" && stability != "deprecated" && stability != "unmaintained" { + errs = multierr.Append(errs, fmt.Errorf("invalid stability: %v", stability)) + } + if component == nil { + errs = multierr.Append(errs, fmt.Errorf("missing component for stability: %v", stability)) + } + for _, c := range component { + if c != "metrics" && + c != "traces" && + c != "logs" && + c != "traces_to_traces" && + c != "traces_to_metrics" && + c != "traces_to_logs" && + c != "metrics_to_traces" && + c != "metrics_to_metrics" && + c != "metrics_to_logs" && + c != "logs_to_traces" && + c != "logs_to_metrics" && + c != "logs_to_logs" && + c != "extension" { + errs = multierr.Append(errs, fmt.Errorf("invalid component: %v", c)) + } + } + } + return errs +} + +func (md *metadata) validateResourceAttributes() error { + var errs error + for name, attr := range md.ResourceAttributes { + if attr.Description == "" { + errs = multierr.Append(errs, fmt.Errorf("empty description for resource attribute: %v", name)) + } + empty := ValueType{ValueType: pcommon.ValueTypeEmpty} + if attr.Type == empty { + errs = multierr.Append(errs, fmt.Errorf("empty type for resource attribute: %v", name)) + } + } + return errs +} + +func (md *metadata) validateMetrics() error { + var errs error + usedAttrs := map[attributeName]bool{} + for mn, m := range md.Metrics { + if m.Sum == nil && m.Gauge == nil { + errs = multierr.Append(errs, fmt.Errorf("metric %v doesn't have a metric type key, "+ + "one of the following has to be specified: sum, gauge", mn)) + continue + } + if m.Sum != nil && m.Gauge != nil { + errs = multierr.Append(errs, fmt.Errorf("metric %v has more than one metric type keys, "+ + "only one of the following has to be specified: sum, gauge", mn)) + continue + } + if err := m.validate(); err != nil { + errs = multierr.Append(errs, fmt.Errorf(`metric "%v": %w`, mn, err)) + continue + } + unknownAttrs := make([]attributeName, 0, len(m.Attributes)) + for _, attr := range m.Attributes { + if _, ok := md.Attributes[attr]; ok { + usedAttrs[attr] = true + } else { + unknownAttrs = append(unknownAttrs, attr) + } + } + if len(unknownAttrs) > 0 { + errs = multierr.Append(errs, fmt.Errorf(`metric "%v" refers to undefined attributes: %v`, mn, unknownAttrs)) + } + } + errs = multierr.Append(errs, md.validateAttributes(usedAttrs)) + return errs +} + +func (m *metric) validate() error { + var errs error + if m.Description == "" { + errs = multierr.Append(errs, errors.New(`missing metric description`)) + } + if m.Unit == nil { + errs = multierr.Append(errs, errors.New(`missing metric unit`)) + } + if m.Sum != nil { + errs = multierr.Append(errs, m.Sum.Validate()) + } + if m.Gauge != nil { + errs = multierr.Append(errs, m.Gauge.Validate()) + } + return errs +} + +func (mit MetricInputType) Validate() error { + if mit.InputType != "" && mit.InputType != "string" { + return fmt.Errorf("invalid `input_type` value \"%v\", must be \"\" or \"string\"", mit.InputType) + } + return nil +} + +func (md *metadata) validateAttributes(usedAttrs map[attributeName]bool) error { + var errs error + unusedAttrs := make([]attributeName, 0, len(md.Attributes)) + for attrName, attr := range md.Attributes { + if attr.Description == "" { + errs = multierr.Append(errs, fmt.Errorf(`missing attribute description for: %v`, attrName)) + } + empty := ValueType{ValueType: pcommon.ValueTypeEmpty} + if attr.Type == empty { + errs = multierr.Append(errs, fmt.Errorf("empty type for attribute: %v", attrName)) + } + if !usedAttrs[attrName] { + unusedAttrs = append(unusedAttrs, attrName) + } + } + if len(unusedAttrs) > 0 { + errs = multierr.Append(errs, fmt.Errorf("unused attributes: %v", unusedAttrs)) + } + return errs +} diff --git a/cmd/mdatagen/validate_test.go b/cmd/mdatagen/validate_test.go new file mode 100644 index 00000000000..12871a4ab8c --- /dev/null +++ b/cmd/mdatagen/validate_test.go @@ -0,0 +1,153 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "io/fs" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + wantErr string + }{ + { + name: "testdata/no_type.yaml", + wantErr: "missing type", + }, + { + name: "testdata/no_status.yaml", + wantErr: "missing status", + }, + { + name: "testdata/no_class.yaml", + wantErr: "missing class", + }, + { + name: "testdata/invalid_class.yaml", + wantErr: "invalid class: incorrectclass", + }, + { + name: "testdata/no_stability.yaml", + wantErr: "missing stability", + }, + { + name: "testdata/invalid_stability.yaml", + wantErr: "invalid stability: incorrectstability", + }, + { + name: "testdata/no_stability_component.yaml", + wantErr: "missing component for stability: beta", + }, + { + name: "testdata/invalid_stability_component.yaml", + wantErr: "invalid component: incorrectcomponent", + }, + { + name: "testdata/no_description_rattr.yaml", + wantErr: "empty description for resource attribute: string.resource.attr", + }, + { + name: "testdata/no_type_rattr.yaml", + wantErr: "empty type for resource attribute: string.resource.attr", + }, + { + name: "testdata/no_metric_description.yaml", + wantErr: "metric \"default.metric\": missing metric description", + }, + { + name: "testdata/no_metric_unit.yaml", + wantErr: "metric \"default.metric\": missing metric unit", + }, + { + name: "testdata/no_metric_type.yaml", + wantErr: "metric system.cpu.time doesn't have a metric type key, " + + "one of the following has to be specified: sum, gauge", + }, + { + name: "testdata/two_metric_types.yaml", + wantErr: "metric system.cpu.time has more than one metric type keys, " + + "only one of the following has to be specified: sum, gauge", + }, + { + name: "testdata/invalid_input_type.yaml", + wantErr: "metric \"system.cpu.time\": invalid `input_type` value \"double\", must be \"\" or \"string\"", + }, + { + name: "testdata/unknown_metric_attribute.yaml", + wantErr: "metric \"system.cpu.time\" refers to undefined attributes: [missing]", + }, + { + name: "testdata/unused_attribute.yaml", + wantErr: "unused attributes: [unused_attr]", + }, + { + name: "testdata/no_description_attr.yaml", + wantErr: "missing attribute description for: string_attr", + }, + { + name: "testdata/no_type_attr.yaml", + wantErr: "empty type for attribute: used_attr", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := loadMetadata(tt.name) + require.Error(t, err) + require.EqualError(t, err, tt.wantErr) + }) + } +} + +func TestValidateMetricDuplicates(t *testing.T) { + allowedMetrics := map[string][]string{ + "container.cpu.utilization": {"docker_stats", "kubeletstats"}, + "container.memory.rss": {"docker_stats", "kubeletstats"}, + "container.uptime": {"docker_stats", "kubeletstats"}, + } + allMetrics := map[string][]string{} + err := filepath.Walk("../../receiver", func(path string, info fs.FileInfo, err error) error { + if info.Name() == "metadata.yaml" { + md, err := loadMetadata(path) + require.NoError(t, err) + if len(md.Metrics) > 0 { + for metricName := range md.Metrics { + allMetrics[md.Type] = append(allMetrics[md.Type], string(metricName)) + } + } + } + return nil + }) + require.NoError(t, err) + + seen := make(map[string]string) + for receiver, metrics := range allMetrics { + for _, metricName := range metrics { + if val, exists := seen[metricName]; exists { + receivers, allowed := allowedMetrics[metricName] + assert.True( + t, + allowed && contains(receiver, receivers) && contains(val, receivers), + fmt.Sprintf("Duplicate metric %v in receivers %v and %v. Please validate that this is intentional by adding the metric name and receiver types in the allowedMetrics map in this test\n", metricName, receiver, val), + ) + } + seen[metricName] = receiver + } + } +} + +func contains(r string, rs []string) bool { + for _, s := range rs { + if s == r { + return true + } + } + return false +} diff --git a/versions.yaml b/versions.yaml index 470450fe870..65c090a474c 100644 --- a/versions.yaml +++ b/versions.yaml @@ -12,6 +12,7 @@ module-sets: modules: - go.opentelemetry.io/collector - go.opentelemetry.io/collector/cmd/builder + - go.opentelemetry.io/collector/cmd/mdatagen - go.opentelemetry.io/collector/component - go.opentelemetry.io/collector/confmap - go.opentelemetry.io/collector/config/configauth