From 328a7d56847226ba52370342f4703ad6b0b5a37e Mon Sep 17 00:00:00 2001 From: Daniel Sanz <13658011+sdn4z@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:40:41 +0200 Subject: [PATCH] feat: add Sentry integration --- README.md | 7 ++++ justfile | 6 ++-- pyproject.toml | 1 + src/lightman_ai/cli.py | 2 ++ src/lightman_ai/core/sentry.py | 28 ++++++++++++++++ tests/test_main.py | 60 ++++++++++++++++++++++++++++++++++ uv.lock | 17 +++++++++- 7 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/lightman_ai/core/sentry.py diff --git a/README.md b/README.md index b080478..311eb5e 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,13 @@ This approach ensures that performance metrics reflect real-world usage scenario **Make sure to fill in the `RELEVANT_ARTICLES` with the ones you classify as relevant, so that you can compare the accuracy after running the `eval` script.*** +## Sentry + +- The application will automatically pick up and use environment variables if they are present in your environment or `.env` file. +- To enable Sentry error monitoring, set the `SENTRY_DSN` environment variable. This is **mandatory** for Sentry to be enabled. If `SENTRY_DSN` is not set, Sentry will be skipped and the application will run normally. +- If Sentry fails to initialize for any reason (e.g., network issues, invalid DSN), the application will log a warning and continue execution without error monitoring. +- Sentry is **optional**: the application does not require it to function, and all features will work even if Sentry is not configured or fails to start. + ## 📄 License This project is licensed under the [MIT License](LICENSE) - see the LICENSE file for details. diff --git a/justfile b/justfile index c99e306..f0370ee 100644 --- a/justfile +++ b/justfile @@ -40,9 +40,9 @@ test-all: venv # Format all code in the project. format: venv - {{ run }} ruff check {{ target_dirs }} + {{ run }} ruff format {{ target_dirs }} # Lint all code in the project. lint: venv - {{ run }} ruff check {{ target_dirs }} - {{ run }} mypy {{ target_dirs }} + {{ run }} ruff check {{ target_dirs }} + {{ run }} mypy {{ target_dirs }} diff --git a/pyproject.toml b/pyproject.toml index 0efbc09..c30e5a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "ruff>=0.12.4", "pydantic-ai-slim[google,openai]>=0.4.4", "logfire>=3.25.0", + "sentry-sdk>=2.21.0,<3.0.0", ] name = "lightman_ai" version = "0.17.0" diff --git a/src/lightman_ai/cli.py b/src/lightman_ai/cli.py index 200e93d..edc8ed7 100644 --- a/src/lightman_ai/cli.py +++ b/src/lightman_ai/cli.py @@ -6,6 +6,7 @@ from lightman_ai.constants import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_SECTION, DEFAULT_ENV_FILE from lightman_ai.core.config import FileConfig, FinalConfig, PromptConfig from lightman_ai.core.exceptions import ConfigNotFoundError, InvalidConfigError, PromptNotFoundError +from lightman_ai.core.sentry import configure_sentry from lightman_ai.main import lightman logger = logging.getLogger("lightman") @@ -74,6 +75,7 @@ def run( Holds no logic. It calls the main method and returns 0 when succesful . """ load_dotenv(env_file) + configure_sentry() try: prompt_config = PromptConfig.get_config_from_file(path=prompt_file) config_from_file = FileConfig.get_config_from_file(config_section=config, path=config_file) diff --git a/src/lightman_ai/core/sentry.py b/src/lightman_ai/core/sentry.py new file mode 100644 index 0000000..9af99e5 --- /dev/null +++ b/src/lightman_ai/core/sentry.py @@ -0,0 +1,28 @@ +import logging +import os +from importlib import metadata + +import sentry_sdk +from sentry_sdk.integrations.logging import LoggingIntegration + + +def configure_sentry() -> None: + """Configure Sentry for error tracking and performance monitoring using env vars with fallbacks.""" + try: + if not os.getenv("SENTRY_DSN"): + logging.getLogger("lightman").info("SENTRY_DSN not configured, skipping Sentry initialization") + return + + # Logging level from ENV + logging_level_str = os.getenv("LOGGING_LEVEL", "ERROR").upper() + logging_level = getattr(logging, logging_level_str, logging.ERROR) + + # Set up logging integration + sentry_logging = LoggingIntegration(level=logging.INFO, event_level=logging_level) + + sentry_sdk.init( + release=metadata.version("lightman-ai"), + integrations=[sentry_logging], + ) + except Exception as e: + logging.getLogger("lightman").warning("Could not instantiate Sentry! %s.\nContinuing with the execution.", e) diff --git a/tests/test_main.py b/tests/test_main.py index 8726180..a69b204 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,6 +4,7 @@ import pytest from lightman_ai.article.models import SelectedArticle, SelectedArticlesList +from lightman_ai.core.sentry import configure_sentry from lightman_ai.main import _create_service_desk_issues, lightman from tests.utils import patch_agent @@ -174,3 +175,62 @@ def test_create_service_desk_issues_all_failures( "Could not create ServiceDesk issue: New Attack Vector Discovered, https://example.com/article2" in caplog.text ) + + +class TestSentryIntegration: + """Tests for Sentry integration behavior.""" + + @patch.dict("os.environ", {}, clear=True) # Clear all env vars + def test_sentry_skipped_when_dsn_not_set(self, caplog: pytest.LogCaptureFixture) -> None: + """Test that Sentry initialization is skipped when SENTRY_DSN is not set.""" + with caplog.at_level(logging.INFO): + configure_sentry() + + # Should log that Sentry is skipped + assert "SENTRY_DSN not configured, skipping Sentry initialization" in caplog.text + + @patch.dict("os.environ", {"SENTRY_DSN": "https://test@sentry.io/123"}) + @patch("lightman_ai.core.sentry.sentry_sdk.init") + def test_sentry_execution_continues_when_init_fails( + self, mock_sentry_init: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + """Test that execution continues when Sentry initialization fails.""" + # Make sentry_sdk.init raise an exception + mock_sentry_init.side_effect = Exception("Sentry connection failed") + + with caplog.at_level(logging.WARNING): + # This should not raise an exception + configure_sentry() + + # Should log the warning and continue + assert "Could not instantiate Sentry! Sentry connection failed" in caplog.text + assert "Continuing with the execution" in caplog.text + + # Verify that sentry_sdk.init was called (and failed) + mock_sentry_init.assert_called_once() + + @patch.dict("os.environ", {"SENTRY_DSN": "https://test@sentry.io/123"}) + @patch("lightman_ai.core.sentry.sentry_sdk.init") + @patch("lightman_ai.core.sentry.metadata.version") + def test_sentry_initializes_successfully( + self, mock_version: Mock, mock_sentry_init: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + """Test that Sentry initializes successfully when configured properly.""" + # Mock the version lookup + mock_version.return_value = "1.0.0" + + with caplog.at_level(logging.INFO): + configure_sentry() + + # Should not log any warnings or errors + assert "Could not instantiate Sentry" not in caplog.text + assert "SENTRY_DSN not configured" not in caplog.text + + # Verify that sentry_sdk.init was called with expected parameters + mock_sentry_init.assert_called_once() + call_kwargs = mock_sentry_init.call_args.kwargs + assert "release" in call_kwargs + assert "integrations" in call_kwargs + + # Verify version was looked up + mock_version.assert_called_once_with("lightman-ai") diff --git a/uv.lock b/uv.lock index 4c5543a..1af0fd9 100644 --- a/uv.lock +++ b/uv.lock @@ -487,7 +487,7 @@ wheels = [ [[package]] name = "lightman-ai" -version = "0.16.13" +version = "0.17.0" source = { editable = "." } dependencies = [ { name = "click" }, @@ -497,6 +497,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "python-dotenv" }, { name = "ruff" }, + { name = "sentry-sdk" }, { name = "stamina" }, { name = "tiktoken" }, { name = "tomlkit" }, @@ -539,6 +540,7 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.1.1,<2.0.0" }, { name = "ruff", specifier = ">=0.12.4" }, { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.11.0,<1.0.0" }, + { name = "sentry-sdk", specifier = ">=2.21.0,<3.0.0" }, { name = "stamina", specifier = ">=25.1.0,<26.0.0" }, { name = "tiktoken", specifier = ">=0.9.0,<1.0.0" }, { name = "tomlkit", specifier = ">=0.13.3,<1.0.0" }, @@ -1212,6 +1214,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" }, ] +[[package]] +name = "sentry-sdk" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/82/dfe4a91fd38e048fbb55ca6c072710408e8802015aa27cde18e8684bb1e9/sentry_sdk-2.33.2.tar.gz", hash = "sha256:e85002234b7b8efac9b74c2d91dbd4f8f3970dc28da8798e39530e65cb740f94", size = 335804, upload-time = "2025-07-22T10:41:18.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/4d825d5eb6e924dfcc6a91c8185578a7b0a5c41fd2416a6f49c8226d6ef9/sentry_sdk-2.33.2-py2.py3-none-any.whl", hash = "sha256:8d57a3b4861b243aa9d558fda75509ad487db14f488cbdb6c78c614979d77632", size = 356692, upload-time = "2025-07-22T10:41:16.531Z" }, +] + [[package]] name = "sniffio" version = "1.3.1"