Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/utils/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
"multiclient",
"multimod",
"mycert",
"mycomponent",
"myconnector",
"myexporter",
"myextension",
Expand Down
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 @@ An example of how this generated documentation looks can be found in [documentat
## 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 @@ status:
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 @@ package main

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
status:
class: receiver
stability:
beta: [metrics, traces]

feature_gates:
- id: mycomponent.newFeature
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
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
104 changes: 104 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,74 @@ 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)
idRegexp := regexp.MustCompile(`^[0-9a-zA-Z.]*$`)

// Validate that feature gates are sorted by ID
if !slices.IsSortedFunc(md.FeatureGates, func(a, b FeatureGate) int {
return strings.Compare(string(a.ID), string(b.ID))
}) {
errs = errors.Join(errs, errors.New("feature gates must be sorted by ID"))
}

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
}

// Validate ID follows the allowed character pattern
if !idRegexp.MatchString(string(gate.ID)) {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": ID contains invalid characters, must match ^[0-9a-zA-Z.]*$`, gate.ID))
}

// 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 that each feature gate has a reference link
if gate.ReferenceURL == "" {
errs = errors.Join(errs, fmt.Errorf(`feature gate "%v": reference_url 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 @@ -521,3 +596,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