diff --git a/.chloggen/use-cobra-add-version.yaml b/.chloggen/use-cobra-add-version.yaml new file mode 100644 index 00000000000..1d5eddf6b89 --- /dev/null +++ b/.chloggen/use-cobra-add-version.yaml @@ -0,0 +1,20 @@ +# 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: Use cobra for the command, add version flag + +# One or more tracking issues or pull requests related to the change +issues: [11196] + +# 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 075a1fa9674..5eb7586596e 100644 --- a/cmd/mdatagen/README.md +++ b/cmd/mdatagen/README.md @@ -57,7 +57,7 @@ You can run `cd cmd/mdatagen && $(GOCMD) install .` to install the `mdatagen` to ## 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). +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). 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. diff --git a/cmd/mdatagen/go.mod b/cmd/mdatagen/go.mod index dd643f8c13d..300b5c6a8d6 100644 --- a/cmd/mdatagen/go.mod +++ b/cmd/mdatagen/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( github.com/google/go-cmp v0.6.0 + github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/collector/component v0.109.0 go.opentelemetry.io/collector/config/configtelemetry v0.109.0 @@ -30,6 +31,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.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 @@ -39,6 +41,7 @@ require ( 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 + github.com/spf13/pflag v1.0.5 // indirect go.opentelemetry.io/collector/consumer/consumerprofiles v0.109.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.109.0 // indirect go.opentelemetry.io/collector/receiver/receiverprofiles v0.109.0 // indirect diff --git a/cmd/mdatagen/go.sum b/cmd/mdatagen/go.sum index f73b8235de3..7dddf67f2b6 100644 --- a/cmd/mdatagen/go.sum +++ b/cmd/mdatagen/go.sum @@ -1,3 +1,4 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -15,6 +16,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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= @@ -42,6 +45,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go new file mode 100644 index 00000000000..fbd4088a34f --- /dev/null +++ b/cmd/mdatagen/internal/command.go @@ -0,0 +1,424 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "go.opentelemetry.io/collector/cmd/mdatagen/internal" + +import ( + "bytes" + "errors" + "fmt" + "go/format" + "os" + "path/filepath" + "regexp" + "runtime/debug" + "strings" + "text/template" + + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +const ( + statusStart = "" + statusEnd = "" +) + +func getVersion() (string, error) { + // the second returned value is a boolean, which is true if the binaries are built with module support. + info, ok := debug.ReadBuildInfo() + if !ok { + return "", errors.New("could not read build info") + } + return info.Main.Version, nil +} + +// NewCommand constructs a new cobra.Command using the given Settings. +// Any URIs specified in CollectorSettings.ConfigProviderSettings.ResolverSettings.URIs +// are considered defaults and will be overwritten by config flags passed as +// command-line arguments to the executable. +// At least one Provider must be set. +func NewCommand() (*cobra.Command, error) { + ver, err := getVersion() + if err != nil { + return nil, err + } + rootCmd := &cobra.Command{ + Use: "mdatagen", + Version: ver, + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + return run(args[0]) + }, + } + return rootCmd, nil +} + +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) + packageName := filepath.Base(ymlDir) + + 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" && !md.Status.NotComponent { + if err = generateFile(filepath.Join(tmplDir, "status.go.tmpl"), + filepath.Join(codeDir, "generated_status.go"), md, "metadata"); err != nil { + return err + } + if err = generateFile(filepath.Join(tmplDir, "component_test.go.tmpl"), + filepath.Join(ymlDir, "generated_component_test.go"), md, packageName); err != nil { + return err + } + } + + if err = generateFile(filepath.Join(tmplDir, "package_test.go.tmpl"), + filepath.Join(ymlDir, "generated_package_test.go"), md, packageName); 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 + } + } + } + + toGenerate := map[string]string{} + + if len(md.Telemetry.Metrics) != 0 { // if there are telemetry metrics, generate telemetry specific files + if err = generateFile(filepath.Join(tmplDir, "component_telemetry_test.go.tmpl"), + filepath.Join(ymlDir, "generated_component_telemetry_test.go"), md, packageName); err != nil { + return err + } + toGenerate[filepath.Join(tmplDir, "telemetry.go.tmpl")] = filepath.Join(codeDir, "generated_telemetry.go") + toGenerate[filepath.Join(tmplDir, "telemetry_test.go.tmpl")] = filepath.Join(codeDir, "generated_telemetry_test.go") + } + + if len(md.Metrics) != 0 || len(md.Telemetry.Metrics) != 0 { // if there's metrics or internal metrics, generate documentation for them + toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md") + } + + for tmpl, dst := range toGenerate { + if err = generateFile(tmpl, dst, md, "metadata"); 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) + } + + toGenerate = map[string]string{ + filepath.Join(tmplDir, "testdata", "config.yaml.tmpl"): filepath.Join(codeDir, "testdata", "config.yaml"), + filepath.Join(tmplDir, "config.go.tmpl"): filepath.Join(codeDir, "generated_config.go"), + filepath.Join(tmplDir, "config_test.go.tmpl"): filepath.Join(codeDir, "generated_config_test.go"), + } + + if len(md.ResourceAttributes) > 0 { // only generate resource files if resource attributes are configured + toGenerate[filepath.Join(tmplDir, "resource.go.tmpl")] = filepath.Join(codeDir, "generated_resource.go") + toGenerate[filepath.Join(tmplDir, "resource_test.go.tmpl")] = filepath.Join(codeDir, "generated_resource_test.go") + } + + if len(md.Metrics) > 0 { // only generate metrics if metrics are present + toGenerate[filepath.Join(tmplDir, "metrics.go.tmpl")] = filepath.Join(codeDir, "generated_metrics.go") + toGenerate[filepath.Join(tmplDir, "metrics_test.go.tmpl")] = filepath.Join(codeDir, "generated_metrics_test.go") + } + + for tmpl, dst := range toGenerate { + if err = generateFile(tmpl, dst, md, "metadata"); err != nil { + return err + } + } + + return nil +} + +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] + }, + "telemetryInfo": func(mn MetricName) Metric { + return md.Telemetry.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, + "toLowerCase": strings.ToLower, + "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" + }, + "isConnector": func() bool { + return md.Status.Class == "connector" + }, + "isCommand": func() bool { + return md.Status.Class == "cmd" + }, + "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 + }, + "supportsLogsToLogs": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "logs_to_logs" { + return true + } + } + } + return false + }, + "supportsLogsToMetrics": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "logs_to_metrics" { + return true + } + } + } + return false + }, + "supportsLogsToTraces": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "logs_to_traces" { + return true + } + } + } + return false + }, + "supportsMetricsToLogs": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "metrics_to_logs" { + return true + } + } + } + return false + }, + "supportsMetricsToMetrics": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "metrics_to_metrics" { + return true + } + } + } + return false + }, + "supportsMetricsToTraces": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "metrics_to_traces" { + return true + } + } + } + return false + }, + "supportsTracesToLogs": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "traces_to_logs" { + return true + } + } + } + return false + }, + "supportsTracesToMetrics": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "traces_to_metrics" { + return true + } + } + } + return false + }, + "supportsTracesToTraces": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "traces_to_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 md.GithubProject == "" { + md.GithubProject = "open-telemetry/opentelemetry-collector-contrib" + } + + 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/internal/command_test.go similarity index 95% rename from cmd/mdatagen/main_test.go rename to cmd/mdatagen/internal/command_test.go index 707f9bc45e1..b1d82b55f58 100644 --- a/cmd/mdatagen/main_test.go +++ b/cmd/mdatagen/internal/command_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package main +package internal import ( "bytes" @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/cmd/mdatagen/internal" "go.opentelemetry.io/collector/component" ) @@ -112,7 +111,7 @@ func TestRunContents(t *testing.T) { tmpdir := filepath.Join(t.TempDir(), "shortname") err := os.MkdirAll(tmpdir, 0750) require.NoError(t, err) - ymlContent, err := os.ReadFile(filepath.Join("internal/testdata", tt.yml)) + 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)) @@ -266,7 +265,7 @@ func TestInlineReplace(t *testing.T) { warnings []string stability map[component.StabilityLevel][]string distros []string - codeowners *internal.Codeowners + codeowners *Codeowners githubProject string }{ { @@ -308,7 +307,7 @@ Some info about a component outputFile: "readme_with_status_codeowners_and_seeking_new.md", componentClass: "receiver", distros: []string{"contrib"}, - codeowners: &internal.Codeowners{ + codeowners: &Codeowners{ Active: []string{"foo"}, SeekingNew: true, }, @@ -325,7 +324,7 @@ Some info about a component outputFile: "readme_with_status_codeowners_and_emeritus.md", componentClass: "receiver", distros: []string{"contrib"}, - codeowners: &internal.Codeowners{ + codeowners: &Codeowners{ Active: []string{"foo"}, Emeritus: []string{"bar"}, }, @@ -342,7 +341,7 @@ Some info about a component outputFile: "readme_with_status_codeowners.md", componentClass: "receiver", distros: []string{"contrib"}, - codeowners: &internal.Codeowners{ + codeowners: &Codeowners{ Active: []string{"foo"}, }, }, @@ -426,11 +425,11 @@ Some info about a component if len(tt.stability) > 0 { stability = tt.stability } - md := internal.Metadata{ + md := Metadata{ GithubProject: tt.githubProject, Type: "foo", ShortFolderName: "foo", - Status: &internal.Status{ + Status: &Status{ Stability: stability, Distributions: tt.distros, Class: tt.componentClass, @@ -450,7 +449,7 @@ Some info about a component 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("internal/testdata", tt.outputFile)) + 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)) @@ -464,14 +463,14 @@ func TestGenerateStatusMetadata(t *testing.T) { tests := []struct { name string output string - md internal.Metadata + md Metadata expected string }{ { name: "foo component with beta status", - md: internal.Metadata{ + md: Metadata{ Type: "foo", - Status: &internal.Status{ + Status: &Status{ Stability: map[component.StabilityLevel][]string{ component.StabilityLevelBeta: {"metrics"}, }, @@ -499,9 +498,9 @@ const ( }, { name: "foo component with alpha status", - md: internal.Metadata{ + md: Metadata{ Type: "foo", - Status: &internal.Status{ + Status: &Status{ Stability: map[component.StabilityLevel][]string{ component.StabilityLevelAlpha: {"metrics"}, }, @@ -546,14 +545,14 @@ func TestGenerateTelemetryMetadata(t *testing.T) { tests := []struct { name string output string - md internal.Metadata + md Metadata expected string }{ { name: "foo component with beta status", - md: internal.Metadata{ + md: Metadata{ Type: "foo", - Status: &internal.Status{ + Status: &Status{ Stability: map[component.StabilityLevel][]string{ component.StabilityLevelBeta: {"metrics"}, }, @@ -589,9 +588,9 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer { }, { name: "foo component with alpha status", - md: internal.Metadata{ + md: Metadata{ Type: "foo", - Status: &internal.Status{ + Status: &Status{ Stability: map[component.StabilityLevel][]string{ component.StabilityLevelAlpha: {"metrics"}, }, diff --git a/cmd/mdatagen/main.go b/cmd/mdatagen/main.go index 0f27b925fce..32bab21949d 100644 --- a/cmd/mdatagen/main.go +++ b/cmd/mdatagen/main.go @@ -1,410 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//go:generate mdatagen metadata.yaml - package main -import ( - "bytes" - "errors" - "flag" - "fmt" - "go/format" - "log" - "os" - "path/filepath" - "regexp" - "strings" - "text/template" +//go:generate mdatagen metadata.yaml - "golang.org/x/text/cases" - "golang.org/x/text/language" +import ( + "github.com/spf13/cobra" "go.opentelemetry.io/collector/cmd/mdatagen/internal" ) -const ( - statusStart = "" - statusEnd = "" -) - func main() { - flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s metadata.yaml\n", os.Args[0]) - flag.PrintDefaults() - } - 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) - packageName := filepath.Base(ymlDir) - - md, err := internal.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" && !md.Status.NotComponent { - if err = generateFile(filepath.Join(tmplDir, "status.go.tmpl"), - filepath.Join(codeDir, "generated_status.go"), md, "metadata"); err != nil { - return err - } - if err = generateFile(filepath.Join(tmplDir, "component_test.go.tmpl"), - filepath.Join(ymlDir, "generated_component_test.go"), md, packageName); err != nil { - return err - } - } - - if err = generateFile(filepath.Join(tmplDir, "package_test.go.tmpl"), - filepath.Join(ymlDir, "generated_package_test.go"), md, packageName); 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 - } - } - } - - toGenerate := map[string]string{} - - if len(md.Telemetry.Metrics) != 0 { // if there are telemetry metrics, generate telemetry specific files - if err = generateFile(filepath.Join(tmplDir, "component_telemetry_test.go.tmpl"), - filepath.Join(ymlDir, "generated_component_telemetry_test.go"), md, packageName); err != nil { - return err - } - toGenerate[filepath.Join(tmplDir, "telemetry.go.tmpl")] = filepath.Join(codeDir, "generated_telemetry.go") - toGenerate[filepath.Join(tmplDir, "telemetry_test.go.tmpl")] = filepath.Join(codeDir, "generated_telemetry_test.go") - } - - if len(md.Metrics) != 0 || len(md.Telemetry.Metrics) != 0 { // if there's metrics or internal metrics, generate documentation for them - toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md") - } - - for tmpl, dst := range toGenerate { - if err = generateFile(tmpl, dst, md, "metadata"); 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) - } - - toGenerate = map[string]string{ - filepath.Join(tmplDir, "testdata", "config.yaml.tmpl"): filepath.Join(codeDir, "testdata", "config.yaml"), - filepath.Join(tmplDir, "config.go.tmpl"): filepath.Join(codeDir, "generated_config.go"), - filepath.Join(tmplDir, "config_test.go.tmpl"): filepath.Join(codeDir, "generated_config_test.go"), - } - - if len(md.ResourceAttributes) > 0 { // only generate resource files if resource attributes are configured - toGenerate[filepath.Join(tmplDir, "resource.go.tmpl")] = filepath.Join(codeDir, "generated_resource.go") - toGenerate[filepath.Join(tmplDir, "resource_test.go.tmpl")] = filepath.Join(codeDir, "generated_resource_test.go") - } - - if len(md.Metrics) > 0 { // only generate metrics if metrics are present - toGenerate[filepath.Join(tmplDir, "metrics.go.tmpl")] = filepath.Join(codeDir, "generated_metrics.go") - toGenerate[filepath.Join(tmplDir, "metrics_test.go.tmpl")] = filepath.Join(codeDir, "generated_metrics_test.go") - } - - for tmpl, dst := range toGenerate { - if err = generateFile(tmpl, dst, md, "metadata"); err != nil { - return err - } - } - - return nil -} - -func templatize(tmplFile string, md internal.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 internal.FormatIdentifier(s, true) - }, - "attributeInfo": func(an internal.AttributeName) internal.Attribute { - return md.Attributes[an] - }, - "metricInfo": func(mn internal.MetricName) internal.Metric { - return md.Metrics[mn] - }, - "telemetryInfo": func(mn internal.MetricName) internal.Metric { - return md.Telemetry.Metrics[mn] - }, - "parseImportsRequired": func(metrics map[internal.MetricName]internal.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, - "toLowerCase": strings.ToLower, - "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 internal.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" - }, - "isConnector": func() bool { - return md.Status.Class == "connector" - }, - "isCommand": func() bool { - return md.Status.Class == "cmd" - }, - "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 - }, - "supportsLogsToLogs": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "logs_to_logs" { - return true - } - } - } - return false - }, - "supportsLogsToMetrics": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "logs_to_metrics" { - return true - } - } - } - return false - }, - "supportsLogsToTraces": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "logs_to_traces" { - return true - } - } - } - return false - }, - "supportsMetricsToLogs": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "metrics_to_logs" { - return true - } - } - } - return false - }, - "supportsMetricsToMetrics": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "metrics_to_metrics" { - return true - } - } - } - return false - }, - "supportsMetricsToTraces": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "metrics_to_traces" { - return true - } - } - } - return false - }, - "supportsTracesToLogs": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "traces_to_logs" { - return true - } - } - } - return false - }, - "supportsTracesToMetrics": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "traces_to_metrics" { - return true - } - } - } - return false - }, - "supportsTracesToTraces": func() bool { - for _, signals := range md.Status.Stability { - for _, s := range signals { - if s == "traces_to_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(internal.TemplateFS, strings.ReplaceAll(tmplFile, "\\", "/"))) -} - -func inlineReplace(tmplFile string, outputFile string, md internal.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 md.GithubProject == "" { - md.GithubProject = "open-telemetry/opentelemetry-collector-contrib" - } - - if err := tmpl.Execute(&buf, internal.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 internal.Metadata, goPackage string) error { - tmpl := templatize(tmplFile, md) - buf := bytes.Buffer{} - - if err := tmpl.Execute(&buf, internal.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 + cmd, err := internal.NewCommand() + cobra.CheckErr(err) + cobra.CheckErr(cmd.Execute()) }