Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@

IS_BUGSNAG_ENABLED: bool = bool(_BUGSNAG_API_KEY)
_setup_called: bool = False
_service_name: str | None = None


def _before_notify(event: bugsnag_event.Event) -> None:
"""Attach contextual logging metadata to every Bugsnag event."""
context = contextual_logging.get_all_context_metadata()
if context:
event.add_tab("tangle_context", context)

custom: dict[str, str] = {}
if _service_name:
custom["slice_name"] = _service_name

if _CUSTOM_GROUPING_KEY and event.original_error:
# Use the full chain for grouping so that "LauncherError <- TimeoutError"
# and "LauncherError <- ApiException" land in separate, stable groups.
Expand All @@ -50,7 +56,7 @@ def _before_notify(event: bugsnag_event.Event) -> None:
)
prefix = (event.metadata.get("extra") or {}).get("grouping_prefix")
key_value = f"{prefix}: {chain}" if prefix else chain
event.add_tab("custom", {_CUSTOM_GROUPING_KEY: key_value})
custom[_CUSTOM_GROUPING_KEY] = key_value
Comment thread
Mbeaulne marked this conversation as resolved.
if prefix and event.errors:
try:
for error in event.errors:
Expand All @@ -74,14 +80,20 @@ def _before_notify(event: bugsnag_event.Event) -> None:
"Could not set chain title on errorClass", exc_info=True
)

if custom:
event.add_tab("custom", custom)


def setup(*, service_name: str | None = None) -> None:
"""Configure the Bugsnag client.

No-op if TANGLE_BUGSNAG_API_KEY is not set.

Args:
service_name: Identifies the process in Bugsnag (e.g. "tangle-api").
service_name: Identifies the process in Bugsnag (e.g. "tangle-orchestrator-production").
Also emitted as ``slice_name`` in the Bugsnag "custom" metadata tab on every event,
so errors can be filtered by service slice. Note: ``slice_name`` is a reserved key —
avoid setting ``TANGLE_BUGSNAG_CUSTOM_GROUPING_KEY=slice_name``.
"""
if not IS_BUGSNAG_ENABLED:
return
Expand All @@ -104,8 +116,9 @@ def setup(*, service_name: str | None = None) -> None:
project_root=service_name,
)
bugsnag_sdk.before_notify(_before_notify)
global _setup_called
global _setup_called, _service_name
_setup_called = True
_service_name = service_name
Comment thread
Mbeaulne marked this conversation as resolved.
except Exception:
_logger.exception("Failed to initialize Bugsnag")

Expand Down
55 changes: 55 additions & 0 deletions tests/instrumentation/test_bugsnag.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,31 @@ def test_before_notify_skips_error_class_prefix_gracefully_on_bad_errors_structu
bugsnag_module._before_notify(mock_event)


def test_before_notify_sets_slice_name_when_service_name_configured(monkeypatch):
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
monkeypatch.setenv("TANGLE_ENV", "staging")

import importlib
import cloud_pipelines_backend.instrumentation.bugsnag_instrumentation as bugsnag_module

importlib.reload(bugsnag_module)

from cloud_pipelines_backend.instrumentation import contextual_logging

contextual_logging.clear_context_metadata()

with mock.patch("bugsnag.configure"), mock.patch("bugsnag.before_notify"):
bugsnag_module.setup(service_name="orchestrator")

mock_event = mock.MagicMock()
mock_event.original_error = None
mock_event.metadata = {}

bugsnag_module._before_notify(mock_event)

mock_event.add_tab.assert_called_once_with("custom", {"slice_name": "orchestrator"})
Comment thread
Mbeaulne marked this conversation as resolved.


def test_before_notify_skips_empty_context(monkeypatch):
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
monkeypatch.setenv("TANGLE_ENV", "staging")
Expand All @@ -287,3 +312,33 @@ def test_before_notify_skips_empty_context(monkeypatch):
mock_event = mock.MagicMock()
bugsnag_module._before_notify(mock_event)
mock_event.add_tab.assert_not_called()


def test_before_notify_omits_custom_tab_when_no_service_name_and_no_grouping_key(
monkeypatch,
):
monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key")
monkeypatch.delenv("TANGLE_BUGSNAG_CUSTOM_GROUPING_KEY", raising=False)

import importlib

import cloud_pipelines_backend.instrumentation.bugsnag_instrumentation as bugsnag_module

importlib.reload(bugsnag_module)

from cloud_pipelines_backend.instrumentation import contextual_logging

contextual_logging.clear_context_metadata()

with mock.patch("bugsnag.configure"), mock.patch("bugsnag.before_notify"):
bugsnag_module.setup(service_name=None)

mock_event = mock.MagicMock()
mock_event.original_error = None
mock_event.metadata = {}
bugsnag_module._before_notify(mock_event)

custom_calls = [
c for c in mock_event.add_tab.call_args_list if c.args[0] == "custom"
]
assert custom_calls == []
Loading