Skip to content

Commit e6a36dc

Browse files
committed
bugsnag: emit slice_name from service_name in custom metadata tab
Observe error analytics filters on custom.slice_name to assign errors to a slice. Store the service_name passed to setup() and write it as custom.slice_name on every event so errors from orchestrator, api, and metrics_poller each land in their own slice. Consolidates all custom tab writes into a single add_tab call.
1 parent 035ad1f commit e6a36dc

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

cloud_pipelines_backend/instrumentation/bugsnag_instrumentation.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,19 @@
3535

3636
IS_BUGSNAG_ENABLED: bool = bool(_BUGSNAG_API_KEY)
3737
_setup_called: bool = False
38+
_service_name: str | None = None
3839

3940

4041
def _before_notify(event: bugsnag_event.Event) -> None:
4142
"""Attach contextual logging metadata to every Bugsnag event."""
4243
context = contextual_logging.get_all_context_metadata()
4344
if context:
4445
event.add_tab("tangle_context", context)
46+
47+
custom: dict[str, str] = {}
48+
if _service_name:
49+
custom["slice_name"] = _service_name
50+
4551
if _CUSTOM_GROUPING_KEY and event.original_error:
4652
# Use the full chain for grouping so that "LauncherError <- TimeoutError"
4753
# and "LauncherError <- ApiException" land in separate, stable groups.
@@ -50,7 +56,7 @@ def _before_notify(event: bugsnag_event.Event) -> None:
5056
)
5157
prefix = (event.metadata.get("extra") or {}).get("grouping_prefix")
5258
key_value = f"{prefix}: {chain}" if prefix else chain
53-
event.add_tab("custom", {_CUSTOM_GROUPING_KEY: key_value})
59+
custom[_CUSTOM_GROUPING_KEY] = key_value
5460
if prefix and event.errors:
5561
try:
5662
for error in event.errors:
@@ -74,14 +80,20 @@ def _before_notify(event: bugsnag_event.Event) -> None:
7480
"Could not set chain title on errorClass", exc_info=True
7581
)
7682

83+
if custom:
84+
event.add_tab("custom", custom)
85+
7786

7887
def setup(*, service_name: str | None = None) -> None:
7988
"""Configure the Bugsnag client.
8089
8190
No-op if TANGLE_BUGSNAG_API_KEY is not set.
8291
8392
Args:
84-
service_name: Identifies the process in Bugsnag (e.g. "tangle-api").
93+
service_name: Identifies the process in Bugsnag (e.g. "tangle-orchestrator-production").
94+
Also emitted as ``slice_name`` in the Bugsnag "custom" metadata tab on every event,
95+
so errors can be filtered by service slice. Note: ``slice_name`` is a reserved key —
96+
avoid setting ``TANGLE_BUGSNAG_CUSTOM_GROUPING_KEY=slice_name``.
8597
"""
8698
if not IS_BUGSNAG_ENABLED:
8799
return
@@ -104,8 +116,9 @@ def setup(*, service_name: str | None = None) -> None:
104116
project_root=service_name,
105117
)
106118
bugsnag_sdk.before_notify(_before_notify)
107-
global _setup_called
119+
global _setup_called, _service_name
108120
_setup_called = True
121+
_service_name = service_name
109122
except Exception:
110123
_logger.exception("Failed to initialize Bugsnag")
111124

tests/instrumentation/test_bugsnag.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,31 @@ def test_before_notify_skips_error_class_prefix_gracefully_on_bad_errors_structu
271271
bugsnag_module._before_notify(mock_event)
272272

273273

274+
def test_before_notify_sets_slice_name_when_service_name_configured(monkeypatch):
275+
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
276+
monkeypatch.setenv("TANGLE_ENV", "staging")
277+
278+
import importlib
279+
import cloud_pipelines_backend.instrumentation.bugsnag_instrumentation as bugsnag_module
280+
281+
importlib.reload(bugsnag_module)
282+
283+
from cloud_pipelines_backend.instrumentation import contextual_logging
284+
285+
contextual_logging.clear_context_metadata()
286+
287+
with mock.patch("bugsnag.configure"), mock.patch("bugsnag.before_notify"):
288+
bugsnag_module.setup(service_name="orchestrator")
289+
290+
mock_event = mock.MagicMock()
291+
mock_event.original_error = None
292+
mock_event.metadata = {}
293+
294+
bugsnag_module._before_notify(mock_event)
295+
296+
mock_event.add_tab.assert_called_once_with("custom", {"slice_name": "orchestrator"})
297+
298+
274299
def test_before_notify_skips_empty_context(monkeypatch):
275300
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
276301
monkeypatch.setenv("TANGLE_ENV", "staging")
@@ -287,3 +312,33 @@ def test_before_notify_skips_empty_context(monkeypatch):
287312
mock_event = mock.MagicMock()
288313
bugsnag_module._before_notify(mock_event)
289314
mock_event.add_tab.assert_not_called()
315+
316+
317+
def test_before_notify_omits_custom_tab_when_no_service_name_and_no_grouping_key(
318+
monkeypatch,
319+
):
320+
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
321+
monkeypatch.delenv("TANGLE_BUGSNAG_CUSTOM_GROUPING_KEY", raising=False)
322+
323+
import importlib
324+
325+
import cloud_pipelines_backend.instrumentation.bugsnag_instrumentation as bugsnag_module
326+
327+
importlib.reload(bugsnag_module)
328+
329+
from cloud_pipelines_backend.instrumentation import contextual_logging
330+
331+
contextual_logging.clear_context_metadata()
332+
333+
with mock.patch("bugsnag.configure"), mock.patch("bugsnag.before_notify"):
334+
bugsnag_module.setup(service_name=None)
335+
336+
mock_event = mock.MagicMock()
337+
mock_event.original_error = None
338+
mock_event.metadata = {}
339+
bugsnag_module._before_notify(mock_event)
340+
341+
custom_calls = [
342+
c for c in mock_event.add_tab.call_args_list if c.args[0] == "custom"
343+
]
344+
assert custom_calls == []

0 commit comments

Comments
 (0)