Skip to content

Commit 01ca818

Browse files
andrzej-stencelcmacknz
authored andcommitted
fix(otelconsumer): do not retry 401 errors from Elasticsearch (elastic#50261)
* fix(otelconsumer): treat 401 errors from Elasticsearch as permanent * refactor: prevent tight coupling with docappender library I'm not sure if it makes things better (less coupling) or worse (less explicit). * libbeat: mage check --------- Co-authored-by: Craig MacKenzie <craig.mackenzie@elastic.co>
1 parent 7f15130 commit 01ca818

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

libbeat/otel/otelconsumer/otelconsumer.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"errors"
2323
"fmt"
24+
"net/http"
2425
"os"
2526
"runtime"
2627
"time"
@@ -47,6 +48,12 @@ const (
4748
esDocumentIDAttribute = "elasticsearch.document_id"
4849
)
4950

51+
// statusCodeError is satisfied by errors that carry an HTTP status code,
52+
// such as docappender.ErrorFlushFailed errors returned from the OTelCol Elasticsearch exporter.
53+
type statusCodeError interface {
54+
StatusCode() int
55+
}
56+
5057
type otelConsumer struct {
5158
observer outputs.Observer
5259
logsConsumer consumer.Logs
@@ -193,13 +200,20 @@ func (out *otelConsumer) logsPublish(ctx context.Context, batch publisher.Batch)
193200

194201
err := out.logsConsumer.ConsumeLogs(otelctx.NewConsumerContext(ctx, out.beatInfo), pLogs)
195202
if err != nil {
203+
// Work around the fact that Elasticsearch exporter returns 401 as a non-permanent error.
204+
isAuthorizationError := false
205+
var statusErr statusCodeError
206+
if errors.As(err, &statusErr) {
207+
isAuthorizationError = statusErr.StatusCode() == http.StatusUnauthorized
208+
}
209+
196210
// Permanent errors shouldn't be retried. This tipically means
197211
// the data cannot be serialized by the exporter that is attached
198212
// to the pipeline or when the destination refuses the data because
199213
// it cannot decode it. Retrying in this case is useless.
200214
//
201215
// See https://github.com/open-telemetry/opentelemetry-collector/blob/1c47d89/receiver/doc.go#L23-L40
202-
if consumererror.IsPermanent(err) {
216+
if consumererror.IsPermanent(err) || isAuthorizationError {
203217
st.PermanentErrors(len(events))
204218
batch.Drop()
205219
} else {

libbeat/otel/otelconsumer/otelconsumer_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,32 @@ func TestPublish(t *testing.T) {
240240
assert.Equal(t, outest.BatchRetry, batch.Signals[0].Tag)
241241
})
242242

243+
t.Run("drops batch on 401 Unauthorized error", func(t *testing.T) {
244+
batch := outest.NewBatch(event1, event2, event3)
245+
246+
otelConsumer := makeOtelConsumer(t, func(ctx context.Context, ld plog.Logs) error {
247+
return &testStatusCodeError{statusCode: 401, msg: "flush failed (401): unauthorized"}
248+
})
249+
250+
err := otelConsumer.Publish(ctx, batch)
251+
assert.NoError(t, err)
252+
assert.Len(t, batch.Signals, 1)
253+
assert.Equal(t, outest.BatchDrop, batch.Signals[0].Tag)
254+
})
255+
256+
t.Run("retries batch on non-401 status code error", func(t *testing.T) {
257+
batch := outest.NewBatch(event1, event2, event3)
258+
259+
otelConsumer := makeOtelConsumer(t, func(ctx context.Context, ld plog.Logs) error {
260+
return &testStatusCodeError{statusCode: 500, msg: "flush failed (500): internal server error"}
261+
})
262+
263+
err := otelConsumer.Publish(ctx, batch)
264+
assert.NoError(t, err)
265+
assert.Len(t, batch.Signals, 1)
266+
assert.Equal(t, outest.BatchRetry, batch.Signals[0].Tag)
267+
})
268+
243269
t.Run("sets the elasticsearchexporter doc id attribute from metadata", func(t *testing.T) {
244270
batch := outest.NewBatch(event4)
245271

@@ -364,3 +390,11 @@ func checkEventsActive(reg *monitoring.Registry) int64 {
364390
outputSnapshot := monitoring.CollectFlatSnapshot(reg, monitoring.Full, true)
365391
return outputSnapshot.Ints["events.active"]
366392
}
393+
394+
type testStatusCodeError struct {
395+
statusCode int
396+
msg string
397+
}
398+
399+
func (e *testStatusCodeError) Error() string { return e.msg }
400+
func (e *testStatusCodeError) StatusCode() int { return e.statusCode }

0 commit comments

Comments
 (0)