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
30 changes: 30 additions & 0 deletions .chloggen/ocb-component-schema-alternative.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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. receiver/otlp)
component: cmd/mdatagen

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add config JSON schema generation

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

# (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: |
The component config JSON schema can be optionally enabled by:
```
schema:
enabled: true
```

# 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: [api]
7 changes: 7 additions & 0 deletions cmd/mdatagen/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ go 1.24.0

require (
github.com/google/go-cmp v0.7.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/collector/component v1.49.0
go.opentelemetry.io/collector/component/componenttest v0.143.0
go.opentelemetry.io/collector/config/configopaque v1.49.0
go.opentelemetry.io/collector/config/configoptional v1.49.0
go.opentelemetry.io/collector/confmap v1.49.0
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.49.0
go.opentelemetry.io/collector/connector v0.143.0
Expand All @@ -32,6 +35,7 @@ require (
go.uber.org/goleak v1.3.0
go.uber.org/zap v1.27.1
golang.org/x/text v0.32.0
golang.org/x/tools v0.40.0
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -57,6 +61,7 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.143.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.143.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.143.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.143.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.143.0 // indirect
Expand All @@ -72,6 +77,8 @@ require (
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions cmd/mdatagen/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions cmd/mdatagen/internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"

"go.opentelemetry.io/collector/cmd/mdatagen/internal/schemagen"
)

const (
Expand Down Expand Up @@ -216,6 +218,11 @@ func run(ymlPath string) error {
}
}

// Generate JSON schema if enabled
if err := generateSchema(md, ymlDir); err != nil {
return fmt.Errorf("failed to generate schema: %w", err)
}

return nil
}

Expand Down Expand Up @@ -492,3 +499,60 @@ func validateYAMLKeyOrder(raw []byte) error {
}
return nil
}

// generateSchema generates a JSON schema for the component's config if enabled.
func generateSchema(md Metadata, ymlDir string) error {
// Skip if schema generation is not enabled
if md.Schema == nil || !md.Schema.Enabled {
return nil
}

// Skip non-component types
if md.Status == nil || slices.Contains(nonComponents, md.Status.Class) {
return nil
}

// Create schemas directory
outputDir := filepath.Join(ymlDir, "internal", md.GeneratedPackageName, "schemas")
if err := os.MkdirAll(outputDir, 0o700); err != nil {
return fmt.Errorf("failed to create schemas directory: %w", err)
}

// Parse config type specification
pkgPath, typeName := parseConfigType(md.Schema.ConfigType)

// Create analyzer and generator
analyzer := schemagen.NewPackageAnalyzer(ymlDir)
generator := schemagen.NewSchemaGenerator(outputDir, analyzer)

// Generate schema
return generator.GenerateSchema(md.Status.Class, md.Type, typeName, pkgPath)
}

// parseConfigType parses a config type specification into package path and type name.
// Examples:
// - "Config" -> ("", "Config")
// - "go.opentelemetry.io/collector/pkg.Config" -> ("go.opentelemetry.io/collector/pkg", "Config")
func parseConfigType(configType string) (pkgPath, typeName string) {
if configType == "" {
return "", ""
}

// Find the last dot that separates package path from type name
lastDot := strings.LastIndex(configType, ".")
if lastDot == -1 {
// No dot means it's just a type name in the local package
return "", configType
}

// Check if this looks like a package path (contains "/" before the last dot)
potentialPkg := configType[:lastDot]
if strings.Contains(potentialPkg, "/") {
// It's a fully qualified type: pkg/path.TypeName
return potentialPkg, configType[lastDot+1:]
}

// No slash means it's just a type name (e.g., "Config" or possibly "pkg.Config" for local)
// Treat as local type name
return "", configType
}
37 changes: 37 additions & 0 deletions cmd/mdatagen/internal/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,3 +820,40 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer {
})
}
}

func TestGenerateSchema_Skipped(t *testing.T) {
tests := []struct {
name string
md Metadata
}{
{
name: "schema is nil",
md: Metadata{
Type: "test",
Status: &Status{Class: "exporter"},
},
},
{
name: "explicitly disabled",
md: Metadata{
Type: "test",
Status: &Status{Class: "exporter"},
Schema: &SchemaConfig{Enabled: false},
},
},
{
name: "non-component type",
md: Metadata{
Type: "test",
Status: &Status{Class: "cmd"},
Schema: &SchemaConfig{Enabled: true},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := generateSchema(tt.md, t.TempDir())
require.NoError(t, err)
})
}
}
3 changes: 3 additions & 0 deletions cmd/mdatagen/internal/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func TestLoadMetadata(t *testing.T) {
SemConvVersion: "1.38.0",
PackageName: "go.opentelemetry.io/collector/cmd/mdatagen/internal/samplereceiver",
ReaggregationEnabled: true,
Schema: &SchemaConfig{
Enabled: true,
},
Status: &Status{
DisableCodeCov: true,
Class: "receiver",
Expand Down
2 changes: 2 additions & 0 deletions cmd/mdatagen/internal/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type Metadata struct {
Tests Tests `mapstructure:"tests"`
// PackageName is the name of the package where the component is defined.
PackageName string `mapstructure:"package_name"`
// Schema holds configuration for JSON schema generation.
Schema *SchemaConfig `mapstructure:"schema"`
}

func (md Metadata) GetCodeCovComponentID() string {
Expand Down
108 changes: 108 additions & 0 deletions cmd/mdatagen/internal/samplereceiver/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package samplereceiver // import "go.opentelemetry.io/collector/cmd/mdatagen/internal/samplereceiver"

import (
"time"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configopaque"
"go.opentelemetry.io/collector/config/configoptional"
)

var _ component.Factory = (*AnotherStruct)(nil)

type AnotherStruct struct{}

func (a AnotherStruct) Type() component.Type {
// TODO implement me
panic("implement me")
}

func (a AnotherStruct) CreateDefaultConfig() component.Config {
// TODO implement me
panic("implement me")
}

var _ component.Config = (*MyConfig)(nil)

type CustomString string

// NetworkConfig holds network configuration that should be squashed into parent.
type NetworkConfig struct {
// Host is the network host.
Host string `mapstructure:"host"`

// Port is the network port.
Port int `mapstructure:"port"`
}

// MyConfig defines configuration for the sample exporter used to test schema generation.
type MyConfig struct {
// Network is squashed into the parent config.
Network NetworkConfig `mapstructure:",squash"`

// ID is the component identifier.
ID component.ID `mapstructure:"id"`

// Endpoint is the target URL to send data to.
Endpoint string `mapstructure:"endpoint"`

// CustomString is a custom string.
CustomString CustomString `mapstructure:"custom_string"`

// Timeout is the maximum time to wait for a response.
Timeout time.Duration `mapstructure:"timeout"`

// StartTime is the time when the receiver should start collecting data.
StartTime time.Time `mapstructure:"start_time"`

// Enabled controls whether the exporter is active.
Enabled bool `mapstructure:"enabled"`

// BatchSize is the number of items to send in each batch.
BatchSize int `mapstructure:"batch_size"`

// Headers are additional headers to include in requests.
Headers map[string]string `mapstructure:"headers"`

// Retry contains retry configuration.
Retry RetryConfig `mapstructure:"retry"`

// Tags are optional tags to attach.
Tags []string `mapstructure:"tags"`

// APIKey is a secret API key (opaque string).
APIKey configopaque.String `mapstructure:"api_key"`

// OptionalRetry is an optional retry configuration.
OptionalRetry configoptional.Optional[RetryConfig] `mapstructure:"optional_retry"`

// Secrets is a list of secret key-value pairs.
Secrets configopaque.MapList `mapstructure:"secrets"`

// Endpoints is a list of endpoint configurations.
Endpoints []EndpointConfig `mapstructure:"endpoints"`

// InternalState is an internal field that should be excluded from the schema.
InternalState string `mapstructure:"-"`
}

// EndpointConfig holds configuration for a single endpoint.
type EndpointConfig struct {
// URL is the endpoint URL.
URL string `mapstructure:"url"`

// Priority is the endpoint priority.
Priority int `mapstructure:"priority"`
}

// RetryConfig holds retry settings.
type RetryConfig struct {
// MaxRetries is the maximum number of retries.
MaxRetries int `mapstructure:"max_retries"`

// InitialInterval is the initial retry interval.
InitialInterval time.Duration `mapstructure:"initial_interval"`
}
2 changes: 1 addition & 1 deletion cmd/mdatagen/internal/samplereceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func NewFactory() receiver.Factory {
return receiver.NewFactory(
metadata.Type,
func() component.Config { return &struct{}{} },
func() component.Config { return &MyConfig{} },
receiver.WithTraces(createTraces, metadata.TracesStability),
receiver.WithMetrics(createMetrics, metadata.MetricsStability),
receiver.WithLogs(createLogs, metadata.LogsStability))
Expand Down
Loading
Loading