Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .chloggen/mdatagen-featuregates-docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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: Add automatic documentation generation for component feature gates

# One or more tracking issues or pull requests related to the change
issues: [14067]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
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.

# 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]
64 changes: 53 additions & 11 deletions cmd/mdatagen/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
# Metadata Generator

<!-- status autogenerated section -->
| Status | |
| ------------- |-----------|
| Stability | [alpha]: metrics |
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector?query=is%3Aissue%20is%3Aopen%20label%3Acmd%2Fmdatagen%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector/issues?q=is%3Aopen+is%3Aissue+label%3Acmd%2Fmdatagen) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector?query=is%3Aissue%20is%3Aclosed%20label%3Acmd%2Fmdatagen%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector/issues?q=is%3Aclosed+is%3Aissue+label%3Acmd%2Fmdatagen) |
| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@dmitryax](https://www.github.com/dmitryax) |

| Status | |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stability | [alpha]: metrics |
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector?query=is%3Aissue%20is%3Aopen%20label%3Acmd%2Fmdatagen%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector/issues?q=is%3Aopen+is%3Aissue+label%3Acmd%2Fmdatagen) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector?query=is%3Aissue%20is%3Aclosed%20label%3Acmd%2Fmdatagen%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector/issues?q=is%3Aclosed+is%3Aissue+label%3Acmd%2Fmdatagen) |
| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@dmitryax](https://www.github.com/dmitryax) |

[alpha]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#alpha

<!-- end autogenerated section -->

Every component's documentation should include a brief description of the component and guidance on how to use it.
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.
Examples of this metadata about a component are:

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

The metadata generator defines a schema for specifying this information to ensure it is complete and well-formed.
The metadata generator is then able to ingest the metadata, validate it against the schema and produce documentation in a standardized format.
Expand All @@ -26,10 +28,12 @@
## Using the Metadata Generator

In order for a component to benefit from the metadata generator (`mdatagen`) these requirements need to be met:

1. A yaml file containing the metadata that needs to be included in the component
2. The component should declare a `go:generate mdatagen` directive which tells `mdatagen` what to generate

As an example, here is a minimal `metadata.yaml` for the [OTLP receiver](https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver/otlpreceiver):

```yaml
type: otlp
status:
Expand All @@ -42,6 +46,7 @@
Detailed information about the schema of `metadata.yaml` can be found in [metadata-schema.yaml](./metadata-schema.yaml).

The `go:generate mdatagen` directive is usually defined in a `doc.go` file in the same package as the component, for example:

```go
//go:generate mdatagen metadata.yaml

Expand All @@ -50,11 +55,48 @@

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

* The ElasticSearch receiver has an extensive [metadata.yaml](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/elasticsearchreceiver/metadata.yaml)
* 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.
- The ElasticSearch receiver has an extensive [metadata.yaml](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/elasticsearchreceiver/metadata.yaml)
- 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.

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.

### Feature Gates Documentation

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`:

```yaml
type: mycomponent

Check warning on line 68 in cmd/mdatagen/README.md

View workflow job for this annotation

GitHub Actions / spell-check

Unknown word (mycomponent)
status:
class: receiver
stability:
beta: [metrics, traces]

feature_gates:
- id: mycomponent.newFeature

Check warning on line 75 in cmd/mdatagen/README.md

View workflow job for this annotation

GitHub Actions / spell-check

Unknown word (mycomponent)
description: 'Enables new feature functionality that improves performance'
stage: alpha
from_version: 'v0.100.0'
reference_url: 'https://github.com/open-telemetry/opentelemetry-collector/issues/12345'

- id: mycomponent.stableFeature

Check warning on line 81 in cmd/mdatagen/README.md

View workflow job for this annotation

GitHub Actions / spell-check

Unknown word (mycomponent)
description: 'A feature that has reached stability'
stage: stable
from_version: 'v0.90.0'
to_version: 'v0.95.0'
reference_url: 'https://github.com/open-telemetry/opentelemetry-collector/issues/11111'
```

This will generate a "Feature Gates" section in the component's `documentation.md` file with a table containing:

- **Feature Gate**: The gate identifier
- **Stage**: The lifecycle stage (alpha, beta, stable, deprecated)
- **Description**: Brief description of what the gate controls
- **From Version**: Version when the gate was introduced
- **To Version**: Version when stable/deprecated gates will be removed (if applicable)
- **Reference**: Link to additional contextual information

The feature gate definitions should correspond to actual gates registered in your component code using the [Feature Gates API](../../featuregate/README.md).

### Generate multiple metadata packages

By default, `mdatagen` will generate a package called `metadata` in the `internal` directory. If you want to generate a package with a different name, you can use the `generated_package_name` configuration field to provide an alternate name.
Expand Down
2 changes: 1 addition & 1 deletion cmd/mdatagen/internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func run(ymlPath string) error {
}
}

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
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
toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md")
}

Expand Down
12 changes: 11 additions & 1 deletion cmd/mdatagen/internal/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestRunContents(t *testing.T) {
wantGoleakSkip bool
wantGoleakSetup bool
wantGoleakTeardown bool
wantDocumentationGenerated bool
wantErr bool
wantOrderErr bool
wantAttributes []string
Expand Down Expand Up @@ -192,6 +193,13 @@ func TestRunContents(t *testing.T) {
wantComponentTestGenerated: true,
wantLogsGenerated: true,
},
{
yml: "feature_gates.yaml",
wantStatusGenerated: true,
wantReadmeGenerated: true,
wantComponentTestGenerated: true,
wantDocumentationGenerated: true,
},
{
yml: "with_conditional_attribute.yaml",
wantStatusGenerated: true,
Expand Down Expand Up @@ -301,7 +309,9 @@ foo
require.NoFileExists(t, filepath.Join(tmpdir, generatedPackageDir, "generated_telemetry.go"))
}

if !tt.wantMetricsGenerated && !tt.wantTelemetryGenerated && !tt.wantResourceAttributesGenerated && !tt.wantEventsGenerated {
if tt.wantDocumentationGenerated {
require.FileExists(t, filepath.Join(tmpdir, "documentation.md"))
} else if !tt.wantMetricsGenerated && !tt.wantTelemetryGenerated && !tt.wantResourceAttributesGenerated && !tt.wantEventsGenerated {
require.NoFileExists(t, filepath.Join(tmpdir, "documentation.md"))
}

Expand Down
1 change: 1 addition & 0 deletions cmd/mdatagen/internal/embedded_templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestEnsureTemplatesLoaded(t *testing.T) {
path.Join(rootDir, "telemetrytest.go.tmpl"): {},
path.Join(rootDir, "telemetrytest_test.go.tmpl"): {},
path.Join(rootDir, "helper.tmpl"): {},
path.Join(rootDir, "feature_gates.md.tmpl"): {},
}
count = 0
)
Expand Down
86 changes: 86 additions & 0 deletions cmd/mdatagen/internal/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type Metadata struct {
Tests Tests `mapstructure:"tests"`
// PackageName is the name of the package where the component is defined.
PackageName string `mapstructure:"package_name"`
// FeatureGates that are managed by the component.
FeatureGates []FeatureGate `mapstructure:"feature_gates"`
// FeatureGates that are managed by the component.
}

func (md Metadata) GetCodeCovComponentID() string {
Expand Down Expand Up @@ -89,6 +92,10 @@ func (md *Metadata) Validate() error {
errs = errors.Join(errs, err)
}

if err := md.validateFeatureGates(); err != nil {
errs = errors.Join(errs, err)
}

return errs
}

Expand Down Expand Up @@ -270,6 +277,56 @@ func validateEvents(events map[EventName]Event, attributes map[AttributeName]Att
return errs
}

func (md *Metadata) validateFeatureGates() error {
var errs error
seen := make(map[FeatureGateID]bool)

for i, gate := range md.FeatureGates {
// Validate gate ID is not empty
if string(gate.ID) == "" {
errs = errors.Join(errs, fmt.Errorf("feature gate at index %d: ID cannot be empty", i))
continue
}

// Check for duplicate IDs
if seen[gate.ID] {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": duplicate ID`, gate.ID))
continue
}
seen[gate.ID] = true

// Validate gate has required fields
if gate.Description == "" {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": description is required`, gate.ID))
}

// Validate stage is one of the allowed values
validStages := map[FeatureGateStage]bool{
FeatureGateStageAlpha: true,
FeatureGateStageBeta: true,
FeatureGateStageStable: true,
FeatureGateStageDeprecated: true,
}
if !validStages[gate.Stage] {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": invalid stage "%v", must be one of: alpha, beta, stable, deprecated`, gate.ID, gate.Stage))
}

// Validate version formats if provided
if gate.FromVersion != "" && !strings.HasPrefix(gate.FromVersion, "v") {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": from_version "%v" must start with 'v'`, gate.ID, gate.FromVersion))
}
if gate.ToVersion != "" && !strings.HasPrefix(gate.ToVersion, "v") {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": to_version "%v" must start with 'v'`, gate.ID, gate.ToVersion))
}

// Validate that stable/deprecated gates should have to_version
if (gate.Stage == FeatureGateStageStable || gate.Stage == FeatureGateStageDeprecated) && gate.ToVersion == "" {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": to_version is required for %v stage gates`, gate.ID, gate.Stage))
}
}
return errs
}

type AttributeName string

// AttributeRequirementLevel defines the requirement level of an attribute.
Expand Down Expand Up @@ -514,3 +571,32 @@ type EntityAttributeRef struct {
// Ref is the reference to a resource attribute.
Ref AttributeName `mapstructure:"ref"`
}

// FeatureGateID represents the identifier for a feature gate.
type FeatureGateID string

// FeatureGateStage represents the lifecycle stage of a feature gate.
type FeatureGateStage string

const (
FeatureGateStageAlpha FeatureGateStage = "alpha"
FeatureGateStageBeta FeatureGateStage = "beta"
FeatureGateStageStable FeatureGateStage = "stable"
FeatureGateStageDeprecated FeatureGateStage = "deprecated"
)

// FeatureGate represents a feature gate definition in metadata.
type FeatureGate struct {
// ID is the unique identifier for the feature gate.
ID FeatureGateID `mapstructure:"id"`
// Description of the feature gate.
Description string `mapstructure:"description"`
// Stage is the lifecycle stage of the feature gate.
Stage FeatureGateStage `mapstructure:"stage"`
// FromVersion is the version when the feature gate was introduced.
FromVersion string `mapstructure:"from_version"`
// ToVersion is the version when the feature gate reached stable stage.
ToVersion string `mapstructure:"to_version"`
// ReferenceURL is the URL with contextual information about the feature gate.
ReferenceURL string `mapstructure:"reference_url"`
}
Loading
Loading