Skip to content

Commit 4e77c78

Browse files
devin-ai-integration[bot]bot_apk
andcommitted
feat(cdk): add sentry_sdk.capture_message() to MemoryMonitor for high-memory warnings
Add Sentry capture_message() call when memory usage exceeds 90% threshold. The Sentry alert fires once per sync to reduce noise. Uses a top-level try/except ImportError guard so environments without sentry_sdk degrade gracefully. Resolves airbytehq/airbyte-internal-issues#16092 Co-Authored-By: bot_apk <apk@cognition.ai>
1 parent 0e57414 commit 4e77c78

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

airbyte_cdk/utils/memory_monitor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from pathlib import Path
99
from typing import Optional
1010

11+
try:
12+
import sentry_sdk
13+
except ImportError:
14+
sentry_sdk = None # type: ignore[assignment]
15+
1116
logger = logging.getLogger("airbyte")
1217

1318
# cgroup v2 paths
@@ -47,6 +52,7 @@ def __init__(
4752
self._message_count = 0
4853
self._cgroup_version: Optional[int] = None
4954
self._probed = False
55+
self._sentry_alerted = False
5056

5157
def _probe_cgroup(self) -> None:
5258
"""Detect which cgroup version (if any) is available.
@@ -138,3 +144,10 @@ def check_memory_usage(self) -> None:
138144
usage_gb,
139145
limit_gb,
140146
)
147+
if not self._sentry_alerted and sentry_sdk is not None:
148+
self._sentry_alerted = True
149+
sentry_sdk.capture_message(
150+
"Source memory usage at %d%% of container limit (%.2f / %.2f GB)."
151+
% (usage_percent, usage_gb, limit_gb),
152+
level="warning",
153+
)

unit_tests/utils/test_memory_monitor.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from pathlib import Path
7-
from unittest.mock import patch
7+
from unittest.mock import MagicMock, patch
88

99
import pytest
1010

@@ -245,3 +245,74 @@ def mock_read_text(self: Path) -> str:
245245
):
246246
monitor.check_memory_usage()
247247
assert not caplog.records
248+
249+
250+
# ---------------------------------------------------------------------------
251+
# check_memory_usage — Sentry capture_message
252+
# ---------------------------------------------------------------------------
253+
254+
255+
def test_sentry_capture_message_called_on_high_memory() -> None:
256+
"""sentry_sdk.capture_message() should be called once when memory exceeds 90%."""
257+
mock_capture = MagicMock()
258+
monitor = MemoryMonitor(check_interval=1)
259+
with (
260+
patch.object(Path, "exists", _v2_exists),
261+
patch.object(Path, "read_text", _v2_mock_read(usage=_MOCK_USAGE_AT_90)),
262+
patch("airbyte_cdk.utils.memory_monitor.sentry_sdk") as mock_sentry,
263+
):
264+
mock_sentry.capture_message = mock_capture
265+
monitor.check_memory_usage()
266+
267+
mock_capture.assert_called_once()
268+
call_args = mock_capture.call_args
269+
assert "91%" in call_args[0][0]
270+
assert call_args[1]["level"] == "warning"
271+
272+
273+
def test_sentry_capture_message_only_once_per_sync() -> None:
274+
"""sentry_sdk.capture_message() should fire only once even if memory stays high."""
275+
mock_capture = MagicMock()
276+
monitor = MemoryMonitor(check_interval=1)
277+
with (
278+
patch.object(Path, "exists", _v2_exists),
279+
patch.object(Path, "read_text", _v2_mock_read(usage=_MOCK_USAGE_AT_90)),
280+
patch("airbyte_cdk.utils.memory_monitor.sentry_sdk") as mock_sentry,
281+
):
282+
mock_sentry.capture_message = mock_capture
283+
monitor.check_memory_usage()
284+
monitor.check_memory_usage()
285+
monitor.check_memory_usage()
286+
287+
mock_capture.assert_called_once()
288+
289+
290+
def test_sentry_not_called_below_threshold() -> None:
291+
"""sentry_sdk.capture_message() should not be called when memory is below 90%."""
292+
mock_capture = MagicMock()
293+
monitor = MemoryMonitor(check_interval=1)
294+
with (
295+
patch.object(Path, "exists", _v2_exists),
296+
patch.object(Path, "read_text", _v2_mock_read(usage=_MOCK_USAGE_BELOW)),
297+
patch("airbyte_cdk.utils.memory_monitor.sentry_sdk") as mock_sentry,
298+
):
299+
mock_sentry.capture_message = mock_capture
300+
monitor.check_memory_usage()
301+
302+
mock_capture.assert_not_called()
303+
304+
305+
def test_sentry_unavailable_degrades_gracefully(caplog: pytest.LogCaptureFixture) -> None:
306+
"""When sentry_sdk is None (not installed), warning log should still be emitted."""
307+
monitor = MemoryMonitor(check_interval=1)
308+
with (
309+
caplog.at_level(logging.WARNING, logger="airbyte"),
310+
patch.object(Path, "exists", _v2_exists),
311+
patch.object(Path, "read_text", _v2_mock_read(usage=_MOCK_USAGE_AT_90)),
312+
patch("airbyte_cdk.utils.memory_monitor.sentry_sdk", None),
313+
):
314+
monitor.check_memory_usage()
315+
316+
# Warning log should still be emitted even when sentry_sdk is unavailable
317+
assert len(caplog.records) == 1
318+
assert "91%" in caplog.records[0].message

0 commit comments

Comments
 (0)