Skip to content

Commit b202619

Browse files
authored
Retry connection errors (#214)
1 parent abd9ad7 commit b202619

File tree

3 files changed

+45
-1
lines changed

3 files changed

+45
-1
lines changed

logfire/_internal/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
673673
endpoint=self.metrics_endpoint,
674674
headers=headers,
675675
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
676+
session=session,
676677
)
677678
)
678679
]

logfire/_internal/exporters/otlp.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import time
5+
from random import random
46
from typing import Any, Iterable, Sequence, cast
57

8+
import requests.exceptions
69
from opentelemetry.sdk.trace import ReadableSpan
710
from opentelemetry.sdk.trace.export import SpanExportResult
811
from requests import Session
@@ -38,7 +41,20 @@ def gen() -> Iterable[bytes]:
3841
yield chunk
3942

4043
request.body = gen() # type: ignore
41-
return super().send(request, **kwargs)
44+
45+
max_attempts = 7
46+
for attempt in range(max_attempts): # pragma: no branch
47+
try:
48+
response = super().send(request, **kwargs)
49+
except requests.exceptions.RequestException:
50+
if attempt < max_attempts - 1:
51+
# Exponential backoff with jitter
52+
time.sleep(2**attempt + random())
53+
continue
54+
raise
55+
return response
56+
57+
raise RuntimeError('Unreachable code') # for pyright # pragma: no cover
4258

4359
def _check_body_size(self, size: int) -> None:
4460
if size > self.max_body_size:

tests/exporters/test_otlp_session.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from typing import Any, Iterable, cast
2+
from unittest.mock import Mock
23

34
import pytest
5+
import requests.exceptions
6+
from dirty_equals import IsFloat
47
from requests.models import PreparedRequest, Response as Response
58
from requests.sessions import HTTPAdapter
69

@@ -42,3 +45,27 @@ def test_max_body_size_generator() -> None:
4245
with pytest.raises(BodyTooLargeError) as e:
4346
s.post('http://example.com', data=iter([b'abc'] * 100))
4447
assert str(e.value) == 'Request body is too large (12 bytes), must be less than 10 bytes.'
48+
49+
50+
def test_connection_error_retries(monkeypatch: pytest.MonkeyPatch) -> None:
51+
sleep_mock = Mock(return_value=0)
52+
monkeypatch.setattr('time.sleep', sleep_mock)
53+
54+
class ConnectionErrorAdapter(HTTPAdapter):
55+
def send(self, request: PreparedRequest, *args: Any, **kwargs: Any) -> Response:
56+
raise requests.exceptions.ConnectionError()
57+
58+
session = OTLPExporterHttpSession(max_body_size=10)
59+
session.mount('http://', ConnectionErrorAdapter())
60+
61+
with pytest.raises(requests.exceptions.ConnectionError):
62+
session.post('http://example.com', data='123')
63+
64+
assert [call.args for call in sleep_mock.call_args_list] == [
65+
(IsFloat(gt=1, lt=2),),
66+
(IsFloat(gt=2, lt=3),),
67+
(IsFloat(gt=4, lt=5),),
68+
(IsFloat(gt=8, lt=9),),
69+
(IsFloat(gt=16, lt=17),),
70+
(IsFloat(gt=32, lt=33),),
71+
]

0 commit comments

Comments
 (0)