From 32369e5d9252f28ee84616082047eaeb2c6e7487 Mon Sep 17 00:00:00 2001
From: Gemene Narcis <naarcis96@gmail.com>
Date: Wed, 16 Oct 2024 13:42:20 +0300
Subject: [PATCH 1/4] [cmd/mdatagen]: Add feature gates support to
 metadata-schema.yaml

---
 cmd/mdatagen/internal/command.go              |   6 +-
 .../internal/embedded_templates_test.go       |   1 +
 cmd/mdatagen/internal/loader.go               | 363 ++++++++++++++++++
 cmd/mdatagen/internal/loader_test.go          |  86 ++++-
 .../internal/sampleprocessor/metadata.yaml    |  21 +
 .../internal/templates/feature_gates.go.tmpl  |  22 ++
 cmd/mdatagen/metadata-schema.yaml             |  17 +
 7 files changed, 514 insertions(+), 2 deletions(-)
 create mode 100644 cmd/mdatagen/internal/templates/feature_gates.go.tmpl

diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go
index 9d64f4492af..2c52cc623a2 100644
--- a/cmd/mdatagen/internal/command.go
+++ b/cmd/mdatagen/internal/command.go
@@ -120,6 +120,10 @@ func run(ymlPath string) error {
 		toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md")
 	}
 
+	if len(md.FeatureGates) != 0 {
+		toGenerate[filepath.Join(tmplDir, "feature_gates.go.tmpl")] = filepath.Join(ymlDir, "generated_feature_gates.go")
+	}
+
 	for tmpl, dst := range toGenerate {
 		if err = generateFile(tmpl, dst, md, "metadata"); err != nil {
 			return err
@@ -376,7 +380,7 @@ func inlineReplace(tmplFile string, outputFile string, md Metadata, start string
 		return err
 	}
 
-	var re = regexp.MustCompile(fmt.Sprintf("%s[\\s\\S]*%s", start, end))
+	re := regexp.MustCompile(fmt.Sprintf("%s[\\s\\S]*%s", start, end))
 	if !re.Match(readmeContents) {
 		return nil
 	}
diff --git a/cmd/mdatagen/internal/embedded_templates_test.go b/cmd/mdatagen/internal/embedded_templates_test.go
index 97bc268edb3..4858d87166c 100644
--- a/cmd/mdatagen/internal/embedded_templates_test.go
+++ b/cmd/mdatagen/internal/embedded_templates_test.go
@@ -24,6 +24,7 @@ func TestEnsureTemplatesLoaded(t *testing.T) {
 			path.Join(rootDir, "component_test.go.tmpl"):           {},
 			path.Join(rootDir, "component_telemetry_test.go.tmpl"): {},
 			path.Join(rootDir, "documentation.md.tmpl"):            {},
+			path.Join(rootDir, "feature_gates.go.tmpl"):            {},
 			path.Join(rootDir, "metrics.go.tmpl"):                  {},
 			path.Join(rootDir, "metrics_test.go.tmpl"):             {},
 			path.Join(rootDir, "resource.go.tmpl"):                 {},
diff --git a/cmd/mdatagen/internal/loader.go b/cmd/mdatagen/internal/loader.go
index 02896460548..174e331c333 100644
--- a/cmd/mdatagen/internal/loader.go
+++ b/cmd/mdatagen/internal/loader.go
@@ -5,14 +5,327 @@ package internal // import "go.opentelemetry.io/collector/cmd/mdatagen/internal"
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/config/configtelemetry"
+	"go.opentelemetry.io/collector/confmap"
 	"go.opentelemetry.io/collector/confmap/confmaptest"
 	"go.opentelemetry.io/collector/confmap/provider/fileprovider"
+	"go.opentelemetry.io/collector/filter"
+	"go.opentelemetry.io/collector/pdata/pcommon"
 )
 
+var (
+	// idRegexp is used to validate the ID of a Gate.
+	// IDs' characters must be alphanumeric or dots.
+	idRegexp           = regexp.MustCompile(`^[0-9a-zA-Z\.]*$`)
+	versionRegexp      = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
+	referenceURLRegexp = regexp.MustCompile(`^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(\/[^\s]*)?$`)
+	validStages        = map[string]bool{
+		"StageAlpha":      true,
+		"StageBeta":       true,
+		"StageStable":     true,
+		"StageDeprecated": true,
+	}
+)
+
+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)
+}
+
+type featureGateName string
+
+func (mn featureGateName) Render() (string, error) {
+	return FormatIdentifier(string(mn), true)
+}
+
+func (mn featureGateName) 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 stability struct {
+	Level string `mapstructure:"level"`
+	From  string `mapstructure:"from"`
+}
+
+func (s stability) String() string {
+	if len(s.Level) == 0 || strings.EqualFold(s.Level, component.StabilityLevelStable.String()) {
+		return ""
+	}
+	if len(s.From) > 0 {
+		return fmt.Sprintf(" [%s since %s]", s.Level, s.From)
+	}
+	return fmt.Sprintf(" [%s]", s.Level)
+}
+
+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"`
+
+	// The stability level of the metric.
+	Stability stability `mapstructure:"stability"`
+
+	// ExtendedDocumentation of the metric. If specified, this will
+	// be appended to the description used in generated documentation.
+	ExtendedDocumentation string `mapstructure:"extended_documentation"`
+
+	// Optional can be used to specify metrics that may
+	// or may not be present in all cases, depending on configuration.
+	Optional bool `mapstructure:"optional"`
+
+	// 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"`
+	// Histogram stores metadata for histogram metric type
+	Histogram *histogram `mapstructure:"histogram,omitempty"`
+
+	// Attributes is the list of attributes that the metric emits.
+	Attributes []AttributeName `mapstructure:"attributes"`
+
+	// Level specifies the minimum `configtelemetry.Level` for which
+	// the metric will be emitted. This only applies to internal telemetry
+	// configuration.
+	Level configtelemetry.Level `mapstructure:"level"`
+}
+
+func (m *Metric) Unmarshal(parser *confmap.Conf) error {
+	if !parser.IsSet("enabled") {
+		return errors.New("missing required field: `enabled`")
+	}
+	return parser.Unmarshal(m)
+}
+
+func (m Metric) Data() MetricData {
+	if m.Sum != nil {
+		return m.Sum
+	}
+	if m.Gauge != nil {
+		return m.Gauge
+	}
+	if m.Histogram != nil {
+		return m.Histogram
+	}
+	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"`
+	// Include can be used to filter attributes.
+	Include []filter.Config `mapstructure:"include"`
+	// Include can be used to filter attributes.
+	Exclude []filter.Config `mapstructure:"exclude"`
+	// 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(`[]byte("%s-val")`, a.FullName)
+	}
+	return ""
+}
+
+type ignore struct {
+	Top []string `mapstructure:"top"`
+	Any []string `mapstructure:"any"`
+}
+
+type goLeak struct {
+	Skip     bool   `mapstructure:"skip"`
+	Ignore   ignore `mapstructure:"ignore"`
+	Setup    string `mapstructure:"setup"`
+	Teardown string `mapstructure:"teardown"`
+}
+
+type tests struct {
+	Config              any    `mapstructure:"config"`
+	SkipLifecycle       bool   `mapstructure:"skip_lifecycle"`
+	SkipShutdown        bool   `mapstructure:"skip_shutdown"`
+	GoLeak              goLeak `mapstructure:"goleak"`
+	ExpectConsumerError bool   `mapstructure:"expect_consumer_error"`
+	Host                string `mapstructure:"host"`
+}
+
+type telemetry struct {
+	Level   configtelemetry.Level `mapstructure:"level"`
+	Metrics map[MetricName]Metric `mapstructure:"metrics"`
+}
+
+func (t telemetry) Levels() map[string]interface{} {
+	levels := map[string]interface{}{}
+	for _, m := range t.Metrics {
+		levels[m.Level.String()] = nil
+	}
+	return levels
+}
+
+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"`
+	// 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.
+	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"`
+	// GithubProject is the project where the component README lives in the format of org/repo, defaults to open-telemetry/opentelemetry-collector-contrib
+	GithubProject string `mapstructure:"github_project"`
+	// ScopeName of the metrics emitted by the component.
+	ScopeName string `mapstructure:"scope_name"`
+	// ShortFolderName is the shortened folder name of the component, removing class if present
+	ShortFolderName string `mapstructure:"-"`
+	// Tests is the set of tests generated with the component
+	Tests tests `mapstructure:"tests"`
+	// FeatureGates that can be used for the component.
+	FeatureGates map[featureGateName]featureGate `mapstructure:"feature_gates"`
+}
+
 func setAttributesFullName(attrs map[AttributeName]Attribute) {
 	for k, v := range attrs {
 		v.FullName = k
@@ -88,3 +401,53 @@ func packageName() (string, error) {
 	}
 	return strings.TrimSpace(string(output)), nil
 }
+
+type featureGate struct {
+	// Required.
+	ID string `mapstructure:"id"`
+	// Description describes the purpose of the attribute.
+	Description string `mapstructure:"description"`
+	// Stage current stage at which the feature gate is in the development lifecyle
+	Stage string `mapstructure:"stage"`
+	// ReferenceURL can optionally give the url of the feature_gate
+	ReferenceURL string `mapstructure:"reference_url"`
+	// FromVersion optional field which gives the release version from which the gate has been given the current stage
+	FromVersion string `mapstructure:"from_version"`
+	// ToVersion optional field which gives the release version till which the gate the gate had the given lifecycle stage
+	ToVersion string `mapstructure:"to_version"`
+	// FeatureGateName name of the feature gate
+	FeatureGateName featureGateName `mapstructure:"-"`
+}
+
+func (f *featureGate) validate(parser *confmap.Conf) error {
+	var err []error
+	if !parser.IsSet("id") {
+		err = append(err, errors.New("missing required field: `id`"))
+	} else if !idRegexp.MatchString(fmt.Sprintf("%v", parser.Get("id"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in ID"))
+	}
+
+	if !parser.IsSet("stage") {
+		err = append(err, errors.New("missing required field: `stage`"))
+	} else if _, ok := validStages[fmt.Sprintf("%v", parser.Get("stage"))]; !ok {
+		err = append(err, fmt.Errorf("invalid stage"))
+	}
+
+	if parser.IsSet("from_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("from_version"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in from_version"))
+	}
+	if parser.IsSet("to_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("to_version"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in to_version"))
+	}
+	if parser.IsSet("reference_url") && !referenceURLRegexp.MatchString(fmt.Sprintf("%v", parser.Get("reference_url"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in reference_url"))
+	}
+	return errors.Join(err...)
+}
+
+func (f *featureGate) Unmarshal(parser *confmap.Conf) error {
+	if err := f.validate(parser); err != nil {
+		return err
+	}
+	return parser.Unmarshal(f)
+}
diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go
index 19f3e118410..bda93f536ce 100644
--- a/cmd/mdatagen/internal/loader_test.go
+++ b/cmd/mdatagen/internal/loader_test.go
@@ -4,11 +4,13 @@
 package internal
 
 import (
+	"errors"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
 	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/confmap"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	"go.opentelemetry.io/collector/pdata/pmetric"
 )
@@ -369,6 +371,88 @@ func TestLoadMetadata(t *testing.T) {
 	}
 }
 
+func TestFeatureGateValidate(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       map[string]interface{}
+		expectedErr error
+	}{
+		{
+			name: "valid input",
+			input: map[string]interface{}{
+				"id":            "valid.id",
+				"stage":         "StageAlpha",
+				"from_version":  "v1.2.3",
+				"to_version":    "v1.3.0",
+				"reference_url": "http://example.com",
+			},
+			expectedErr: nil,
+		},
+		{
+			name: "missing id",
+			input: map[string]interface{}{
+				"stage": "StageAlpha",
+			},
+			expectedErr: errors.New("missing required field: `id`"),
+		},
+		{
+			name: "invalid id",
+			input: map[string]interface{}{
+				"id":    "invalid@id",
+				"stage": "StageAlpha",
+			},
+			expectedErr: errors.New("invalid character(s) in ID"),
+		},
+		{
+			name: "missing stage",
+			input: map[string]interface{}{
+				"id": "valid.id",
+			},
+			expectedErr: errors.New("missing required field: `stage`"),
+		},
+		{
+			name: "invalid stage",
+			input: map[string]interface{}{
+				"id":    "valid.id",
+				"stage": "InvalidStage",
+			},
+			expectedErr: errors.New("invalid stage"),
+		},
+		{
+			name: "invalid from_version",
+			input: map[string]interface{}{
+				"id":           "valid.id",
+				"stage":        "StageAlpha",
+				"from_version": "v1.2.a",
+			},
+			expectedErr: errors.New("invalid character(s) in from_version"),
+		},
+		{
+			name: "invalid reference_url",
+			input: map[string]interface{}{
+				"id":            "valid.id",
+				"stage":         "StageAlpha",
+				"reference_url": "invalid-url",
+			},
+			expectedErr: errors.New("invalid character(s) in reference_url"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			parser := confmap.NewFromStringMap(tt.input)
+			featureGate := &featureGate{}
+			err := featureGate.validate(parser)
+			if tt.expectedErr != nil {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expectedErr.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
 func strPtr(s string) *string {
 	return &s
 }
diff --git a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
index 521bd62c452..f2152be8edc 100644
--- a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
+++ b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
@@ -66,3 +66,24 @@ resource_attributes:
     enabled: true
     warnings:
       if_enabled: This resource_attribute is deprecated and will be removed soon.
+
+feature_gates:
+  useOTTLBridge:
+    id: useOTTLBridgeGate
+    stage: StageAlpha
+    description: When enabled, filterlog will convert filterlog configuration to OTTL and use filterottl evaluation
+    from_version": 1.0.0
+    reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18642
+    to_version: 2.0.0
+
+
+  allowFileDeletion:
+    id: allowFileDeletionGate
+    stage: StageStable
+    description: When enabled, allows usage of the `delete_after_read` setting.
+    from_version": 1.5.0
+    reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/16314
+
+  allowFileCreation:
+    id: allowFileCreationGate
+    stage: StageStable
\ No newline at end of file
diff --git a/cmd/mdatagen/internal/templates/feature_gates.go.tmpl b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
new file mode 100644
index 00000000000..e6d871e5c1c
--- /dev/null
+++ b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
@@ -0,0 +1,22 @@
+// Code generated by mdatagen. DO NOT EDIT.
+
+package {{ .Package }}
+
+import (
+	"go.opentelemetry.io/collector/featuregate"
+)
+
+func init() {
+{{- range .FeatureGates }}
+    {
+        var opts []featuregate.RegisterOption
+        opts = append(opts
+            {{- if .from_version }}, featuregate.WithRegisterFromVersion("{{.from_version}}") {{- end }}
+            {{- if .description }}, featuregate.WithRegisterDescription("{{.description}}") {{- end }}
+            {{- if .reference_url }}, featuregate.WithRegisterReferenceURL("{{.reference_url}}") {{- end }}
+            {{- if .to_version }}, featuregate.WithRegisterToVersion("{{.to_version}}") {{- end }})
+        _ = featuregate.GlobalRegistry().MustRegister("{{.id}}", featuregate.{{.stage}}, opts...)
+    }
+{{- end }}
+
+}
\ No newline at end of file
diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml
index afd1f09b62a..3d2d08ddd70 100644
--- a/cmd/mdatagen/metadata-schema.yaml
+++ b/cmd/mdatagen/metadata-schema.yaml
@@ -175,3 +175,20 @@ telemetry:
       # Optional: array of attributes that were defined in the attributes section that are emitted by this metric.
       # Note: Only the following attribute types are supported: <string|int|double|bool>
       attributes: [string]
+
+#Optional: Gate is an immutable object that is owned by the Registry and represents an individual feature that
+# may be enabled or disabled based on the lifecycle state of the feature and CLI flags specified by the user.
+feature_gates:
+  <feature_gate.name>:
+    #Required: id of the feature gate
+    id:
+    #Optional: description of the gate
+    description:
+    #Required: current stage at which the feature gate is in the development lifecyle
+    stage:
+    #Optional: link to the issue where the gate has been discussed
+    reference_url:
+    #Optional: the release version from which the gate has been given the current stage
+    from_version:
+    #Optional: the release version till which the gate had the given lifecycle stage
+    to_version:
\ No newline at end of file

From bb3c744df02f25bf094db5bf3639ad5931495c44 Mon Sep 17 00:00:00 2001
From: Narcis Gemene <7252787+narcis96@users.noreply.github.com>
Date: Thu, 24 Oct 2024 09:15:57 +0100
Subject: [PATCH 2/4] Update
 cmd/mdatagen/internal/sampleprocessor/metadata.yaml

Co-authored-by: Pablo Baeyens <pbaeyens31+github@gmail.com>
---
 cmd/mdatagen/internal/sampleprocessor/metadata.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
index f2152be8edc..448cc2c64b4 100644
--- a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
+++ b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
@@ -72,7 +72,7 @@ feature_gates:
     id: useOTTLBridgeGate
     stage: StageAlpha
     description: When enabled, filterlog will convert filterlog configuration to OTTL and use filterottl evaluation
-    from_version": 1.0.0
+    from_version: 1.0.0
     reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18642
     to_version: 2.0.0
 

From 01e73a227d74268079602b4fa36b81800d37eeda Mon Sep 17 00:00:00 2001
From: Narcis Gemene <7252787+narcis96@users.noreply.github.com>
Date: Thu, 24 Oct 2024 09:16:04 +0100
Subject: [PATCH 3/4] Update
 cmd/mdatagen/internal/sampleprocessor/metadata.yaml

Co-authored-by: Pablo Baeyens <pbaeyens31+github@gmail.com>
---
 cmd/mdatagen/internal/loader.go               | 363 ------------------
 cmd/mdatagen/internal/loader_test.go          |  86 +----
 cmd/mdatagen/internal/metadata.go             |  77 ++++
 cmd/mdatagen/internal/metadata_test.go        |  68 ++++
 .../internal/sampleprocessor/metadata.yaml    |   7 +-
 .../internal/templates/feature_gates.go.tmpl  |  12 +-
 6 files changed, 155 insertions(+), 458 deletions(-)

diff --git a/cmd/mdatagen/internal/loader.go b/cmd/mdatagen/internal/loader.go
index 174e331c333..02896460548 100644
--- a/cmd/mdatagen/internal/loader.go
+++ b/cmd/mdatagen/internal/loader.go
@@ -5,327 +5,14 @@ package internal // import "go.opentelemetry.io/collector/cmd/mdatagen/internal"
 
 import (
 	"context"
-	"errors"
-	"fmt"
 	"os/exec"
 	"path/filepath"
-	"regexp"
 	"strings"
 
-	"go.opentelemetry.io/collector/component"
-	"go.opentelemetry.io/collector/config/configtelemetry"
-	"go.opentelemetry.io/collector/confmap"
 	"go.opentelemetry.io/collector/confmap/confmaptest"
 	"go.opentelemetry.io/collector/confmap/provider/fileprovider"
-	"go.opentelemetry.io/collector/filter"
-	"go.opentelemetry.io/collector/pdata/pcommon"
 )
 
-var (
-	// idRegexp is used to validate the ID of a Gate.
-	// IDs' characters must be alphanumeric or dots.
-	idRegexp           = regexp.MustCompile(`^[0-9a-zA-Z\.]*$`)
-	versionRegexp      = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
-	referenceURLRegexp = regexp.MustCompile(`^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(\/[^\s]*)?$`)
-	validStages        = map[string]bool{
-		"StageAlpha":      true,
-		"StageBeta":       true,
-		"StageStable":     true,
-		"StageDeprecated": true,
-	}
-)
-
-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)
-}
-
-type featureGateName string
-
-func (mn featureGateName) Render() (string, error) {
-	return FormatIdentifier(string(mn), true)
-}
-
-func (mn featureGateName) 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 stability struct {
-	Level string `mapstructure:"level"`
-	From  string `mapstructure:"from"`
-}
-
-func (s stability) String() string {
-	if len(s.Level) == 0 || strings.EqualFold(s.Level, component.StabilityLevelStable.String()) {
-		return ""
-	}
-	if len(s.From) > 0 {
-		return fmt.Sprintf(" [%s since %s]", s.Level, s.From)
-	}
-	return fmt.Sprintf(" [%s]", s.Level)
-}
-
-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"`
-
-	// The stability level of the metric.
-	Stability stability `mapstructure:"stability"`
-
-	// ExtendedDocumentation of the metric. If specified, this will
-	// be appended to the description used in generated documentation.
-	ExtendedDocumentation string `mapstructure:"extended_documentation"`
-
-	// Optional can be used to specify metrics that may
-	// or may not be present in all cases, depending on configuration.
-	Optional bool `mapstructure:"optional"`
-
-	// 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"`
-	// Histogram stores metadata for histogram metric type
-	Histogram *histogram `mapstructure:"histogram,omitempty"`
-
-	// Attributes is the list of attributes that the metric emits.
-	Attributes []AttributeName `mapstructure:"attributes"`
-
-	// Level specifies the minimum `configtelemetry.Level` for which
-	// the metric will be emitted. This only applies to internal telemetry
-	// configuration.
-	Level configtelemetry.Level `mapstructure:"level"`
-}
-
-func (m *Metric) Unmarshal(parser *confmap.Conf) error {
-	if !parser.IsSet("enabled") {
-		return errors.New("missing required field: `enabled`")
-	}
-	return parser.Unmarshal(m)
-}
-
-func (m Metric) Data() MetricData {
-	if m.Sum != nil {
-		return m.Sum
-	}
-	if m.Gauge != nil {
-		return m.Gauge
-	}
-	if m.Histogram != nil {
-		return m.Histogram
-	}
-	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"`
-	// Include can be used to filter attributes.
-	Include []filter.Config `mapstructure:"include"`
-	// Include can be used to filter attributes.
-	Exclude []filter.Config `mapstructure:"exclude"`
-	// 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(`[]byte("%s-val")`, a.FullName)
-	}
-	return ""
-}
-
-type ignore struct {
-	Top []string `mapstructure:"top"`
-	Any []string `mapstructure:"any"`
-}
-
-type goLeak struct {
-	Skip     bool   `mapstructure:"skip"`
-	Ignore   ignore `mapstructure:"ignore"`
-	Setup    string `mapstructure:"setup"`
-	Teardown string `mapstructure:"teardown"`
-}
-
-type tests struct {
-	Config              any    `mapstructure:"config"`
-	SkipLifecycle       bool   `mapstructure:"skip_lifecycle"`
-	SkipShutdown        bool   `mapstructure:"skip_shutdown"`
-	GoLeak              goLeak `mapstructure:"goleak"`
-	ExpectConsumerError bool   `mapstructure:"expect_consumer_error"`
-	Host                string `mapstructure:"host"`
-}
-
-type telemetry struct {
-	Level   configtelemetry.Level `mapstructure:"level"`
-	Metrics map[MetricName]Metric `mapstructure:"metrics"`
-}
-
-func (t telemetry) Levels() map[string]interface{} {
-	levels := map[string]interface{}{}
-	for _, m := range t.Metrics {
-		levels[m.Level.String()] = nil
-	}
-	return levels
-}
-
-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"`
-	// 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.
-	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"`
-	// GithubProject is the project where the component README lives in the format of org/repo, defaults to open-telemetry/opentelemetry-collector-contrib
-	GithubProject string `mapstructure:"github_project"`
-	// ScopeName of the metrics emitted by the component.
-	ScopeName string `mapstructure:"scope_name"`
-	// ShortFolderName is the shortened folder name of the component, removing class if present
-	ShortFolderName string `mapstructure:"-"`
-	// Tests is the set of tests generated with the component
-	Tests tests `mapstructure:"tests"`
-	// FeatureGates that can be used for the component.
-	FeatureGates map[featureGateName]featureGate `mapstructure:"feature_gates"`
-}
-
 func setAttributesFullName(attrs map[AttributeName]Attribute) {
 	for k, v := range attrs {
 		v.FullName = k
@@ -401,53 +88,3 @@ func packageName() (string, error) {
 	}
 	return strings.TrimSpace(string(output)), nil
 }
-
-type featureGate struct {
-	// Required.
-	ID string `mapstructure:"id"`
-	// Description describes the purpose of the attribute.
-	Description string `mapstructure:"description"`
-	// Stage current stage at which the feature gate is in the development lifecyle
-	Stage string `mapstructure:"stage"`
-	// ReferenceURL can optionally give the url of the feature_gate
-	ReferenceURL string `mapstructure:"reference_url"`
-	// FromVersion optional field which gives the release version from which the gate has been given the current stage
-	FromVersion string `mapstructure:"from_version"`
-	// ToVersion optional field which gives the release version till which the gate the gate had the given lifecycle stage
-	ToVersion string `mapstructure:"to_version"`
-	// FeatureGateName name of the feature gate
-	FeatureGateName featureGateName `mapstructure:"-"`
-}
-
-func (f *featureGate) validate(parser *confmap.Conf) error {
-	var err []error
-	if !parser.IsSet("id") {
-		err = append(err, errors.New("missing required field: `id`"))
-	} else if !idRegexp.MatchString(fmt.Sprintf("%v", parser.Get("id"))) {
-		err = append(err, fmt.Errorf("invalid character(s) in ID"))
-	}
-
-	if !parser.IsSet("stage") {
-		err = append(err, errors.New("missing required field: `stage`"))
-	} else if _, ok := validStages[fmt.Sprintf("%v", parser.Get("stage"))]; !ok {
-		err = append(err, fmt.Errorf("invalid stage"))
-	}
-
-	if parser.IsSet("from_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("from_version"))) {
-		err = append(err, fmt.Errorf("invalid character(s) in from_version"))
-	}
-	if parser.IsSet("to_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("to_version"))) {
-		err = append(err, fmt.Errorf("invalid character(s) in to_version"))
-	}
-	if parser.IsSet("reference_url") && !referenceURLRegexp.MatchString(fmt.Sprintf("%v", parser.Get("reference_url"))) {
-		err = append(err, fmt.Errorf("invalid character(s) in reference_url"))
-	}
-	return errors.Join(err...)
-}
-
-func (f *featureGate) Unmarshal(parser *confmap.Conf) error {
-	if err := f.validate(parser); err != nil {
-		return err
-	}
-	return parser.Unmarshal(f)
-}
diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go
index bda93f536ce..19f3e118410 100644
--- a/cmd/mdatagen/internal/loader_test.go
+++ b/cmd/mdatagen/internal/loader_test.go
@@ -4,13 +4,11 @@
 package internal
 
 import (
-	"errors"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
 	"go.opentelemetry.io/collector/component"
-	"go.opentelemetry.io/collector/confmap"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	"go.opentelemetry.io/collector/pdata/pmetric"
 )
@@ -371,88 +369,6 @@ func TestLoadMetadata(t *testing.T) {
 	}
 }
 
-func TestFeatureGateValidate(t *testing.T) {
-	tests := []struct {
-		name        string
-		input       map[string]interface{}
-		expectedErr error
-	}{
-		{
-			name: "valid input",
-			input: map[string]interface{}{
-				"id":            "valid.id",
-				"stage":         "StageAlpha",
-				"from_version":  "v1.2.3",
-				"to_version":    "v1.3.0",
-				"reference_url": "http://example.com",
-			},
-			expectedErr: nil,
-		},
-		{
-			name: "missing id",
-			input: map[string]interface{}{
-				"stage": "StageAlpha",
-			},
-			expectedErr: errors.New("missing required field: `id`"),
-		},
-		{
-			name: "invalid id",
-			input: map[string]interface{}{
-				"id":    "invalid@id",
-				"stage": "StageAlpha",
-			},
-			expectedErr: errors.New("invalid character(s) in ID"),
-		},
-		{
-			name: "missing stage",
-			input: map[string]interface{}{
-				"id": "valid.id",
-			},
-			expectedErr: errors.New("missing required field: `stage`"),
-		},
-		{
-			name: "invalid stage",
-			input: map[string]interface{}{
-				"id":    "valid.id",
-				"stage": "InvalidStage",
-			},
-			expectedErr: errors.New("invalid stage"),
-		},
-		{
-			name: "invalid from_version",
-			input: map[string]interface{}{
-				"id":           "valid.id",
-				"stage":        "StageAlpha",
-				"from_version": "v1.2.a",
-			},
-			expectedErr: errors.New("invalid character(s) in from_version"),
-		},
-		{
-			name: "invalid reference_url",
-			input: map[string]interface{}{
-				"id":            "valid.id",
-				"stage":         "StageAlpha",
-				"reference_url": "invalid-url",
-			},
-			expectedErr: errors.New("invalid character(s) in reference_url"),
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			parser := confmap.NewFromStringMap(tt.input)
-			featureGate := &featureGate{}
-			err := featureGate.validate(parser)
-			if tt.expectedErr != nil {
-				require.Error(t, err)
-				assert.Contains(t, err.Error(), tt.expectedErr.Error())
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
 func strPtr(s string) *string {
 	return &s
 }
diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go
index 8a9fdba0d71..9831c540ac6 100644
--- a/cmd/mdatagen/internal/metadata.go
+++ b/cmd/mdatagen/internal/metadata.go
@@ -9,6 +9,7 @@ import (
 	"regexp"
 	"strings"
 
+	"go.opentelemetry.io/collector/confmap"
 	"go.opentelemetry.io/collector/filter"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 )
@@ -40,6 +41,8 @@ type Metadata struct {
 	ShortFolderName string `mapstructure:"-"`
 	// Tests is the set of tests generated with the component
 	Tests Tests `mapstructure:"tests"`
+	// FeatureGates that can be used for the component.
+	FeatureGates map[featureGateName]featureGate `mapstructure:"feature_gates"`
 }
 
 func (md *Metadata) Validate() error {
@@ -156,6 +159,70 @@ func validateMetrics(metrics map[MetricName]Metric, attributes map[AttributeName
 	return errs
 }
 
+var (
+	// idRegexp is used to validate the ID of a Gate.
+	// IDs' characters must be alphanumeric or dots.
+	idRegexp           = regexp.MustCompile(`^[0-9a-zA-Z\.]*$`)
+	versionRegexp      = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
+	referenceURLRegexp = regexp.MustCompile(`^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(\/[^\s]*)?$`)
+	validStages        = map[string]bool{
+		"StageAlpha":      true,
+		"StageBeta":       true,
+		"StageStable":     true,
+		"StageDeprecated": true,
+	}
+)
+
+type featureGate struct {
+	// Required.
+	ID string `mapstructure:"id"`
+	// Description describes the purpose of the attribute.
+	Description string `mapstructure:"description"`
+	// Stage current stage at which the feature gate is in the development lifecyle
+	Stage string `mapstructure:"stage"`
+	// ReferenceURL can optionally give the url of the feature_gate
+	ReferenceURL string `mapstructure:"reference_url"`
+	// FromVersion optional field which gives the release version from which the gate has been given the current stage
+	FromVersion string `mapstructure:"from_version"`
+	// ToVersion optional field which gives the release version till which the gate the gate had the given lifecycle stage
+	ToVersion string `mapstructure:"to_version"`
+	// FeatureGateName name of the feature gate
+	FeatureGateName featureGateName `mapstructure:"-"`
+}
+
+func validateFeatureGate(parser *confmap.Conf) error {
+	var err []error
+	if !parser.IsSet("id") {
+		err = append(err, errors.New("missing required field: `id`"))
+	} else if !idRegexp.MatchString(fmt.Sprintf("%v", parser.Get("id"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in ID"))
+	}
+
+	if !parser.IsSet("stage") {
+		err = append(err, errors.New("missing required field: `stage`"))
+	} else if _, ok := validStages[fmt.Sprintf("%v", parser.Get("stage"))]; !ok {
+		err = append(err, fmt.Errorf("invalid stage"))
+	}
+
+	if parser.IsSet("from_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("from_version"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in from_version"))
+	}
+	if parser.IsSet("to_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("to_version"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in to_version"))
+	}
+	if parser.IsSet("reference_url") && !referenceURLRegexp.MatchString(fmt.Sprintf("%v", parser.Get("reference_url"))) {
+		err = append(err, fmt.Errorf("invalid character(s) in reference_url"))
+	}
+	return errors.Join(err...)
+}
+
+func (f *featureGate) Unmarshal(parser *confmap.Conf) error {
+	if err := validateFeatureGate(parser); err != nil {
+		return err
+	}
+	return parser.Unmarshal(f)
+}
+
 type AttributeName string
 
 func (mn AttributeName) Render() (string, error) {
@@ -166,6 +233,16 @@ func (mn AttributeName) RenderUnexported() (string, error) {
 	return FormatIdentifier(string(mn), false)
 }
 
+type featureGateName string
+
+func (mn featureGateName) Render() (string, error) {
+	return FormatIdentifier(string(mn), true)
+}
+
+func (mn featureGateName) RenderUnexported() (string, error) {
+	return FormatIdentifier(string(mn), false)
+}
+
 // ValueType defines an attribute value type.
 type ValueType struct {
 	// ValueType is type of the attribute value.
diff --git a/cmd/mdatagen/internal/metadata_test.go b/cmd/mdatagen/internal/metadata_test.go
index 83b102ce315..a6dcac99d70 100644
--- a/cmd/mdatagen/internal/metadata_test.go
+++ b/cmd/mdatagen/internal/metadata_test.go
@@ -10,6 +10,8 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"go.opentelemetry.io/collector/confmap"
 )
 
 func TestValidate(t *testing.T) {
@@ -142,6 +144,72 @@ func TestValidateMetricDuplicates(t *testing.T) {
 	}
 }
 
+func TestFeatureGateValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   map[string]interface{}
+		wantErr string
+	}{
+		{
+			name: "missing id",
+			input: map[string]interface{}{
+				"stage": "StageAlpha",
+			},
+			wantErr: "missing required field: `id`",
+		},
+		{
+			name: "invalid id",
+			input: map[string]interface{}{
+				"id":    "invalid@id",
+				"stage": "StageAlpha",
+			},
+			wantErr: "invalid character(s) in ID",
+		},
+		{
+			name: "missing stage",
+			input: map[string]interface{}{
+				"id": "valid.id",
+			},
+			wantErr: "missing required field: `stage`",
+		},
+		{
+			name: "invalid stage",
+			input: map[string]interface{}{
+				"id":    "valid.id",
+				"stage": "InvalidStage",
+			},
+			wantErr: "invalid stage",
+		},
+		{
+			name: "invalid from_version",
+			input: map[string]interface{}{
+				"id":           "valid.id",
+				"stage":        "StageAlpha",
+				"from_version": "v1.2.a",
+			},
+			wantErr: "invalid character(s) in from_version",
+		},
+		{
+			name: "invalid reference_url",
+			input: map[string]interface{}{
+				"id":            "valid.id",
+				"stage":         "StageAlpha",
+				"reference_url": "invalid-url",
+			},
+			wantErr: "invalid character(s) in reference_url",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			parser := confmap.NewFromStringMap(tt.input)
+			err := validateFeatureGate(parser)
+			require.Error(t, err)
+			require.EqualError(t, err, tt.wantErr)
+		})
+	}
+}
+
 func contains(r string, rs []string) bool {
 	for _, s := range rs {
 		if s == r {
diff --git a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
index 448cc2c64b4..861a86c003d 100644
--- a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
+++ b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml
@@ -72,16 +72,15 @@ feature_gates:
     id: useOTTLBridgeGate
     stage: StageAlpha
     description: When enabled, filterlog will convert filterlog configuration to OTTL and use filterottl evaluation
-    from_version: 1.0.0
+    from_version: v1.0.0
     reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18642
-    to_version: 2.0.0
-
+    to_version: v2.0.0
 
   allowFileDeletion:
     id: allowFileDeletionGate
     stage: StageStable
     description: When enabled, allows usage of the `delete_after_read` setting.
-    from_version": 1.5.0
+    from_version: v1.5.0
     reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/16314
 
   allowFileCreation:
diff --git a/cmd/mdatagen/internal/templates/feature_gates.go.tmpl b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
index e6d871e5c1c..5bce7aafb26 100644
--- a/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
+++ b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
@@ -7,15 +7,15 @@ import (
 )
 
 func init() {
-{{- range .FeatureGates }}
+{{- range $name, $fg := .FeatureGates }}
     {
         var opts []featuregate.RegisterOption
         opts = append(opts
-            {{- if .from_version }}, featuregate.WithRegisterFromVersion("{{.from_version}}") {{- end }}
-            {{- if .description }}, featuregate.WithRegisterDescription("{{.description}}") {{- end }}
-            {{- if .reference_url }}, featuregate.WithRegisterReferenceURL("{{.reference_url}}") {{- end }}
-            {{- if .to_version }}, featuregate.WithRegisterToVersion("{{.to_version}}") {{- end }})
-        _ = featuregate.GlobalRegistry().MustRegister("{{.id}}", featuregate.{{.stage}}, opts...)
+            {{- if $fg.FromVersion }}, featuregate.WithRegisterFromVersion("{{ $fg.FromVersion }}") {{- end }}
+            {{- if $fg.Description }}, featuregate.WithRegisterDescription("{{ $fg.Description }}") {{- end }}
+            {{- if $fg.ReferenceURL }}, featuregate.WithRegisterReferenceURL("{{ $fg.ReferenceURL }}") {{- end }}
+            {{- if $fg.ToVersion }}, featuregate.WithRegisterToVersion("{{ $fg.ToVersion }}") {{- end }})
+        _ = featuregate.GlobalRegistry().MustRegister("{{ $fg.ID }}", featuregate.{{ $fg.Stage }}, opts...)
     }
 {{- end }}
 

From 43268db92de7701bbadf1474101a47684e2e8ae7 Mon Sep 17 00:00:00 2001
From: Narcis Gemene <7252787+narcis96@users.noreply.github.com>
Date: Wed, 22 Jan 2025 14:27:27 +0200
Subject: [PATCH 4/4] Update embedded_templates_test.go

---
 cmd/mdatagen/internal/embedded_templates_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/mdatagen/internal/embedded_templates_test.go b/cmd/mdatagen/internal/embedded_templates_test.go
index c266c59f3f1..7c6f1e4d34b 100644
--- a/cmd/mdatagen/internal/embedded_templates_test.go
+++ b/cmd/mdatagen/internal/embedded_templates_test.go
@@ -23,7 +23,7 @@ func TestEnsureTemplatesLoaded(t *testing.T) {
 		templateFiles = map[string]struct{}{
 			path.Join(rootDir, "component_test.go.tmpl"):       {},
 			path.Join(rootDir, "documentation.md.tmpl"):        {},
-      path.Join(rootDir, "feature_gates.go.tmpl"): {},      
+      			path.Join(rootDir, "feature_gates.go.tmpl"): {},      
 			path.Join(rootDir, "metrics.go.tmpl"):              {},
 			path.Join(rootDir, "metrics_test.go.tmpl"):         {},
 			path.Join(rootDir, "resource.go.tmpl"):             {},