From 82117770f77ad2e2b2eaabd8e372e7029fc778f4 Mon Sep 17 00:00:00 2001 From: Braydon Kains <93549768+braydonk@users.noreply.github.com> Date: Thu, 10 Oct 2024 12:37:02 -0400 Subject: [PATCH] mdatagen: support custom package name (#11232) #### Description This PR adds support for a new ~~`--package_name` flag~~ `generated_package_name` config field to allow mdatagen to generate packages with names other than `metadata`. #### Link to tracking issue Fixes #11231 #### Testing * Unit tests * `go install`ing this branch and using it in a test scenario with two yaml files and generating two packages. #### Documentation --- .chloggen/mdatagen_package_gen.yaml | 25 +++++++++++ cmd/mdatagen/README.md | 27 +++++++++++- cmd/mdatagen/internal/command.go | 10 ++--- cmd/mdatagen/internal/command_test.go | 42 +++++++++++-------- cmd/mdatagen/internal/loader.go | 5 +++ cmd/mdatagen/internal/loader_test.go | 36 ++++++++++++---- .../custom_generated_package_name.yaml | 17 ++++++++ .../testdata/generated_package_name.yaml | 10 +++++ cmd/mdatagen/metadata-schema.yaml | 3 ++ 9 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 .chloggen/mdatagen_package_gen.yaml create mode 100644 cmd/mdatagen/internal/testdata/custom_generated_package_name.yaml create mode 100644 cmd/mdatagen/internal/testdata/generated_package_name.yaml diff --git a/.chloggen/mdatagen_package_gen.yaml b/.chloggen/mdatagen_package_gen.yaml new file mode 100644 index 00000000000..4f061be1295 --- /dev/null +++ b/.chloggen/mdatagen_package_gen.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: Added generated_package_name config field to support custom generated package name. + +# One or more tracking issues or pull requests related to the change +issues: [11231] + +# (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: [user] diff --git a/cmd/mdatagen/README.md b/cmd/mdatagen/README.md index 5eb7586596e..19170ada72a 100644 --- a/cmd/mdatagen/README.md +++ b/cmd/mdatagen/README.md @@ -26,7 +26,7 @@ An example of how this generated documentation looks can be found in [documentat ## 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 +1. A yaml file containing the metadata that 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): @@ -55,6 +55,31 @@ Below are some more examples that can be used for reference: 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. +### Generate multiple metadata packages + +By default, `mdatagen` will generate a package called `metadata` in the `internal` directory. If you want to generate a package with a different name, you can use the `generated_package_name` configuration field to provide an alternate name. + +```yaml +type: otlp +generated_package_name: customname +status: + class: receiver + stability: + beta: [logs] + stable: [metrics, traces] +``` + +The most common scenario for this would be making major changes to a receiver's metadata without breaking what exists. In this scenario, `mdatagen` could produce separate packages for different metadata specs in the same receiver: + +```go +//go:generate mdatagen metadata.yaml +//go:generate mdatagen custom.yaml + +package main +``` + +With two different packages generated, the behaviour for which metadata is used can be easily controlled via featuregate or a similar mechanism. + ## Contributing to the Metadata Generator The code for generating the documentation can be found in [loader.go](./internal/loader.go) and the templates for rendering the documentation can be found in [templates](./internal/templates). diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go index 8758c8a6ddf..5abe033a943 100644 --- a/cmd/mdatagen/internal/command.go +++ b/cmd/mdatagen/internal/command.go @@ -74,7 +74,7 @@ func run(ymlPath string) error { tmplDir := "templates" - codeDir := filepath.Join(ymlDir, "internal", "metadata") + codeDir := filepath.Join(ymlDir, "internal", md.GeneratedPackageName) if err = os.MkdirAll(codeDir, 0700); err != nil { return fmt.Errorf("unable to create output directory %q: %w", codeDir, err) } @@ -99,7 +99,7 @@ func run(ymlPath string) error { if err = inlineReplace( filepath.Join(tmplDir, "readme.md.tmpl"), filepath.Join(ymlDir, "README.md"), - md, statusStart, statusEnd); err != nil { + md, statusStart, statusEnd, md.GeneratedPackageName); err != nil { return err } } @@ -151,7 +151,7 @@ func run(ymlPath string) error { } for tmpl, dst := range toGenerate { - if err = generateFile(tmpl, dst, md, "metadata"); err != nil { + if err = generateFile(tmpl, dst, md, md.GeneratedPackageName); err != nil { return err } } @@ -371,7 +371,7 @@ func executeTemplate(tmplFile string, md Metadata, goPackage string) ([]byte, er return buf.Bytes(), nil } -func inlineReplace(tmplFile string, outputFile string, md Metadata, start string, end string) error { +func inlineReplace(tmplFile string, outputFile string, md Metadata, start string, end string, goPackage string) error { var readmeContents []byte var err error if readmeContents, err = os.ReadFile(outputFile); err != nil { // nolint: gosec @@ -387,7 +387,7 @@ func inlineReplace(tmplFile string, outputFile string, md Metadata, start string md.GithubProject = "open-telemetry/opentelemetry-collector-contrib" } - buf, err := executeTemplate(tmplFile, md, "metadata") + buf, err := executeTemplate(tmplFile, md, goPackage) if err != nil { return err } diff --git a/cmd/mdatagen/internal/command_test.go b/cmd/mdatagen/internal/command_test.go index fdffe59cc25..27358666ed4 100644 --- a/cmd/mdatagen/internal/command_test.go +++ b/cmd/mdatagen/internal/command_test.go @@ -129,6 +129,10 @@ func TestRunContents(t *testing.T) { wantConfigGenerated: true, wantStatusGenerated: true, }, + { + yml: "custom_generated_package_name.yaml", + wantStatusGenerated: true, + }, } for _, tt := range tests { t.Run(tt.yml, func(t *testing.T) { @@ -151,12 +155,16 @@ foo } require.NoError(t, err) + md, err := LoadMetadata(metadataFile) + require.NoError(t, err) + generatedPackageDir := filepath.Join("internal", md.GeneratedPackageName) + var contents []byte 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, generatedPackageDir, "generated_metrics.go")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_metrics_test.go")) require.FileExists(t, filepath.Join(tmpdir, "documentation.md")) - contents, err = os.ReadFile(filepath.Join(tmpdir, "internal/metadata/generated_metrics.go")) // nolint: gosec + contents, err = os.ReadFile(filepath.Join(tmpdir, generatedPackageDir, "generated_metrics.go")) // nolint: gosec require.NoError(t, err) if tt.wantMetricsContext { require.Contains(t, string(contents), "\"context\"") @@ -164,23 +172,23 @@ foo require.NotContains(t, string(contents), "\"context\"") } } 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, generatedPackageDir, "generated_metrics.go")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_metrics_test.go")) } 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")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_config.go")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "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")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_config.go")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_config_test.go")) } if tt.wantTelemetryGenerated { - require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_telemetry.go")) - require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_telemetry_test.go")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry.go")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry_test.go")) require.FileExists(t, filepath.Join(tmpdir, "documentation.md")) - contents, err = os.ReadFile(filepath.Join(tmpdir, "internal/metadata/generated_telemetry.go")) // nolint: gosec + contents, err = os.ReadFile(filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry.go")) // nolint: gosec require.NoError(t, err) if tt.wantMetricsContext { require.Contains(t, string(contents), "\"context\"") @@ -188,8 +196,8 @@ foo require.NotContains(t, string(contents), "\"context\"") } } else { - require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_telemetry.go")) - require.NoFileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_telemetry_test.go")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry.go")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry_test.go")) } if !tt.wantMetricsGenerated && !tt.wantTelemetryGenerated && !tt.wantResourceAttributesGenerated { @@ -197,12 +205,12 @@ foo } if tt.wantStatusGenerated { - require.FileExists(t, filepath.Join(tmpdir, "internal/metadata/generated_status.go")) + require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "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")) + require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_status.go")) contents, err = os.ReadFile(filepath.Join(tmpdir, "README.md")) // nolint: gosec require.NoError(t, err) require.Contains(t, string(contents), "foo") @@ -466,7 +474,7 @@ Some info about a component 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) + err := inlineReplace("templates/readme.md.tmpl", readmeFile, md, statusStart, statusEnd, "metadata") require.NoError(t, err) require.FileExists(t, filepath.Join(tmpdir, "README.md")) diff --git a/cmd/mdatagen/internal/loader.go b/cmd/mdatagen/internal/loader.go index cdc59066f4e..b51fc86f141 100644 --- a/cmd/mdatagen/internal/loader.go +++ b/cmd/mdatagen/internal/loader.go @@ -276,6 +276,8 @@ type Metadata struct { Parent string `mapstructure:"parent"` // Status information for the component. Status *Status `mapstructure:"status"` + // The name of the package that will be generated. + GeneratedPackageName string `mapstructure:"generated_package_name"` // Telemetry information for the component. Telemetry telemetry `mapstructure:"telemetry"` // SemConvVersion is a version number of OpenTelemetry semantic conventions applied to the scraped metrics. @@ -330,6 +332,9 @@ func LoadMetadata(filePath string) (Metadata, error) { return md, err } } + if md.GeneratedPackageName == "" { + md.GeneratedPackageName = "metadata" + } if err = md.Validate(); err != nil { return md, err diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go index b99f088df3d..ed404744f75 100644 --- a/cmd/mdatagen/internal/loader_test.go +++ b/cmd/mdatagen/internal/loader_test.go @@ -22,9 +22,10 @@ func TestLoadMetadata(t *testing.T) { { name: "samplereceiver/metadata.yaml", want: Metadata{ - GithubProject: "open-telemetry/opentelemetry-collector", - Type: "sample", - SemConvVersion: "1.9.0", + GithubProject: "open-telemetry/opentelemetry-collector", + GeneratedPackageName: "metadata", + Type: "sample", + SemConvVersion: "1.9.0", Status: &Status{ Class: "receiver", Stability: map[component.StabilityLevel][]string{ @@ -297,11 +298,30 @@ func TestLoadMetadata(t *testing.T) { { name: "testdata/parent.yaml", want: Metadata{ - Type: "subcomponent", - Parent: "parentComponent", - ScopeName: "go.opentelemetry.io/collector/cmd/mdatagen/internal", - ShortFolderName: "testdata", - Tests: tests{Host: "componenttest.NewNopHost()"}, + Type: "subcomponent", + Parent: "parentComponent", + GeneratedPackageName: "metadata", + ScopeName: "go.opentelemetry.io/collector/cmd/mdatagen/internal", + ShortFolderName: "testdata", + Tests: tests{Host: "componenttest.NewNopHost()"}, + }, + }, + { + name: "testdata/generated_package_name.yaml", + want: Metadata{ + Type: "custom", + GeneratedPackageName: "customname", + ScopeName: "go.opentelemetry.io/collector/cmd/mdatagen/internal", + ShortFolderName: "testdata", + Tests: tests{Host: "componenttest.NewNopHost()"}, + Status: &Status{ + Class: "receiver", + Stability: map[component.StabilityLevel][]string{ + component.StabilityLevelDevelopment: {"logs"}, + component.StabilityLevelBeta: {"traces"}, + component.StabilityLevelStable: {"metrics"}, + }, + }, }, }, { diff --git a/cmd/mdatagen/internal/testdata/custom_generated_package_name.yaml b/cmd/mdatagen/internal/testdata/custom_generated_package_name.yaml new file mode 100644 index 00000000000..9797baf3105 --- /dev/null +++ b/cmd/mdatagen/internal/testdata/custom_generated_package_name.yaml @@ -0,0 +1,17 @@ +type: metricreceiver + +generated_package_name: custom + +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 + +tests: + skip_lifecycle: true + skip_shutdown: true diff --git a/cmd/mdatagen/internal/testdata/generated_package_name.yaml b/cmd/mdatagen/internal/testdata/generated_package_name.yaml new file mode 100644 index 00000000000..a880e69e608 --- /dev/null +++ b/cmd/mdatagen/internal/testdata/generated_package_name.yaml @@ -0,0 +1,10 @@ +type: custom + +generated_package_name: customname + +status: + class: receiver + stability: + development: [logs] + beta: [traces] + stable: [metrics] \ No newline at end of file diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 4e30a31b7e2..afd1f09b62a 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -7,6 +7,9 @@ parent: string # Optional: Scope name for the telemetry generated by the component. If not set, name of the go package will be used. scope_name: string +# Optional: The name of the package that mdatagen generates. If not set, the name "metadata" will be used. +generated_package_name: 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)