Skip to content

Commit 0755152

Browse files
authored
feat(mdatagen): add automatic feature gate documentation generation (#14130)
#### Description Implements automatic documentation generation for component feature gates via mdatagen. Component authors can now declare feature gates in `metadata.yaml`, and mdatagen will automatically generate standardized markdown documentation including gate IDs, stages, descriptions, version information, and reference links. **Key Changes:** - Extended `metadata-schema.yaml` with `feature_gates` section - Added `FeatureGate` types with validation (required fields, valid stages, version format) - Created documentation template rendering feature gates as markdown table - Integrated into existing documentation generation pipeline - Added test coverage and usage documentation #### Link to tracking issue ~~Fixes~~ (edit by @mx-psi) Updates #14067 #### Testing - All 282 tests passing - Added `TestRunContents/feature_gates.yaml` test case - Validated documentation generation and error handling - Lint checks passing #### Documentation - Added "Feature Gates Documentation" section to `cmd/mdatagen/README.md` with usage examples - Generated example visible in `cmd/mdatagen/internal/testdata/documentation.md` --------- Signed-off-by: SACHIN <[email protected]> Signed-off-by: SACHIN KUMAR <[email protected]> Signed-off-by: sAchin-680 <[email protected]>
1 parent a162c7f commit 0755152

File tree

12 files changed

+440
-8
lines changed

12 files changed

+440
-8
lines changed

.github/workflows/utils/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@
314314
"multiclient",
315315
"multimod",
316316
"mycert",
317+
"mycomponent",
317318
"myconnector",
318319
"myexporter",
319320
"myextension",

cmd/mdatagen/README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Every component's documentation should include a brief description of the compon
1414
There is also some information about the component (or metadata) that should be included to help end-users understand the current state of the component and whether it is right for their use case.
1515
Examples of this metadata about a component are:
1616

17-
* its stability level
18-
* the distributions containing it
19-
* the types of pipelines it supports
20-
* metrics emitted in the case of a scraping receiver, a scraper, or a connector
17+
- its stability level
18+
- the distributions containing it
19+
- the types of pipelines it supports
20+
- metrics emitted in the case of a scraping receiver, a scraper, or a connector
2121

2222
The metadata generator defines a schema for specifying this information to ensure it is complete and well-formed.
2323
The metadata generator is then able to ingest the metadata, validate it against the schema and produce documentation in a standardized format.
@@ -26,10 +26,12 @@ An example of how this generated documentation looks can be found in [documentat
2626
## Using the Metadata Generator
2727

2828
In order for a component to benefit from the metadata generator (`mdatagen`) these requirements need to be met:
29+
2930
1. A yaml file containing the metadata that needs to be included in the component
3031
2. The component should declare a `go:generate mdatagen` directive which tells `mdatagen` what to generate
3132

3233
As an example, here is a minimal `metadata.yaml` for the [OTLP receiver](https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver/otlpreceiver):
34+
3335
```yaml
3436
type: otlp
3537
status:
@@ -42,6 +44,7 @@ status:
4244
Detailed information about the schema of `metadata.yaml` can be found in [metadata-schema.yaml](./metadata-schema.yaml).
4345

4446
The `go:generate mdatagen` directive is usually defined in a `doc.go` file in the same package as the component, for example:
47+
4548
```go
4649
//go:generate mdatagen metadata.yaml
4750
@@ -50,11 +53,48 @@ package main
5053

5154
Below are some more examples that can be used for reference:
5255

53-
* The ElasticSearch receiver has an extensive [metadata.yaml](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/elasticsearchreceiver/metadata.yaml)
54-
* The host metrics receiver has internal subcomponents, each with their own `metadata.yaml` and `doc.go`. See [cpuscraper](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/hostmetricsreceiver/internal/scraper/cpuscraper) for example.
56+
- The ElasticSearch receiver has an extensive [metadata.yaml](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/elasticsearchreceiver/metadata.yaml)
57+
- The host metrics receiver has internal subcomponents, each with their own `metadata.yaml` and `doc.go`. See [cpuscraper](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/hostmetricsreceiver/internal/scraper/cpuscraper) for example.
5558

5659
You can run `cd cmd/mdatagen && $(GOCMD) install .` to install the `mdatagen` tool in `GOBIN` and then run `mdatagen metadata.yaml` to generate documentation for a specific component or you can run `make generate` to generate documentation for all components.
5760

61+
### Feature Gates Documentation
62+
63+
The metadata generator supports automatic documentation generation for feature gates used by components. Feature gates are documented by adding a `feature_gates` section to your `metadata.yaml`:
64+
65+
```yaml
66+
type: mycomponent
67+
status:
68+
class: receiver
69+
stability:
70+
beta: [metrics, traces]
71+
72+
feature_gates:
73+
- id: mycomponent.newFeature
74+
description: 'Enables new feature functionality that improves performance'
75+
stage: alpha
76+
from_version: 'v0.100.0'
77+
reference_url: 'https://github.com/open-telemetry/opentelemetry-collector/issues/12345'
78+
79+
- id: mycomponent.stableFeature
80+
description: 'A feature that has reached stability'
81+
stage: stable
82+
from_version: 'v0.90.0'
83+
to_version: 'v0.95.0'
84+
reference_url: 'https://github.com/open-telemetry/opentelemetry-collector/issues/11111'
85+
```
86+
87+
This will generate a "Feature Gates" section in the component's `documentation.md` file with a table containing:
88+
89+
- **Feature Gate**: The gate identifier
90+
- **Stage**: The lifecycle stage (alpha, beta, stable, deprecated)
91+
- **Description**: Brief description of what the gate controls
92+
- **From Version**: Version when the gate was introduced
93+
- **To Version**: Version when stable/deprecated gates will be removed (if applicable)
94+
- **Reference**: Link to additional contextual information
95+
96+
The feature gate definitions should correspond to actual gates registered in your component code using the [Feature Gates API](../../featuregate/README.md).
97+
5898
### Generate multiple metadata packages
5999

60100
By default, `mdatagen` will generate a package called `metadata` in the `internal` directory. If you want to generate a package with a different name, you can use the `generated_package_name` configuration field to provide an alternate name.

cmd/mdatagen/internal/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func run(ymlPath string) error {
172172
}
173173
}
174174

175-
if len(md.Metrics) != 0 || len(md.Telemetry.Metrics) != 0 || len(md.ResourceAttributes) != 0 || len(md.Events) != 0 { // if there's metrics or internal metrics or events, generate documentation for them
175+
if len(md.Metrics) != 0 || len(md.Telemetry.Metrics) != 0 || len(md.ResourceAttributes) != 0 || len(md.Events) != 0 || len(md.FeatureGates) != 0 { // if there's metrics or internal metrics or events or feature gates, generate documentation for them
176176
toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md")
177177
}
178178

cmd/mdatagen/internal/command_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func TestRunContents(t *testing.T) {
4747
wantGoleakSkip bool
4848
wantGoleakSetup bool
4949
wantGoleakTeardown bool
50+
wantFeatureGatesGenerated bool
5051
wantErr bool
5152
wantOrderErr bool
5253
wantAttributes []string
@@ -192,6 +193,13 @@ func TestRunContents(t *testing.T) {
192193
wantComponentTestGenerated: true,
193194
wantLogsGenerated: true,
194195
},
196+
{
197+
yml: "feature_gates.yaml",
198+
wantStatusGenerated: true,
199+
wantReadmeGenerated: true,
200+
wantComponentTestGenerated: true,
201+
wantFeatureGatesGenerated: true,
202+
},
195203
{
196204
yml: "with_conditional_attribute.yaml",
197205
wantStatusGenerated: true,
@@ -246,6 +254,9 @@ foo
246254
}
247255
require.NoError(t, err)
248256

257+
// Documentation is generated when any of these features are present
258+
wantDocumentationGenerated := tt.wantFeatureGatesGenerated || tt.wantMetricsGenerated || tt.wantTelemetryGenerated || tt.wantResourceAttributesGenerated || tt.wantEventsGenerated
259+
249260
var contents []byte
250261
if tt.wantMetricsGenerated {
251262
require.FileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_metrics.go"))
@@ -301,7 +312,9 @@ foo
301312
require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry.go"))
302313
}
303314

304-
if !tt.wantMetricsGenerated && !tt.wantTelemetryGenerated && !tt.wantResourceAttributesGenerated && !tt.wantEventsGenerated {
315+
if wantDocumentationGenerated {
316+
require.FileExists(t, filepath.Join(tmpdir, "documentation.md"))
317+
} else {
305318
require.NoFileExists(t, filepath.Join(tmpdir, "documentation.md"))
306319
}
307320

cmd/mdatagen/internal/embedded_templates_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func TestEnsureTemplatesLoaded(t *testing.T) {
4040
path.Join(rootDir, "telemetrytest.go.tmpl"): {},
4141
path.Join(rootDir, "telemetrytest_test.go.tmpl"): {},
4242
path.Join(rootDir, "helper.tmpl"): {},
43+
path.Join(rootDir, "feature_gates.md.tmpl"): {},
4344
}
4445
count = 0
4546
)

cmd/mdatagen/internal/metadata.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ type Metadata struct {
5454
Tests Tests `mapstructure:"tests"`
5555
// PackageName is the name of the package where the component is defined.
5656
PackageName string `mapstructure:"package_name"`
57+
// FeatureGates that are managed by the component.
58+
FeatureGates []FeatureGate `mapstructure:"feature_gates"`
5759
}
5860

5961
func (md Metadata) GetCodeCovComponentID() string {
@@ -95,6 +97,10 @@ func (md *Metadata) Validate() error {
9597
errs = errors.Join(errs, err)
9698
}
9799

100+
if err := md.validateFeatureGates(); err != nil {
101+
errs = errors.Join(errs, err)
102+
}
103+
98104
return errs
99105
}
100106

@@ -276,6 +282,76 @@ func validateEvents(events map[EventName]Event, attributes map[AttributeName]Att
276282
return errs
277283
}
278284

285+
func (md *Metadata) validateFeatureGates() error {
286+
var errs error
287+
seen := make(map[FeatureGateID]bool)
288+
idRegexp := regexp.MustCompile(`^[0-9a-zA-Z.]*$`)
289+
290+
// Validate that feature gates are sorted by ID
291+
if !slices.IsSortedFunc(md.FeatureGates, func(a, b FeatureGate) int {
292+
return strings.Compare(string(a.ID), string(b.ID))
293+
}) {
294+
errs = errors.Join(errs, errors.New("feature gates must be sorted by ID"))
295+
}
296+
297+
for i, gate := range md.FeatureGates {
298+
// Validate gate ID is not empty
299+
if string(gate.ID) == "" {
300+
errs = errors.Join(errs, fmt.Errorf("feature gate at index %d: ID cannot be empty", i))
301+
continue
302+
}
303+
304+
// Validate ID follows the allowed character pattern
305+
if !idRegexp.MatchString(string(gate.ID)) {
306+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": ID contains invalid characters, must match ^[0-9a-zA-Z.]*$`, gate.ID))
307+
}
308+
309+
// Check for duplicate IDs
310+
if seen[gate.ID] {
311+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": duplicate ID`, gate.ID))
312+
continue
313+
}
314+
seen[gate.ID] = true
315+
316+
// Validate gate has required fields
317+
if gate.Description == "" {
318+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": description is required`, gate.ID))
319+
}
320+
321+
// Validate that each feature gate has a reference link
322+
if gate.ReferenceURL == "" {
323+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": reference_url is required`, gate.ID))
324+
}
325+
326+
// Validate stage is one of the allowed values
327+
validStages := map[FeatureGateStage]bool{
328+
FeatureGateStageAlpha: true,
329+
FeatureGateStageBeta: true,
330+
FeatureGateStageStable: true,
331+
FeatureGateStageDeprecated: true,
332+
}
333+
if !validStages[gate.Stage] {
334+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": invalid stage "%v", must be one of: alpha, beta, stable, deprecated`, gate.ID, gate.Stage))
335+
}
336+
337+
// Validate from_version is required
338+
if gate.FromVersion == "" {
339+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": from_version is required`, gate.ID))
340+
} else if !strings.HasPrefix(gate.FromVersion, "v") {
341+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": from_version "%v" must start with 'v'`, gate.ID, gate.FromVersion))
342+
}
343+
if gate.ToVersion != "" && !strings.HasPrefix(gate.ToVersion, "v") {
344+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": to_version "%v" must start with 'v'`, gate.ID, gate.ToVersion))
345+
}
346+
347+
// Validate that stable/deprecated gates should have to_version
348+
if (gate.Stage == FeatureGateStageStable || gate.Stage == FeatureGateStageDeprecated) && gate.ToVersion == "" {
349+
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": to_version is required for %v stage gates`, gate.ID, gate.Stage))
350+
}
351+
}
352+
return errs
353+
}
354+
279355
type AttributeName string
280356

281357
// AttributeRequirementLevel defines the requirement level of an attribute.
@@ -563,3 +639,32 @@ type EntityAttributeRef struct {
563639
// Ref is the reference to a resource attribute.
564640
Ref AttributeName `mapstructure:"ref"`
565641
}
642+
643+
// FeatureGateID represents the identifier for a feature gate.
644+
type FeatureGateID string
645+
646+
// FeatureGateStage represents the lifecycle stage of a feature gate.
647+
type FeatureGateStage string
648+
649+
const (
650+
FeatureGateStageAlpha FeatureGateStage = "alpha"
651+
FeatureGateStageBeta FeatureGateStage = "beta"
652+
FeatureGateStageStable FeatureGateStage = "stable"
653+
FeatureGateStageDeprecated FeatureGateStage = "deprecated"
654+
)
655+
656+
// FeatureGate represents a feature gate definition in metadata.
657+
type FeatureGate struct {
658+
// ID is the unique identifier for the feature gate.
659+
ID FeatureGateID `mapstructure:"id"`
660+
// Description of the feature gate.
661+
Description string `mapstructure:"description"`
662+
// Stage is the lifecycle stage of the feature gate.
663+
Stage FeatureGateStage `mapstructure:"stage"`
664+
// FromVersion is the version when the feature gate was introduced.
665+
FromVersion string `mapstructure:"from_version"`
666+
// ToVersion is the version when the feature gate reached stable stage.
667+
ToVersion string `mapstructure:"to_version"`
668+
// ReferenceURL is the URL with contextual information about the feature gate.
669+
ReferenceURL string `mapstructure:"reference_url"`
670+
}

0 commit comments

Comments
 (0)