Skip to content

[exporter/opensearch] Retry transient failures instead of dropping them as permanent#49605

Open
alliasgher wants to merge 3 commits into
open-telemetry:mainfrom
alliasgher:fix/opensearch-transient-errors-retryable
Open

[exporter/opensearch] Retry transient failures instead of dropping them as permanent#49605
alliasgher wants to merge 3 commits into
open-telemetry:mainfrom
alliasgher:fix/opensearch-transient-errors-retryable

Conversation

@alliasgher

Copy link
Copy Markdown
Contributor

Description

The OpenSearch exporter's bulk-indexer OnError callback (onIndexerError) wrapped every indexer-level error as consumererror.NewPermanent. Indexer-level errors are transport/flush failures (connection refused, timeout, DNS), which are transient. Marking them permanent made exporterhelper drop the whole batch even with retry_on_failure fully configured, so telemetry was silently lost whenever OpenSearch was briefly unreachable.

This surfaces indexer-level errors as retryable (a plain error, so joinedError() is not permanent and exporterhelper resends the batch). In processItemFailure, a per-item net.OpError (transport) is now treated as retryable as well, while encoding errors and non-recoverable OpenSearch responses (bad documents) stay permanent. The same fix is applied to both the log and trace bulk indexers.

Link to tracking issue

Fixes #49208

Testing

Added TestOnIndexerErrorIsRetryable and TestProcessItemFailureTransportErrorRetryable (and the trace equivalents), asserting via consumererror.IsPermanent that transport failures are retryable and that encoding errors remain permanent. Existing bulk-indexer tests still pass.

Documentation

No documentation changes needed.

…em as permanent

The bulk indexer's OnError callback (onIndexerError) wrapped every indexer-level
error as consumererror.NewPermanent. Transport failures (connection refused,
timeout, DNS) are indexer-level, so they were marked permanent and exporterhelper
dropped the batch even with retry_on_failure enabled, causing silent data loss.

Surface indexer-level errors as retryable so the batch is resent, and in
processItemFailure treat a per-item net.OpError (transport) as retryable while
keeping encoding and bad-document errors permanent. Same fix for logs and traces.

Fixes open-telemetry#49208

Signed-off-by: Ali <alliasgher123@gmail.com>
Satisfy the usetesting linter.

Signed-off-by: Ali <alliasgher123@gmail.com>
Now that transport failures are retryable, the generated component lifecycle
test (which sends to a nonexistent endpoint) would retry for the full
max_elapsed_time on shutdown and time out. Disable retry_on_failure in the test
config, matching how other exporters (alertmanager, awss3, datadog) configure
their lifecycle tests.

Signed-off-by: Ali <alliasgher123@gmail.com>
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 18, 2026

Copy link
Copy Markdown

Pull request dashboard status

Status last refreshed: 2026-07-21 07:53:28 UTC.

  • Waiting on: Author
  • Next step: Address or respond to 4 review feedback items:
    • Inline threads: 1, 2, 3, 4
    • For each item, link to the commit that addresses it, explain why no change is needed, or ask a follow-up question.

This automated status or its linked feedback items may be incorrect. If something looks wrong, report it with the result you expected.

// which is permanent.
var netErr *net.OpError
if errors.As(itemErr, &netErr) {
lbi.appendRetryLogError(itemErr, logs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only retries *net.OpError. Should a more exhaustive list (e.g. context.DeadlineExceeded, context.Canceled) be covered? Or, assuming we have a safe retry/back-off configuration, should we consider defaulting to retryable and reserve Permanent for errors we can positively ID as non-recoverable?

if errors.As(itemErr, &netErr) {
lbi.appendRetryLogError(itemErr, logs)
} else {
lbi.appendPermanentError(itemErr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a flush failure, doesn't opensearchutil fire both OnFailure (per item, via handleBulkError) and OnError with the same error? Since IsPermanent is errors.As over the joined error, wouldn't this Permanent branch override the retryable onIndexerError fix and drop the batch anyway? See code here and here

}
}

func TestProcessItemFailureTransportErrorRetryable(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drive a real flush via httptest and assert on the joined error, rather than hand-feeding *net.OpError? That would exercise the actual error types the transport produces. Something like:

func TestProcessItemFailureTransportRetryable(t *testing.T) {
	tests := []struct {
		name    string
		handler http.HandlerFunc // nil => point at a dead port
	}{
		{"connection refused", nil},
		{"connection drop mid-request", func(w http.ResponseWriter, _ *http.Request) {
			conn, _, _ := w.(http.Hijacker).Hijack()
			conn.Close()
		}},
		{"reset after read", func(w http.ResponseWriter, r *http.Request) {
			io.Copy(io.Discard, r.Body)
			conn, _, _ := w.(http.Hijacker).Hijack()
			if tcp, ok := conn.(*net.TCPConn); ok {
				tcp.SetLinger(0) // force RST
			}
			conn.Close()
		}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			endpoint := "http://127.0.0.1:1" // dead port
			if tt.handler != nil {
				srv := httptest.NewServer(tt.handler)
				defer srv.Close()
				endpoint = srv.URL
			}
			client, _ := newOpenSearchClient(endpoint, http.DefaultClient, zap.NewNop())
			idx := newLogBulkIndexer("create", model, "")
			require.NoError(t, idx.start(client))
			idx.submit(context.Background(), logs, ir, cfg, time.Now())
			idx.close(context.Background())

			err := idx.joinedError()
			require.Error(t, err)
			require.False(t, consumererror.IsPermanent(err),
				"transient flush failure must be retryable, else retry_on_failure drops the batch")
		})
	}
}

note: Retry transient OpenSearch failures instead of dropping them as permanent errors

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [49208]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it also fixes #45147 and is closely related to #45120. Worth adding them to issues: so they get closed/linked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exporter/opensearchexporter: Silent data loss — transient errors incorrectly classified as Permanent in log and trace bulk indexers

2 participants