diff --git a/.chloggen/feat_zipkin-receiver-disable-keep-alives.yaml b/.chloggen/feat_zipkin-receiver-disable-keep-alives.yaml new file mode 100644 index 0000000000000..01e1d71efa507 --- /dev/null +++ b/.chloggen/feat_zipkin-receiver-disable-keep-alives.yaml @@ -0,0 +1,31 @@ +# 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. filelogreceiver) +component: zipkinreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `disable_keep_alives` configuration option to control HTTP keep-alive behavior + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [42531] + +# (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: | + This allows users to disable HTTP keep-alive connections in the Zipkin receiver. + When disabled, the server will close connections after each request, which can + help with resource management in high-load scenarios or when working with certain + load balancers. The feature is disabled by default to maintain backward compatibility. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# 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] diff --git a/receiver/zipkinreceiver/README.md b/receiver/zipkinreceiver/README.md index 8999343324642..0a3fb2114339c 100644 --- a/receiver/zipkinreceiver/README.md +++ b/receiver/zipkinreceiver/README.md @@ -27,10 +27,19 @@ receivers: zipkin: ``` +To disable HTTP keep-alive connections: + +```yaml +receivers: + zipkin: + disable_keep_alives: true +``` + The following settings are configurable: - `endpoint` (default = localhost:9411): host:port on which the receiver is going to receive data.See our [security best practices doc](https://opentelemetry.io/docs/security/config-best-practices/#protect-against-denial-of-service-attacks) to understand how to set the endpoint in different environments. You can review the [full list of `ServerConfig`](https://github.com/open-telemetry/opentelemetry-collector/tree/main/config/confighttp). - `parse_string_tags` (default = false): if enabled, the receiver will attempt to parse string tags/binary annotations into int/bool/float. +- `disable_keep_alives` (default = false): if true, HTTP keep-alive is disabled. When disabled, the server will close connections after each request. ## Advanced Configuration diff --git a/receiver/zipkinreceiver/config.go b/receiver/zipkinreceiver/config.go index 3a2adf1f97ed4..fca5110fb9d1f 100644 --- a/receiver/zipkinreceiver/config.go +++ b/receiver/zipkinreceiver/config.go @@ -15,6 +15,8 @@ type Config struct { // If enabled the zipkin receiver will attempt to parse string tags/binary annotations into int/bool/float. // Disabled by default ParseStringTags bool `mapstructure:"parse_string_tags"` + // If true, HTTP keep-alive is disabled. By default, keep-alive is enabled. + DisableKeepAlives bool `mapstructure:"disable_keep_alives"` // prevent unkeyed literal initialization _ struct{} diff --git a/receiver/zipkinreceiver/config_test.go b/receiver/zipkinreceiver/config_test.go index 5c7ecf115fcc2..05106ae81d226 100644 --- a/receiver/zipkinreceiver/config_test.go +++ b/receiver/zipkinreceiver/config_test.go @@ -49,6 +49,15 @@ func TestLoadConfig(t *testing.T) { ParseStringTags: true, }, }, + { + id: component.NewIDWithName(metadata.Type, "disable_keep_alives"), + expected: &Config{ + ServerConfig: confighttp.ServerConfig{ + Endpoint: defaultHTTPEndpoint, + }, + DisableKeepAlives: true, + }, + }, } for _, tt := range tests { diff --git a/receiver/zipkinreceiver/testdata/config.yaml b/receiver/zipkinreceiver/testdata/config.yaml index f0ccdc3c65aed..4bf68e192902a 100644 --- a/receiver/zipkinreceiver/testdata/config.yaml +++ b/receiver/zipkinreceiver/testdata/config.yaml @@ -3,3 +3,5 @@ zipkin/customname: endpoint: "localhost:8765" zipkin/parse_strings: parse_string_tags: true +zipkin/disable_keep_alives: + disable_keep_alives: true diff --git a/receiver/zipkinreceiver/trace_receiver.go b/receiver/zipkinreceiver/trace_receiver.go index 8b864202864b1..3d6b71b2f29c1 100644 --- a/receiver/zipkinreceiver/trace_receiver.go +++ b/receiver/zipkinreceiver/trace_receiver.go @@ -100,6 +100,11 @@ func (zr *zipkinReceiver) Start(ctx context.Context, host component.Host) error return err } + // Apply keep-alive setting to the HTTP server + if zr.config.DisableKeepAlives { + zr.server.SetKeepAlivesEnabled(false) + } + var listener net.Listener listener, err = zr.config.ToListener(ctx) if err != nil { diff --git a/receiver/zipkinreceiver/trace_receiver_test.go b/receiver/zipkinreceiver/trace_receiver_test.go index dba1a5bf433e1..df4913f6e2159 100644 --- a/receiver/zipkinreceiver/trace_receiver_test.go +++ b/receiver/zipkinreceiver/trace_receiver_test.go @@ -496,3 +496,57 @@ func TestFromBytesWithNoTimestamp(t *testing.T) { assert.True(t, mapContainedKey) assert.True(t, wasAbsent.Bool()) } + +func TestKeepAliveConfig(t *testing.T) { + tests := []struct { + name string + disableKeepAlives bool + expectedEnabled bool + }{ + { + name: "keep-alive enabled by default", + disableKeepAlives: false, + expectedEnabled: true, + }, + { + name: "keep-alive disabled when configured", + disableKeepAlives: true, + expectedEnabled: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Get a random port for testing + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + addr := listener.Addr().String() + require.NoError(t, listener.Close()) + + cfg := &Config{ + ServerConfig: confighttp.ServerConfig{ + Endpoint: addr, + }, + DisableKeepAlives: tt.disableKeepAlives, + } + + sink := &consumertest.TracesSink{} + r, err := newReceiver(cfg, sink, receivertest.NewNopSettings(metadata.Type)) + require.NoError(t, err) + + require.NoError(t, r.Start(t.Context(), componenttest.NewNopHost())) + t.Cleanup(func() { + require.NoError(t, r.Shutdown(t.Context())) + }) + + // Make a request to verify the server starts successfully + body := `[{"traceId":"1","id":"2","name":"test","timestamp":1234567890123456,"duration":1000000}]` + resp, err := http.Post(fmt.Sprintf("http://%s/api/v2/spans", addr), "application/json", bytes.NewBufferString(body)) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusAccepted, resp.StatusCode) + assert.NotNil(t, r.server, "HTTP server should be initialized") + assert.Equal(t, tt.disableKeepAlives, cfg.DisableKeepAlives, "Configuration should match expected value") + }) + } +}