Skip to content

Commit 2f59992

Browse files
committed
Add compatibility tests for the keepalive config migration
Encode the downstream usage patterns found in contrib as behavioral requirements: partial deprecated fields keeping defaults, factories zeroing or customizing the deprecated fields programmatically, programmatic values not triggering the mixed-config error or deprecation warnings, zero-value configs, programmatic server keep-alive disabling, and marshal/unmarshal roundtrips. The unknown-key tests document the accepted loss of strict unmarshaling for components embedding these configs, which is inherent to implementing confmap.Unmarshaler on them. Most of these tests currently fail; they define the behavior the migration must preserve. Assisted-by: Claude Fable 5
1 parent 9b16fdd commit 2f59992

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package confighttp
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/zap"
15+
"go.uber.org/zap/zapcore"
16+
"go.uber.org/zap/zaptest/observer"
17+
18+
"go.opentelemetry.io/collector/component/componenttest"
19+
"go.opentelemetry.io/collector/confmap"
20+
)
21+
22+
// This file encodes compatibility requirements for the keepalive config
23+
// migration, derived from how opentelemetry-collector-contrib components use
24+
// the deprecated fields today. Each test asserts the behavior the pre-migration
25+
// code (main) produces for the same inputs; a failure here means a real,
26+
// user-visible behavior change in the named downstream components.
27+
28+
func compatClientTransport(t *testing.T, cc ClientConfig) *http.Transport {
29+
t.Helper()
30+
settings := componenttest.NewNopTelemetrySettings()
31+
settings.MeterProvider = nil
32+
settings.TracerProvider = nil
33+
client, err := cc.ToClient(t.Context(), nil, settings)
34+
require.NoError(t, err)
35+
transport, ok := client.Transport.(*http.Transport)
36+
require.True(t, ok, "transport is %T", client.Transport)
37+
return transport
38+
}
39+
40+
// compatServeAndGet builds the server from sc, serves a single request against
41+
// it, and returns the response so callers can observe connection handling.
42+
func compatServeAndGet(t *testing.T, sc ServerConfig) *http.Response {
43+
t.Helper()
44+
srv, err := sc.ToServer(t.Context(), nil, componenttest.NewNopTelemetrySettings(),
45+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
46+
w.WriteHeader(http.StatusOK)
47+
}))
48+
require.NoError(t, err)
49+
50+
ln, err := sc.ToListener(t.Context())
51+
require.NoError(t, err)
52+
done := make(chan struct{})
53+
go func() {
54+
defer close(done)
55+
_ = srv.Serve(ln)
56+
}()
57+
t.Cleanup(func() {
58+
assert.NoError(t, srv.Close())
59+
<-done
60+
})
61+
62+
resp, err := http.Get(fmt.Sprintf("http://%s/", ln.Addr()))
63+
require.NoError(t, err)
64+
require.NoError(t, resp.Body.Close())
65+
return resp
66+
}
67+
68+
// A user setting a subset of the deprecated fields must keep the defaults for
69+
// the settings they did not mention. On main, `idle_conn_timeout: 30s` alone
70+
// leaves MaxIdleConns at its default of 100.
71+
func TestKeepaliveCompatPartialDeprecatedFields(t *testing.T) {
72+
cfg := NewDefaultClientConfig()
73+
conf := confmap.NewFromStringMap(map[string]any{"idle_conn_timeout": "30s"})
74+
require.NoError(t, conf.Unmarshal(&cfg))
75+
76+
transport := compatClientTransport(t, cfg)
77+
assert.Equal(t, 30*time.Second, transport.IdleConnTimeout)
78+
assert.Equal(t, 100, transport.MaxIdleConns)
79+
}
80+
81+
// Factories zero the deprecated fields to mean "unlimited idle connections, no
82+
// idle timeout": prometheusreceiver, nsxtreceiver, httpcheckreceiver,
83+
// huaweicloudcesreceiver, haproxyreceiver, awsecscontainermetricsreceiver, and
84+
// awscontainerinsightreceiver in contrib. Zero must remain meaningful.
85+
func TestKeepaliveCompatFactoryZeroedDeprecatedFields(t *testing.T) {
86+
cfg := NewDefaultClientConfig()
87+
cfg.MaxIdleConns = 0
88+
cfg.IdleConnTimeout = 0
89+
require.NoError(t, confmap.NewFromStringMap(map[string]any{}).Unmarshal(&cfg))
90+
91+
transport := compatClientTransport(t, cfg)
92+
assert.Equal(t, 0, transport.MaxIdleConns)
93+
assert.Equal(t, time.Duration(0), transport.IdleConnTimeout)
94+
}
95+
96+
// Factories also set non-zero values on the deprecated fields, e.g.
97+
// signalfxexporter sets MaxIdleConns and MaxIdleConnsPerHost to 30000.
98+
func TestKeepaliveCompatFactoryCustomDeprecatedFields(t *testing.T) {
99+
cfg := NewDefaultClientConfig()
100+
cfg.MaxIdleConns = 30000
101+
cfg.MaxIdleConnsPerHost = 30000
102+
require.NoError(t, confmap.NewFromStringMap(map[string]any{}).Unmarshal(&cfg))
103+
104+
transport := compatClientTransport(t, cfg)
105+
assert.Equal(t, 30000, transport.MaxIdleConns)
106+
assert.Equal(t, 30000, transport.MaxIdleConnsPerHost)
107+
}
108+
109+
// Deprecated-field values set programmatically by a factory are not "the user
110+
// set both": a user must be able to adopt the keepalive section on a component
111+
// whose factory customizes the deprecated fields without hitting the
112+
// mixed-config error.
113+
func TestKeepaliveCompatFactoryFieldsDoNotConflictWithNewSection(t *testing.T) {
114+
cfg := NewDefaultClientConfig()
115+
cfg.MaxIdleConns = 30000
116+
cfg.MaxIdleConnsPerHost = 30000
117+
conf := confmap.NewFromStringMap(map[string]any{
118+
"keepalive": map[string]any{"idle_conn_timeout": "5m"},
119+
})
120+
require.NoError(t, conf.Unmarshal(&cfg))
121+
}
122+
123+
// Deprecation warnings must be triggered only by configuration the user wrote,
124+
// never by factory-set values: users of a component whose factory customizes
125+
// the deprecated fields (signalfxexporter) can't act on the warning.
126+
func TestKeepaliveCompatNoWarningsForFactoryFields(t *testing.T) {
127+
cfg := NewDefaultClientConfig()
128+
cfg.MaxIdleConns = 30000
129+
require.NoError(t, confmap.NewFromStringMap(map[string]any{}).Unmarshal(&cfg))
130+
131+
core, observed := observer.New(zapcore.WarnLevel)
132+
settings := componenttest.NewNopTelemetrySettings()
133+
settings.MeterProvider = nil
134+
settings.TracerProvider = nil
135+
settings.Logger = zap.New(core)
136+
_, err := cfg.ToClient(t.Context(), nil, settings)
137+
require.NoError(t, err)
138+
assert.Empty(t, observed.All())
139+
}
140+
141+
// A zero-value ClientConfig used programmatically (without NewDefaultClientConfig)
142+
// has keep-alives enabled on main. Test helpers downstream construct bare
143+
// ClientConfig literals and must not silently switch to one-connection-per-request.
144+
func TestKeepaliveCompatZeroValueClientConfig(t *testing.T) {
145+
cfg := ClientConfig{Endpoint: "http://localhost"}
146+
transport := compatClientTransport(t, cfg)
147+
assert.False(t, transport.DisableKeepAlives)
148+
}
149+
150+
// Factories disable server keep-alives programmatically: splunkhecreceiver,
151+
// remotetapprocessor, githubreceiver, gitlabreceiver, libhoneyreceiver,
152+
// collectdreceiver, azurefunctionsreceiver, and prometheusremotewritereceiver
153+
// in contrib all set KeepAlivesEnabled = false in createDefaultConfig. The
154+
// built server must send `Connection: close`.
155+
func TestKeepaliveCompatServerFactoryDisabledKeepAlives(t *testing.T) {
156+
cfg := NewDefaultServerConfig()
157+
cfg.KeepAlivesEnabled = false
158+
conf := confmap.NewFromStringMap(map[string]any{"endpoint": "localhost:0"})
159+
require.NoError(t, conf.Unmarshal(&cfg))
160+
161+
resp := compatServeAndGet(t, cfg)
162+
assert.True(t, resp.Close, "server must disable keep-alives")
163+
}
164+
165+
// The same via user configuration: `keep_alives_enabled: false` in yaml.
166+
func TestKeepaliveCompatServerDeprecatedDisableViaConfig(t *testing.T) {
167+
cfg := NewDefaultServerConfig()
168+
conf := confmap.NewFromStringMap(map[string]any{
169+
"endpoint": "localhost:0",
170+
"keep_alives_enabled": false,
171+
})
172+
require.NoError(t, conf.Unmarshal(&cfg))
173+
174+
resp := compatServeAndGet(t, cfg)
175+
assert.True(t, resp.Close, "server must disable keep-alives")
176+
}
177+
178+
// A config loaded from deprecated fields must survive a marshal/unmarshal
179+
// roundtrip: `print-initial-config` output is valid collector configuration,
180+
// and tooling (e.g. the OpAMP supervisor) re-marshals effective configs.
181+
func TestKeepaliveCompatClientMarshalRoundtrip(t *testing.T) {
182+
cfg := NewDefaultClientConfig()
183+
require.NoError(t, confmap.NewFromStringMap(map[string]any{"idle_conn_timeout": "60s"}).Unmarshal(&cfg))
184+
185+
marshaled := confmap.New()
186+
require.NoError(t, marshaled.Marshal(cfg))
187+
188+
cfg2 := NewDefaultClientConfig()
189+
require.NoError(t, marshaled.Unmarshal(&cfg2), "marshaled output must load without a mixed-config error")
190+
191+
transport := compatClientTransport(t, cfg2)
192+
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
193+
assert.Equal(t, 100, transport.MaxIdleConns)
194+
}
195+
196+
func TestKeepaliveCompatServerMarshalRoundtrip(t *testing.T) {
197+
cfg := NewDefaultServerConfig()
198+
require.NoError(t, confmap.NewFromStringMap(map[string]any{"idle_timeout": "2m"}).Unmarshal(&cfg))
199+
200+
marshaled := confmap.New()
201+
require.NoError(t, marshaled.Marshal(cfg))
202+
203+
cfg2 := NewDefaultServerConfig()
204+
require.NoError(t, marshaled.Unmarshal(&cfg2), "marshaled output must load without a mixed-config error")
205+
}
206+
207+
// Strict unmarshaling rejects unknown keys on main, but implementing
208+
// confmap.Unmarshaler on ClientConfig/ServerConfig forfeits this for every
209+
// embedding component: for squash embedding, confmap's embedded-structs hook
210+
// decodes sibling fields with unused keys ignored, and for named sections the
211+
// Unmarshal implementation must use WithIgnoreUnused to support the squash
212+
// case. These tests document that accepted trade-off. If they start failing,
213+
// strict checking has been restored (e.g. the Unmarshaler was removed or
214+
// confmap learned to track unused keys across the hook) and they should be
215+
// flipped back to asserting an error.
216+
func TestKeepaliveCompatUnknownKeyIgnoredSquash(t *testing.T) {
217+
cfg := namedSquashClientConfig{ClientConfig: NewDefaultClientConfig()}
218+
conf := confmap.NewFromStringMap(map[string]any{
219+
"endpoint": "http://localhost:4318",
220+
"bogus_key": 1,
221+
})
222+
assert.NoError(t, conf.Unmarshal(&cfg), "unknown keys are ignored for configs embedding ClientConfig")
223+
}
224+
225+
func TestKeepaliveCompatUnknownKeyIgnoredNamedSection(t *testing.T) {
226+
type outerConfig struct {
227+
Egress ClientConfig `mapstructure:"egress"`
228+
}
229+
cfg := outerConfig{Egress: NewDefaultClientConfig()}
230+
conf := confmap.NewFromStringMap(map[string]any{
231+
"egress": map[string]any{
232+
"endpoint": "http://localhost:4318",
233+
"bogus_key": 1,
234+
},
235+
})
236+
assert.NoError(t, conf.Unmarshal(&cfg), "unknown keys are ignored inside a ClientConfig section")
237+
}

0 commit comments

Comments
 (0)