Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2955df1
Allow list of name/value pairs for configgrpc.ClientConfig.Headers
jade-guiton-dd Oct 9, 2025
c933de4
Add generic internal/maplist package, and specialized configoptional.…
jade-guiton-dd Oct 10, 2025
80dd3ab
Merge branch 'main' into list-map-duality
jade-guiton-dd Oct 10, 2025
cab6953
gotidy
jade-guiton-dd Oct 10, 2025
10f7315
Fix otelcorecol
jade-guiton-dd Oct 10, 2025
38ce272
Add single changelog under 'all' component
jade-guiton-dd Oct 10, 2025
d3c25ab
Fix ocb test
jade-guiton-dd Oct 10, 2025
38b451d
Add 'maplist' to spellcheck list
jade-guiton-dd Oct 10, 2025
549da85
Enforce key unicity, add `Pairs` method, and more tests
jade-guiton-dd Oct 15, 2025
aa534ad
Better test coverage
jade-guiton-dd Oct 15, 2025
41a3893
Make API closer to native map to ease migration
jade-guiton-dd Oct 20, 2025
18d6730
tidy and lint
jade-guiton-dd Oct 20, 2025
5ba5118
Remove usages of MapListFromList
mx-psi Oct 21, 2025
6046148
Reformat MapList literals
jade-guiton-dd Oct 21, 2025
464ff75
Remove superfluous methods and specialize for configopaque.String
jade-guiton-dd Oct 21, 2025
fd2c22b
Merge branch 'main' into list-map-duality
jade-guiton-dd Oct 21, 2025
a0dd48d
tidy + checkapi
jade-guiton-dd Oct 21, 2025
31225c1
Fix test and minimize diff
jade-guiton-dd Oct 21, 2025
9fc0775
Use MapList without pointer
jade-guiton-dd Oct 22, 2025
0c39a29
tidy + fixes
jade-guiton-dd Oct 22, 2025
3ac2104
Make ResponseHeaders omitempty and fix e2e test
jade-guiton-dd Oct 22, 2025
417796e
Address review feedback
jade-guiton-dd Oct 23, 2025
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
38 changes: 38 additions & 0 deletions .chloggen/list-map-duality.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Use this changelog template to create an entry for release notes.

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

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

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Change type of `configgrpc.ClientConfig.Headers`, `confighttp.ClientConfig.Headers`, and `confighttp.ServerConfig.ResponseHeaders`

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

# (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: |
`configopaque.MapList` is a new alternative to `map[string]configopaque.String` which can unmarshal
both maps and lists of name/value pairs.

For example, if `headers` is a field of type `configopaque.MapList`,
then the following YAML configs will unmarshal to the same thing:
```yaml
headers:
"foo": "bar"

headers:
- name: "foo"
value: "bar"
```

# 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]
4 changes: 2 additions & 2 deletions config/configgrpc/configgrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type ClientConfig struct {
WaitForReady bool `mapstructure:"wait_for_ready,omitempty"`

// The headers associated with gRPC requests.
Headers map[string]configopaque.String `mapstructure:"headers,omitempty"`
Headers configopaque.MapList `mapstructure:"headers,omitempty"`

// Sets the balancer in grpclb_policy to discover the servers. Default is pick_first.
// https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md
Expand Down Expand Up @@ -289,7 +289,7 @@ func (cc *ClientConfig) ToClientConn(
func (cc *ClientConfig) addHeadersIfAbsent(ctx context.Context) context.Context {
kv := make([]string, 0, 2*len(cc.Headers))
existingMd, _ := metadata.FromOutgoingContext(ctx)
for k, v := range cc.Headers {
for k, v := range cc.Headers.Iter {
if len(existingMd.Get(k)) == 0 {
kv = append(kv, k, string(v))
}
Expand Down
20 changes: 10 additions & 10 deletions config/configgrpc/configgrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ func TestAllGrpcClientSettings(t *testing.T) {
{
name: "test all with gzip compression",
settings: ClientConfig{
Headers: map[string]configopaque.String{
"test": "test",
Headers: configopaque.MapList{
{Name: "test", Value: "test"},
},
Endpoint: "localhost:1234",
Compression: configcompression.TypeGzip,
Expand Down Expand Up @@ -192,8 +192,8 @@ func TestAllGrpcClientSettings(t *testing.T) {
{
name: "test all with snappy compression",
settings: ClientConfig{
Headers: map[string]configopaque.String{
"test": "test",
Headers: configopaque.MapList{
{Name: "test", Value: "test"},
},
Endpoint: "localhost:1234",
Compression: configcompression.TypeSnappy,
Expand Down Expand Up @@ -221,8 +221,8 @@ func TestAllGrpcClientSettings(t *testing.T) {
{
name: "test all with zstd compression",
settings: ClientConfig{
Headers: map[string]configopaque.String{
"test": "test",
Headers: configopaque.MapList{
{Name: "test", Value: "test"},
},
Endpoint: "localhost:1234",
Compression: configcompression.TypeZstd,
Expand Down Expand Up @@ -285,8 +285,8 @@ func TestHeaders(t *testing.T) {
TLS: configtls.ClientConfig{
Insecure: true,
},
Headers: map[string]configopaque.String{
"testheader": "testvalue",
Headers: configopaque.MapList{
{Name: "testheader", Value: "testvalue"},
},
})
require.NoError(t, errResp)
Expand Down Expand Up @@ -434,8 +434,8 @@ func TestGrpcServerAuthSettings(t *testing.T) {

func TestGrpcClientConfigInvalidBalancer(t *testing.T) {
settings := ClientConfig{
Headers: map[string]configopaque.String{
"test": "test",
Headers: configopaque.MapList{
{Name: "test", Value: "test"},
},
Endpoint: "localhost:1234",
Compression: "gzip",
Expand Down
9 changes: 4 additions & 5 deletions config/confighttp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type ClientConfig struct {
// Additional headers attached to each HTTP request sent by the client.
// Existing header values are overwritten if collision happens.
// Header values are opaque since they may be sensitive.
Headers map[string]configopaque.String `mapstructure:"headers,omitempty"`
Headers configopaque.MapList `mapstructure:"headers,omitempty"`

// Auth configuration for outgoing HTTP calls.
Auth configoptional.Optional[configauth.Config] `mapstructure:"auth,omitempty"`
Expand Down Expand Up @@ -130,7 +130,6 @@ func NewDefaultClientConfig() ClientConfig {
defaultTransport := http.DefaultTransport.(*http.Transport)

return ClientConfig{
Headers: map[string]configopaque.String{},
MaxIdleConns: defaultTransport.MaxIdleConns,
IdleConnTimeout: defaultTransport.IdleConnTimeout,
ForceAttemptHTTP2: true,
Expand Down Expand Up @@ -283,18 +282,18 @@ func (cc *ClientConfig) ToClient(ctx context.Context, host component.Host, setti
// Custom RoundTripper that adds headers.
type headerRoundTripper struct {
transport http.RoundTripper
headers map[string]configopaque.String
headers configopaque.MapList
}

// RoundTrip is a custom RoundTripper that adds headers to the request.
func (interceptor *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// Set Host header if provided
hostHeader, found := interceptor.headers["Host"]
hostHeader, found := interceptor.headers.Get("Host")
if found && hostHeader != "" {
// `Host` field should be set to override default `Host` header value which is Endpoint
req.Host = string(hostHeader)
}
for k, v := range interceptor.headers {
for k, v := range interceptor.headers.Iter {
req.Header.Set(k, string(v))
}

Expand Down
24 changes: 13 additions & 11 deletions config/confighttp/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ func TestHTTPClientSettingWithAuthConfig(t *testing.T) {
settings: ClientConfig{
Endpoint: "localhost:1234",
Auth: configoptional.Some(configauth.Config{AuthenticatorID: mockID}),
Headers: map[string]configopaque.String{"foo": "bar"},
Headers: configopaque.MapList{
{Name: "foo", Value: "bar"},
},
},
shouldErr: false,
host: &mockHost{
Expand Down Expand Up @@ -505,19 +507,19 @@ func TestHTTPClientSettingWithAuthConfig(t *testing.T) {
func TestHttpClientHeaders(t *testing.T) {
tests := []struct {
name string
headers map[string]configopaque.String
headers configopaque.MapList
}{
{
name: "with_headers",
headers: map[string]configopaque.String{
"header1": "value1",
headers: configopaque.MapList{
{Name: "header1", Value: "value1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range tt.headers {
for k, v := range tt.headers.Iter {
assert.Equal(t, r.Header.Get(k), string(v))
}
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -545,11 +547,11 @@ func TestHttpClientHostHeader(t *testing.T) {
hostHeader := "th"
tt := struct {
name string
headers map[string]configopaque.String
headers configopaque.MapList
}{
name: "with_host_header",
headers: map[string]configopaque.String{
"Host": configopaque.String(hostHeader),
headers: configopaque.MapList{
{Name: "Host", Value: configopaque.String(hostHeader)},
},
}

Expand Down Expand Up @@ -724,9 +726,9 @@ func TestClientUnmarshalYAMLComprehensiveConfig(t *testing.T) {
assert.Equal(t, "example.com", clientConfig.TLS.ServerName)

// Verify headers
expectedHeaders := map[string]configopaque.String{
"User-Agent": "OpenTelemetry-Collector/1.0",
"X-Custom-Header": "custom-value",
expectedHeaders := configopaque.MapList{
{Name: "User-Agent", Value: "OpenTelemetry-Collector/1.0"},
{Name: "X-Custom-Header", Value: "custom-value"},
}
assert.Equal(t, expectedHeaders, clientConfig.Headers)

Expand Down
7 changes: 3 additions & 4 deletions config/confighttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type ServerConfig struct {

// Additional headers attached to each HTTP response sent to the client.
// Header values are opaque since they may be sensitive.
ResponseHeaders map[string]configopaque.String `mapstructure:"response_headers"`
ResponseHeaders configopaque.MapList `mapstructure:"response_headers,omitempty"`

// CompressionAlgorithms configures the list of compression algorithms the server can accept. Default: ["", "gzip", "zstd", "zlib", "snappy", "deflate"]
CompressionAlgorithms []string `mapstructure:"compression_algorithms,omitempty"`
Expand Down Expand Up @@ -102,7 +102,6 @@ type ServerConfig struct {
// We encourage to use this function to create an object of ServerConfig.
func NewDefaultServerConfig() ServerConfig {
return ServerConfig{
ResponseHeaders: map[string]configopaque.String{},
WriteTimeout: 30 * time.Second,
ReadHeaderTimeout: 1 * time.Minute,
IdleTimeout: 1 * time.Minute,
Expand Down Expand Up @@ -287,11 +286,11 @@ func (sc *ServerConfig) ToServer(ctx context.Context, host component.Host, setti
return server, err
}

func responseHeadersHandler(handler http.Handler, headers map[string]configopaque.String) http.Handler {
func responseHeadersHandler(handler http.Handler, headers configopaque.MapList) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()

for k, v := range headers {
for k, v := range headers.Iter {
h.Set(k, string(v))
}

Expand Down
23 changes: 9 additions & 14 deletions config/confighttp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,21 +428,17 @@ func TestHttpCorsWithSettings(t *testing.T) {
func TestHttpServerHeaders(t *testing.T) {
tests := []struct {
name string
headers map[string]configopaque.String
headers configopaque.MapList
}{
{
name: "noHeaders",
headers: nil,
},
{
name: "emptyHeaders",
headers: map[string]configopaque.String{},
},
{
name: "withHeaders",
headers: map[string]configopaque.String{
"x-new-header-1": "value1",
"x-new-header-2": "value2",
headers: configopaque.MapList{
{Name: "x-new-header-1", Value: "value1"},
{Name: "x-new-header-2", Value: "value2"},
},
},
}
Expand Down Expand Up @@ -515,7 +511,7 @@ func verifyCorsResp(t *testing.T, url, origin string, set configoptional.Optiona
assert.Equal(t, wantMaxAge, resp.Header.Get("Access-Control-Max-Age"))
}

func verifyHeadersResp(t *testing.T, url string, expected map[string]configopaque.String) {
func verifyHeadersResp(t *testing.T, url string, expected configopaque.MapList) {
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
require.NoError(t, err, "Error creating request")

Expand All @@ -526,7 +522,7 @@ func verifyHeadersResp(t *testing.T, url string, expected map[string]configopaqu

assert.Equal(t, http.StatusOK, resp.StatusCode)

for k, v := range expected {
for k, v := range expected.Iter {
assert.Equal(t, string(v), resp.Header.Get(k))
}
}
Expand Down Expand Up @@ -950,7 +946,6 @@ func BenchmarkHttpRequest(b *testing.B) {

func TestDefaultHTTPServerSettings(t *testing.T) {
httpServerSettings := NewDefaultServerConfig()
assert.NotNil(t, httpServerSettings.ResponseHeaders)
assert.NotNil(t, httpServerSettings.CORS)
assert.NotNil(t, httpServerSettings.TLS)
assert.Equal(t, 1*time.Minute, httpServerSettings.IdleTimeout)
Expand Down Expand Up @@ -1138,9 +1133,9 @@ func TestServerUnmarshalYAMLComprehensiveConfig(t *testing.T) {
assert.Equal(t, 7200, serverConfig.CORS.Get().MaxAge)

// Verify response headers
expectedResponseHeaders := map[string]configopaque.String{
"Server": "OpenTelemetry-Collector",
"X-Flavor": "apple",
expectedResponseHeaders := configopaque.MapList{
{Name: "Server", Value: "OpenTelemetry-Collector"},
{Name: "X-Flavor", Value: "apple"},
}
assert.Equal(t, expectedResponseHeaders, serverConfig.ResponseHeaders)

Expand Down
22 changes: 19 additions & 3 deletions config/configopaque/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@ go 1.24.0

require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/collector/confmap v1.44.0
go.opentelemetry.io/collector/confmap/xconfmap v0.138.0
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
github.com/knadh/koanf/v2 v2.3.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
go.opentelemetry.io/collector/featuregate v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

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

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

replace go.opentelemetry.io/collector/confmap/xconfmap => ../../confmap/xconfmap
26 changes: 20 additions & 6 deletions config/configopaque/go.sum

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

Loading
Loading