diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go index c4f2c0098d5..a8b685f27e6 100644 --- a/cmd/mdatagen/internal/command.go +++ b/cmd/mdatagen/internal/command.go @@ -128,6 +128,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 diff --git a/cmd/mdatagen/internal/embedded_templates_test.go b/cmd/mdatagen/internal/embedded_templates_test.go index a0af9594172..7c6f1e4d34b 100644 --- a/cmd/mdatagen/internal/embedded_templates_test.go +++ b/cmd/mdatagen/internal/embedded_templates_test.go @@ -23,6 +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, "metrics.go.tmpl"): {}, path.Join(rootDir, "metrics_test.go.tmpl"): {}, path.Join(rootDir, "resource.go.tmpl"): {}, diff --git a/cmd/mdatagen/internal/metadata.go b/cmd/mdatagen/internal/metadata.go index 22b42ff6e0a..dab805e33ee 100644 --- a/cmd/mdatagen/internal/metadata.go +++ b/cmd/mdatagen/internal/metadata.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/filter" "go.opentelemetry.io/collector/pdata/pcommon" ) @@ -41,6 +42,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 { @@ -157,6 +160,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) { @@ -167,6 +234,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 521bd62c452..861a86c003d 100644 --- a/cmd/mdatagen/internal/sampleprocessor/metadata.yaml +++ b/cmd/mdatagen/internal/sampleprocessor/metadata.yaml @@ -66,3 +66,23 @@ 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: v1.0.0 + reference_url: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18642 + to_version: v2.0.0 + + allowFileDeletion: + id: allowFileDeletionGate + stage: StageStable + description: When enabled, allows usage of the `delete_after_read` setting. + from_version: v1.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..5bce7aafb26 --- /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 $name, $fg := .FeatureGates }} + { + var opts []featuregate.RegisterOption + opts = append(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 }} + +} \ No newline at end of file diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 375c5b0fbcf..7f97abb5b0a 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: 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: + : + #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