Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .chloggen/configoptional-validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: configoptional

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Allow validating nested types

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

# (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: '`configoptional.Optional` now implements `xconfmap.Validator`'

# 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, api]
3 changes: 3 additions & 0 deletions config/configgrpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/confmap v1.38.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0 // indirect
go.opentelemetry.io/collector/featuregate v1.38.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.132.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.132.0 // indirect
Expand Down Expand Up @@ -119,3 +120,5 @@ replace go.opentelemetry.io/collector/featuregate => ../../featuregate
replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest

replace go.opentelemetry.io/collector/confmap => ../../confmap

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../confmap/xconfmap
1 change: 1 addition & 0 deletions config/confighttp/xconfighttp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
go.opentelemetry.io/collector/config/configoptional v0.132.0 // indirect
go.opentelemetry.io/collector/config/configtls v1.38.0 // indirect
go.opentelemetry.io/collector/confmap v1.38.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0 // indirect
go.opentelemetry.io/collector/extension/extensionauth v1.38.0 // indirect
go.opentelemetry.io/collector/extension/extensionmiddleware v0.132.0 // indirect
go.opentelemetry.io/collector/featuregate v1.38.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions config/configoptional/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.24
require (
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/collector/confmap v1.38.0
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0
go.uber.org/goleak v1.3.0
)

Expand All @@ -29,3 +30,5 @@ require (
replace go.opentelemetry.io/collector/confmap => ../../confmap

replace go.opentelemetry.io/collector/featuregate => ../../featuregate

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../confmap/xconfmap
24 changes: 24 additions & 0 deletions config/configoptional/optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/xconfmap"
)

type flavor int
Expand Down Expand Up @@ -189,3 +190,26 @@ func (o Optional[T]) Marshal(conf *confmap.Conf) error {

return nil
}

var _ xconfmap.Validator = (*Optional[any])(nil)

// Validate implements [xconfmap.Validator]. This is required because the
// private fields in [xconfmap.Validator] can't be seen by the reflection used
// by [xconfmap.Validate], and therefore we have to continue the validation
// chain manually. This method isn't meant to be called directly, and should
// generally only be called by [xconfmap.Validate].
func (o *Optional[T]) Validate() error {
// When the flavor is None, the user has not passed this value,
// and therefore we should not validate it. The parent struct holding
// the Optional type can determine whether a None value is valid for
// a given config.
//
// If the flavor is still Default, then the user has not passed this
// value and we should also not validate it.
if o.flavor == noneFlavor || o.flavor == defaultFlavor {
return nil
}

// For the some flavor, validate the actual value.
return xconfmap.Validate(o.value)
}
123 changes: 123 additions & 0 deletions config/configoptional/optional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package configoptional

import (
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/confmaptest"
"go.opentelemetry.io/collector/confmap/xconfmap"
)

type Config[T any] struct {
Expand Down Expand Up @@ -460,3 +463,123 @@ func TestComparePointerMarshal(t *testing.T) {
})
}
}

type invalid struct{}

func (invalid) Validate() error {
return errors.New("invalid")
}

var _ xconfmap.Validator = invalid{}

type hasNested struct {
CouldBe Optional[invalid]
}

func TestOptionalValidate(t *testing.T) {
require.NoError(t, xconfmap.Validate(hasNested{
CouldBe: None[invalid](),
}))
require.NoError(t, xconfmap.Validate(hasNested{
CouldBe: Default(invalid{}),
}))
require.Error(t, xconfmap.Validate(hasNested{
CouldBe: Some(invalid{}),
}))
}

type validatedConfig struct {
Default Optional[optionalConfig] `mapstructure:"default"`
Some Optional[someConfig] `mapstructure:"some"`
}

var _ xconfmap.Validator = (*optionalConfig)(nil)

type optionalConfig struct {
StringVal string `mapstructure:"string_val"`
}

func (n optionalConfig) Validate() error {
if n.StringVal == "invalid" {
return errors.New("field `string_val` cannot be set to `invalid`")
}

return nil
}

type someConfig struct {
Nested Optional[optionalConfig] `mapstructure:"nested"`
}

func newDefaultValidatedConfig() validatedConfig {
return validatedConfig{
Default: Default(optionalConfig{StringVal: "valid"}),
}
}

func newInvalidDefaultConfig() validatedConfig {
return validatedConfig{
Default: Default(optionalConfig{StringVal: "invalid"}),
}
}

func TestOptionalFileValidate(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this test a bit confusing. The test struct is pretty convoluted and tests multiple things at once, so it's not easy to convince myself that the test is correct and covers all the cases we care about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I've been erring on the side of testing too many things for this PR, but if it weakens the signal that we've covered the cases we want, I'll pare it down some or at least explain why we're doing what we are.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reduced the struct size and made the test names more descriptive. The only excess now is the nested optional struct, which I left in there just to make sure we're properly calling xconfmap.Validate. Let me know if it looks better.

Copy link
Contributor

@jade-guiton-dd jade-guiton-dd Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot simpler to understand, thank you.

Maybe it would be worth it to add a test case for "invalid default + explicit", just in case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, done.

cases := []struct {
name string
variant string
cfg func() validatedConfig
err error
}{
{
name: "valid default with just key set and no subfields",
variant: "implicit",
cfg: newDefaultValidatedConfig,
},
{
name: "valid default with keys set in default",
variant: "explicit",
cfg: newDefaultValidatedConfig,
},
{
name: "invalid config",
variant: "invalid",
cfg: newDefaultValidatedConfig,
err: errors.New("default: field `string_val` cannot be set to `invalid`\nsome: nested: field `string_val` cannot be set to `invalid`"),
},
{
name: "invalid default throws an error",
variant: "implicit",
cfg: newInvalidDefaultConfig,
err: errors.New("default: field `string_val` cannot be set to `invalid`"),
},
{
name: "invalid default does not throw an error when key is not set",
variant: "no_default",
cfg: newInvalidDefaultConfig,
},
{
name: "invalid default invalid default does not throw an error when the value is overridden",
variant: "explicit",
cfg: newInvalidDefaultConfig,
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
conf, err := confmaptest.LoadConf(fmt.Sprintf("testdata/validate_%s.yaml", tt.variant))
require.NoError(t, err)

cfg := tt.cfg()

err = conf.Unmarshal(&cfg)
require.NoError(t, err)

err = xconfmap.Validate(cfg)
if tt.err == nil {
require.NoError(t, err)
} else {
require.EqualError(t, err, tt.err.Error())
}
})
}
}
5 changes: 5 additions & 0 deletions config/configoptional/testdata/validate_explicit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
default:
string_val: valid
some:
nested:
string_val: valid
4 changes: 4 additions & 0 deletions config/configoptional/testdata/validate_implicit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
default:
some:
nested:
string_val: value1
5 changes: 5 additions & 0 deletions config/configoptional/testdata/validate_invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
default:
string_val: invalid
some:
nested:
string_val: invalid
3 changes: 3 additions & 0 deletions config/configoptional/testdata/validate_no_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
some:
nested:
string_val: value1
3 changes: 3 additions & 0 deletions exporter/debugexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
go.opentelemetry.io/collector/client v1.38.0 // indirect
go.opentelemetry.io/collector/config/configoptional v0.132.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.38.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.132.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.132.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.132.0 // indirect
Expand Down Expand Up @@ -134,3 +135,5 @@ replace go.opentelemetry.io/collector/client => ../../client
replace go.opentelemetry.io/collector/pdata/xpdata => ../../pdata/xpdata

replace go.opentelemetry.io/collector/config/configoptional => ../../config/configoptional

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../confmap/xconfmap
37 changes: 23 additions & 14 deletions exporter/exporterhelper/internal/queuebatch/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,67 +11,76 @@ import (
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap/xconfmap"
"go.opentelemetry.io/collector/exporter/exporterhelper/internal/request"
)

func TestConfig_Validate(t *testing.T) {
cfg := newTestConfig()
require.NoError(t, cfg.Validate())
require.NoError(t, xconfmap.Validate(cfg))

cfg.NumConsumers = 0
require.EqualError(t, cfg.Validate(), "`num_consumers` must be positive")
require.EqualError(t, xconfmap.Validate(cfg), "`num_consumers` must be positive")

cfg = newTestConfig()
cfg.QueueSize = 0
require.EqualError(t, cfg.Validate(), "`queue_size` must be positive")
require.EqualError(t, xconfmap.Validate(cfg), "`queue_size` must be positive")

cfg = newTestConfig()
cfg.QueueSize = 0
require.EqualError(t, cfg.Validate(), "`queue_size` must be positive")
require.EqualError(t, xconfmap.Validate(cfg), "`queue_size` must be positive")

storageID := component.MustNewID("test")
cfg = newTestConfig()
cfg.WaitForResult = true
cfg.StorageID = &storageID
require.EqualError(t, cfg.Validate(), "`wait_for_result` is not supported with a persistent queue configured with `storage`")
require.EqualError(t, xconfmap.Validate(cfg), "`wait_for_result` is not supported with a persistent queue configured with `storage`")

cfg = newTestConfig()
cfg.QueueSize = cfg.Batch.Get().MinSize - 1
require.EqualError(t, cfg.Validate(), "`min_size` must be less than or equal to `queue_size`")
require.EqualError(t, xconfmap.Validate(cfg), "`min_size` must be less than or equal to `queue_size`")

cfg = newTestConfig()
cfg.Batch.Get().Sizer = request.SizerType{}
require.EqualError(t, xconfmap.Validate(cfg), "batch: `batch` supports only `items` or `bytes` sizer")

cfg = newTestConfig()
cfg.Sizer = request.SizerTypeBytes
require.NoError(t, cfg.Validate())
require.NoError(t, xconfmap.Validate(cfg))

// Confirm Validate doesn't return error with invalid config when feature is disabled
cfg.Enabled = false
assert.NoError(t, cfg.Validate())
assert.NoError(t, xconfmap.Validate(cfg))
}

func TestBatchConfig_Validate(t *testing.T) {
cfg := newTestBatchConfig()
require.NoError(t, cfg.Validate())
require.NoError(t, xconfmap.Validate(cfg))

cfg = newTestBatchConfig()
cfg.FlushTimeout = 0
require.EqualError(t, cfg.Validate(), "`flush_timeout` must be positive")
require.EqualError(t, xconfmap.Validate(cfg), "`flush_timeout` must be positive")

cfg = newTestBatchConfig()
cfg.MinSize = -1
require.EqualError(t, cfg.Validate(), "`min_size` must be non-negative")
require.EqualError(t, xconfmap.Validate(cfg), "`min_size` must be non-negative")

cfg = newTestBatchConfig()
cfg.MaxSize = -1
require.EqualError(t, cfg.Validate(), "`max_size` must be non-negative")
require.EqualError(t, xconfmap.Validate(cfg), "`max_size` must be non-negative")

cfg = newTestBatchConfig()
cfg.Sizer = request.SizerTypeRequests
require.EqualError(t, cfg.Validate(), "`batch` supports only `items` or `bytes` sizer")
require.EqualError(t, xconfmap.Validate(cfg), "`batch` supports only `items` or `bytes` sizer")

cfg = newTestBatchConfig()
cfg.Sizer = request.SizerType{}
require.EqualError(t, xconfmap.Validate(cfg), "`batch` supports only `items` or `bytes` sizer")
Comment on lines +77 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀


cfg = newTestBatchConfig()
cfg.MinSize = 2048
cfg.MaxSize = 1024
require.EqualError(t, cfg.Validate(), "`max_size` must be greater or equal to `min_size`")
require.EqualError(t, xconfmap.Validate(cfg), "`max_size` must be greater or equal to `min_size`")
}

func newTestBatchConfig() BatchConfig {
Expand Down
3 changes: 3 additions & 0 deletions exporter/exporterhelper/xexporterhelper/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
go.opentelemetry.io/collector/config/configoptional v0.132.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.38.0 // indirect
go.opentelemetry.io/collector/confmap v1.38.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0 // indirect
go.opentelemetry.io/collector/extension v1.38.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.132.0 // indirect
go.opentelemetry.io/collector/featuregate v1.38.0 // indirect
Expand Down Expand Up @@ -127,3 +128,5 @@ replace go.opentelemetry.io/collector/pdata/xpdata => ../../../pdata/xpdata
replace go.opentelemetry.io/collector/config/configoptional => ../../../config/configoptional

replace go.opentelemetry.io/collector/confmap => ../../../confmap

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../../confmap/xconfmap
3 changes: 3 additions & 0 deletions exporter/exportertest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
go.opentelemetry.io/collector/client v1.38.0 // indirect
go.opentelemetry.io/collector/config/configoptional v0.132.0 // indirect
go.opentelemetry.io/collector/confmap v1.38.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.132.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.132.0 // indirect
go.opentelemetry.io/collector/extension v1.38.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.132.0 // indirect
Expand Down Expand Up @@ -117,3 +118,5 @@ replace go.opentelemetry.io/collector/pdata/xpdata => ../../pdata/xpdata
replace go.opentelemetry.io/collector/config/configoptional => ../../config/configoptional

replace go.opentelemetry.io/collector/confmap => ../../confmap

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../confmap/xconfmap
Loading
Loading