diff --git a/.chloggen/13783.yaml b/.chloggen/13783.yaml new file mode 100644 index 00000000000..bcc298d9cac --- /dev/null +++ b/.chloggen/13783.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: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `keep_alives_enabled` option to ServerConfig to control HTTP keep-alives for all HTTP servers + +# One or more tracking issues or pull requests related to the change +issues: [13783] + +# (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: [user, api] diff --git a/config/confighttp/server.go b/config/confighttp/server.go index a0b293dee89..f9da9a53768 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -92,6 +92,10 @@ type ServerConfig struct { // Middleware handlers are called in the order they appear in this list, // with the first middleware becoming the outermost handler. Middlewares []configmiddleware.Config `mapstructure:"middlewares,omitempty"` + + // KeepAlivesEnabled controls whether HTTP keep-alives are enabled. + // By default, keep-alives are always enabled. Only very resource-constrained environments should disable them. + KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"` } // NewDefaultServerConfig returns ServerConfig type object with default values. @@ -102,6 +106,7 @@ func NewDefaultServerConfig() ServerConfig { WriteTimeout: 30 * time.Second, ReadHeaderTimeout: 1 * time.Minute, IdleTimeout: 1 * time.Minute, + KeepAlivesEnabled: true, } } @@ -276,6 +281,9 @@ func (sc *ServerConfig) ToServer(ctx context.Context, host component.Host, setti ErrorLog: errorLog, } + // Set keep-alives enabled/disabled + server.SetKeepAlivesEnabled(sc.KeepAlivesEnabled) + return server, err } diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index c44df79a662..b254efc2168 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -957,6 +957,68 @@ func TestDefaultHTTPServerSettings(t *testing.T) { assert.Equal(t, 30*time.Second, httpServerSettings.WriteTimeout) assert.Equal(t, time.Duration(0), httpServerSettings.ReadTimeout) assert.Equal(t, 1*time.Minute, httpServerSettings.ReadHeaderTimeout) + assert.True(t, httpServerSettings.KeepAlivesEnabled) // Default should be true (keep-alives enabled by default) +} + +func TestHTTPServerKeepAlives(t *testing.T) { + tests := []struct { + name string + keepAlivesEnabled bool + expectedKeepAlives bool + }{ + { + name: "KeepAlives enabled", + keepAlivesEnabled: true, + expectedKeepAlives: true, + }, + { + name: "KeepAlives disabled", + keepAlivesEnabled: false, + expectedKeepAlives: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &ServerConfig{ + Endpoint: "localhost:0", + KeepAlivesEnabled: tt.keepAlivesEnabled, + } + + server, err := sc.ToServer( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + require.NoError(t, err) + require.NotNil(t, server) + + // Since http.Server.disableKeepAlives is a private field and difficult to test directly, + // we'll verify the configuration was set by testing the server behavior. + // The main verification is that ToServer() succeeds without error when DisableKeepAlives is set. + + ln, err := sc.ToListener(context.Background()) + require.NoError(t, err) + + go func() { + _ = server.Serve(ln) + }() + defer func() { + _ = server.Close() + }() + + resp, err := http.Get("http://" + ln.Addr().String()) + require.NoError(t, err) + require.NotNil(t, resp) + _ = resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + assert.Equal(t, tt.keepAlivesEnabled, sc.KeepAlivesEnabled) + }) + } } func TestHTTPServerTelemetry_Tracing(t *testing.T) { @@ -1058,6 +1120,7 @@ func TestServerUnmarshalYAMLComprehensiveConfig(t *testing.T) { assert.Equal(t, 10*time.Second, serverConfig.ReadHeaderTimeout) assert.Equal(t, 30*time.Second, serverConfig.WriteTimeout) assert.Equal(t, 120*time.Second, serverConfig.IdleTimeout) + assert.True(t, serverConfig.KeepAlivesEnabled) // Should be true as configured in config.yaml assert.Equal(t, int64(33554432), serverConfig.MaxRequestBodySize) assert.True(t, serverConfig.IncludeMetadata) diff --git a/config/confighttp/testdata/config.yaml b/config/confighttp/testdata/config.yaml index af82120e0c8..0b241df6fb9 100644 --- a/config/confighttp/testdata/config.yaml +++ b/config/confighttp/testdata/config.yaml @@ -90,6 +90,9 @@ server: write_timeout: 30s idle_timeout: 120s + # HTTP keep-alives configuration + keep_alives_enabled: true + # Maximum request size max_request_body_size: 33554432 # 32MB diff --git a/internal/e2e/confighttp_test.go b/internal/e2e/confighttp_test.go index 9442836ed5f..12ecc0a75d5 100644 --- a/internal/e2e/confighttp_test.go +++ b/internal/e2e/confighttp_test.go @@ -33,6 +33,7 @@ func TestConfmapMarshalConfigHTTP(t *testing.T) { assert.Equal(t, map[string]any{ "cors": nil, "idle_timeout": 60 * time.Second, + "keep_alives_enabled": true, "read_header_timeout": 60 * time.Second, "response_headers": map[string]any{}, "tls": nil, diff --git a/receiver/otlpreceiver/config_test.go b/receiver/otlpreceiver/config_test.go index 628df5e7bf3..64dc7102d22 100644 --- a/receiver/otlpreceiver/config_test.go +++ b/receiver/otlpreceiver/config_test.go @@ -140,7 +140,8 @@ func TestUnmarshalConfig(t *testing.T) { AllowedOrigins: []string{"https://*.test.com", "https://test.com"}, MaxAge: 7200, }), - ResponseHeaders: map[string]configopaque.String{}, + ResponseHeaders: map[string]configopaque.String{}, + KeepAlivesEnabled: true, }, TracesURLPath: "/traces", MetricsURLPath: "/v2/metrics", @@ -169,8 +170,9 @@ func TestUnmarshalConfigUnix(t *testing.T) { }), HTTP: configoptional.Some(HTTPConfig{ ServerConfig: confighttp.ServerConfig{ - Endpoint: "/tmp/http_otlp.sock", - ResponseHeaders: map[string]configopaque.String{}, + Endpoint: "/tmp/http_otlp.sock", + ResponseHeaders: map[string]configopaque.String{}, + KeepAlivesEnabled: true, }, TracesURLPath: defaultTracesURLPath, MetricsURLPath: defaultMetricsURLPath,