[exporter/opensearch] Retry transient failures instead of dropping them as permanent#49605
Conversation
…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>
Pull request dashboard statusStatus last refreshed: 2026-07-21 07:53:28 UTC.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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] |
Description
The OpenSearch exporter's bulk-indexer
OnErrorcallback (onIndexerError) wrapped every indexer-level error asconsumererror.NewPermanent. Indexer-level errors are transport/flush failures (connection refused, timeout, DNS), which are transient. Marking them permanent madeexporterhelperdrop the whole batch even withretry_on_failurefully 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 andexporterhelperresends the batch). InprocessItemFailure, a per-itemnet.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
TestOnIndexerErrorIsRetryableandTestProcessItemFailureTransportErrorRetryable(and the trace equivalents), asserting viaconsumererror.IsPermanentthat transport failures are retryable and that encoding errors remain permanent. Existing bulk-indexer tests still pass.Documentation
No documentation changes needed.