diff --git a/.chloggen/support_embedded_structs_confmap.yaml b/.chloggen/support_embedded_structs_confmap.yaml new file mode 100755 index 00000000000..7d010e9524d --- /dev/null +++ b/.chloggen/support_embedded_structs_confmap.yaml @@ -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: confmap + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: confmap honors `Unmarshal` methods on config embedded structs. + +# One or more tracking issues or pull requests related to the change +issues: [6671] + +# (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: + +# 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: [] diff --git a/cmd/mdatagen/loader_test.go b/cmd/mdatagen/loader_test.go index aafacc8f252..b5f17a7ad82 100644 --- a/cmd/mdatagen/loader_test.go +++ b/cmd/mdatagen/loader_test.go @@ -245,7 +245,7 @@ func Test_loadMetadata(t *testing.T) { }, { name: "testdata/unknown_value_type.yaml", - wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'value_type': invalid value_type: \"unknown\"", + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n* error decoding 'sum': invalid value_type: \"unknown\"", }, { name: "testdata/no_aggregation.yaml", @@ -255,7 +255,7 @@ func Test_loadMetadata(t *testing.T) { { name: "testdata/invalid_aggregation.yaml", want: metadata{}, - wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'aggregation_temporality': invalid aggregation: \"invalidaggregation\"", + wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': error decoding '': invalid aggregation: \"invalidaggregation\"", }, { name: "testdata/invalid_type_attr.yaml", diff --git a/cmd/mdatagen/metricdata.go b/cmd/mdatagen/metricdata.go index f4515136aae..8da2cd8f346 100644 --- a/cmd/mdatagen/metricdata.go +++ b/cmd/mdatagen/metricdata.go @@ -28,18 +28,22 @@ type MetricData interface { type AggregationTemporality struct { // Aggregation describes if the aggregator reports delta changes // since last report time, or cumulative changes since a fixed start time. - Aggregation pmetric.AggregationTemporality -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -func (agg *AggregationTemporality) UnmarshalText(text []byte) error { - switch vtStr := string(text); vtStr { - case "cumulative": - agg.Aggregation = pmetric.AggregationTemporalityCumulative - case "delta": - agg.Aggregation = pmetric.AggregationTemporalityDelta - default: - return fmt.Errorf("invalid aggregation: %q", vtStr) + Aggregation pmetric.AggregationTemporality `mapstructure:"aggregation_temporality"` +} + +func (agg *AggregationTemporality) Unmarshal(parser *confmap.Conf) error { + v := parser.Get("aggregation_temporality") + if aggValue, ok := v.(pmetric.AggregationTemporality); ok { + agg.Aggregation = aggValue + } else { + switch v { + case "cumulative": + agg.Aggregation = pmetric.AggregationTemporalityCumulative + case "delta": + agg.Aggregation = pmetric.AggregationTemporalityDelta + default: + return fmt.Errorf("invalid aggregation: %q", v) + } } return nil } @@ -73,22 +77,17 @@ func (mit MetricInputType) String() string { // MetricValueType defines the metric number type. type MetricValueType struct { // ValueType is type of the metric number, options are "double", "int". - ValueType pmetric.NumberDataPointValueType + ValueType pmetric.NumberDataPointValueType `mapstructure:"value_type"` } func (mvt *MetricValueType) Unmarshal(parser *confmap.Conf) error { if !parser.IsSet("value_type") { return errors.New("missing required field: `value_type`") } - return nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -func (mvt *MetricValueType) UnmarshalText(text []byte) error { - switch vtStr := string(text); vtStr { - case "int": + switch vtStr := parser.Get("value_type"); vtStr { + case "int", pmetric.NumberDataPointValueTypeInt: mvt.ValueType = pmetric.NumberDataPointValueTypeInt - case "double": + case "double", pmetric.NumberDataPointValueTypeDouble: mvt.ValueType = pmetric.NumberDataPointValueTypeDouble default: return fmt.Errorf("invalid value_type: %q", vtStr) @@ -116,7 +115,7 @@ func (mvt MetricValueType) BasicType() string { } type gauge struct { - MetricValueType `mapstructure:"value_type"` + MetricValueType `mapstructure:",squash"` MetricInputType `mapstructure:",squash"` } @@ -141,9 +140,9 @@ func (d gauge) HasAggregated() bool { } type sum struct { - AggregationTemporality `mapstructure:"aggregation_temporality"` + AggregationTemporality `mapstructure:",squash"` Mono `mapstructure:",squash"` - MetricValueType `mapstructure:"value_type"` + MetricValueType `mapstructure:",squash"` MetricInputType `mapstructure:",squash"` } diff --git a/confmap/confmap.go b/confmap/confmap.go index 9664109f9ab..c43932f2c85 100644 --- a/confmap/confmap.go +++ b/confmap/confmap.go @@ -157,6 +157,7 @@ func decodeConfig(m *Conf, result any, errorUnused bool) error { mapstructure.StringToTimeDurationHookFunc(), mapstructure.TextUnmarshallerHookFunc(), unmarshalerHookFunc(result), + embeddedStructsHookFunc(result), zeroSliceHookFunc(), ), } @@ -261,6 +262,41 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy } } +func embeddedStructsHookFunc(_ any) mapstructure.DecodeHookFuncValue { + return func(from reflect.Value, to reflect.Value) (any, error) { + if to.Type().Kind() != reflect.Struct { + return from.Interface(), nil + } + + finalFrom := from.Interface() + + for i := 0; i < to.Type().NumField(); i++ { + if to.Type().Field(i).IsExported() && to.Type().Field(i).Anonymous { + f := to.Field(i) + if unmarshaler, ok := f.Addr().Interface().(Unmarshaler); ok { + fromMap, ok := finalFrom.(map[string]any) + if !ok { + return from.Interface(), nil + } + if err := unmarshaler.Unmarshal(NewFromStringMap(fromMap)); err != nil { + return nil, err + } + conf := New() + if err := conf.Marshal(unmarshaler); err != nil { + return nil, err + } + resultMap := conf.ToStringMap() + for k, v := range resultMap { + fromMap[k] = v + } + finalFrom = fromMap + } + } + } + return finalFrom, nil + } +} + // Provides a mechanism for individual structs to define their own unmarshal logic, // by implementing the Unmarshaler interface. func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue { diff --git a/confmap/confmap_test.go b/confmap/confmap_test.go index 6680a1bc517..0241b3be7ef 100644 --- a/confmap/confmap_test.go +++ b/confmap/confmap_test.go @@ -309,12 +309,45 @@ func newConfFromFile(t testing.TB, fileName string) map[string]any { } type testConfig struct { - Next *nextConfig `mapstructure:"next"` - Another string `mapstructure:"another"` + Next *nextConfig `mapstructure:"next"` + Another string `mapstructure:"another"` + EmbeddedConfig `mapstructure:",squash"` + EmbeddedConfig2 `mapstructure:",squash"` +} + +type testConfigWithoutUnmarshaler struct { + Next *nextConfig `mapstructure:"next"` + Another string `mapstructure:"another"` + EmbeddedConfig `mapstructure:",squash"` + EmbeddedConfig2 `mapstructure:",squash"` +} + +type EmbeddedConfig struct { + Some string `mapstructure:"some"` +} + +func (ec *EmbeddedConfig) Unmarshal(component *Conf) error { + if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil { + return err + } + ec.Some += " is also called" + return nil +} + +type EmbeddedConfig2 struct { + Some2 string `mapstructure:"some_2"` +} + +func (ec *EmbeddedConfig2) Unmarshal(component *Conf) error { + if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil { + return err + } + ec.Some2 += " also called2" + return nil } func (tc *testConfig) Unmarshal(component *Conf) error { - if err := component.Unmarshal(tc); err != nil { + if err := component.Unmarshal(tc, WithIgnoreUnused()); err != nil { return err } tc.Another += " is only called directly" @@ -340,12 +373,34 @@ func TestUnmarshaler(t *testing.T) { "string": "make sure this", }, "another": "make sure this", + "some": "make sure this", + "some_2": "this better be", }) tc := &testConfig{} assert.NoError(t, cfgMap.Unmarshal(tc)) assert.Equal(t, "make sure this", tc.Another) assert.Equal(t, "make sure this is called", tc.Next.String) + assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some) + assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2) +} + +func TestEmbeddedUnmarshaler(t *testing.T) { + cfgMap := NewFromStringMap(map[string]any{ + "next": map[string]any{ + "string": "make sure this", + }, + "another": "make sure this", + "some": "make sure this", + "some_2": "this better be", + }) + + tc := &testConfigWithoutUnmarshaler{} + assert.NoError(t, cfgMap.Unmarshal(tc)) + assert.Equal(t, "make sure this", tc.Another) + assert.Equal(t, "make sure this is called", tc.Next.String) + assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some) + assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2) } func TestUnmarshalerKeepAlreadyInitialized(t *testing.T) {