From 43a7d090959c6be4191bb8a17efaef9e52fe329f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9=20Delion?= Date: Fri, 31 Oct 2025 16:36:25 +0100 Subject: [PATCH] chore(observability): Integrates with Sentry --- .env.example | 9 + DEVELOPMENT.md | 39 +++++ packages/developer_mcp_server/README.md | 51 ++++++ packages/developer_mcp_server/pyproject.toml | 7 +- .../src/developer_mcp_server/server.py | 8 + packages/gg_api_core/pyproject.toml | 7 +- .../src/gg_api_core/sentry_integration.py | 161 ++++++++++++++++++ packages/secops_mcp_server/README.md | 68 ++++++++ packages/secops_mcp_server/pyproject.toml | 7 +- .../src/secops_mcp_server/server.py | 8 + uv.lock | 31 ++++ 11 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 packages/gg_api_core/src/gg_api_core/sentry_integration.py diff --git a/.env.example b/.env.example index 3741b09..a6a2794 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,12 @@ GITGUARDIAN_URL=https://dashboard.gitguardian.com # Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) LOG_LEVEL=INFO + +# Optional: Sentry Integration for Error Tracking +# Sentry provides error tracking and performance monitoring +# Install with: pip install 'secops-mcp-server[sentry]' +# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +# SENTRY_ENVIRONMENT=production +# SENTRY_RELEASE=1.0.0 +# SENTRY_TRACES_SAMPLE_RATE=0.1 +# SENTRY_PROFILES_SAMPLE_RATE=0.1 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2c35b02..34a283f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -172,6 +172,45 @@ For all transport modes, you can provide a PAT via environment variable: GITGUARDIAN_PERSONAL_ACCESS_TOKEN= developer-mcp-server ``` +## Optional Dependencies + +The project supports optional dependencies (extras) for additional features: + +### Installing Optional Dependencies + +```bash +# Install with specific extras during development +uv sync --extra sentry + +# Install all optional dependencies +uv sync --all-extras + +# Add an optional dependency to the project +uv add --optional sentry sentry-sdk +``` + +### Using Optional Dependencies with uvx + +When running the server with `uvx` from Git, you can include optional dependencies: + +```bash +# Include extras using the #egg syntax +uvx --from 'git+https://github.com/GitGuardian/ggmcp.git@main#egg=secops-mcp-server[sentry]' secops-mcp-server + +# Or install the optional dependency separately +uv pip install sentry-sdk +uvx --from git+https://github.com/GitGuardian/ggmcp.git@main secops-mcp-server +``` + +### Current Optional Dependencies + +- **sentry**: Adds Sentry SDK for error tracking and performance monitoring + - Core package: `gg-api-core[sentry]` + - Available in: `developer-mcp-server[sentry]`, `secops-mcp-server[sentry]` + - Implementation: `gg_api_core/src/gg_api_core/sentry_integration.py` + - Used for: Production error monitoring and alerting + - See individual package READMEs for configuration details + ## Testing Run tests using uv (OAuth is disabled by default in tests): diff --git a/packages/developer_mcp_server/README.md b/packages/developer_mcp_server/README.md index 6d22acf..8bdcb1f 100644 --- a/packages/developer_mcp_server/README.md +++ b/packages/developer_mcp_server/README.md @@ -44,6 +44,11 @@ Note: Honeytoken scopes are omitted for self-hosted instances as they require th |----------|-------------|---------| | `GITGUARDIAN_URL` | GitGuardian base URL | `https://dashboard.gitguardian.com` (SaaS US), `https://dashboard.eu1.gitguardian.com` (SaaS EU), `https://dashboard.gitguardian.mycorp.local` (Self-Hosted) | | `GITGUARDIAN_SCOPES` | Comma-separated list of OAuth scopes | Auto-detected based on instance type | +| `SENTRY_DSN` | Sentry Data Source Name for error tracking (optional) | None | +| `SENTRY_ENVIRONMENT` | Environment name for Sentry (optional) | `production` | +| `SENTRY_RELEASE` | Release version or commit SHA for Sentry (optional) | None | +| `SENTRY_TRACES_SAMPLE_RATE` | Performance traces sampling rate 0.0-1.0 (optional) | `0.1` | +| `SENTRY_PROFILES_SAMPLE_RATE` | Profiling sampling rate 0.0-1.0 (optional) | `0.1` | **OAuth Callback Server**: The OAuth authentication flow uses a local callback server on port range 29170-29998 (same as ggshield). This ensures compatibility with self-hosted GitGuardian instances where the `ggshield_oauth` client is pre-configured with these redirect URIs. @@ -53,6 +58,52 @@ Note: Honeytoken scopes are omitted for self-hosted instances as they require th To override auto-detection, set `GITGUARDIAN_SCOPES` explicitly in your MCP configuration. +## Optional Integrations + +### Sentry Error Tracking + +The MCP server supports optional Sentry integration for error tracking and performance monitoring. This is completely optional and designed to avoid vendor lock-in. + +**Installation:** + +```bash +# Install with pip +pip install 'developer-mcp-server[sentry]' + +# Install with uv (in a project) +uv add 'developer-mcp-server[sentry]' + +# Run with uvx (from Git) +uvx --from 'git+https://github.com/GitGuardian/ggmcp.git@main#egg=developer-mcp-server[sentry]' developer-mcp-server + +# Or install Sentry SDK separately (works with any installation method) +pip install sentry-sdk>=2.0.0 +uv pip install sentry-sdk>=2.0.0 +``` + +**Configuration:** + +Set the `SENTRY_DSN` environment variable to enable Sentry: + +```bash +export SENTRY_DSN="https://your-key@sentry.io/project-id" +export SENTRY_ENVIRONMENT="development" +export SENTRY_RELEASE="1.0.0" + +# Then run the server as usual +developer-mcp-server +``` + +**Features:** + +- Automatic exception tracking +- Performance monitoring with configurable sampling +- Logging integration (INFO+ as breadcrumbs, ERROR+ as events) +- Optional profiling support +- Privacy-focused (PII not sent by default) + +If `SENTRY_DSN` is not set, the server runs normally without any error tracking overhead. + ## Honeytoken Management The server provides functions to create and manage honeytokens, which are fake credentials that can be used to detect unauthorized access to your systems. diff --git a/packages/developer_mcp_server/pyproject.toml b/packages/developer_mcp_server/pyproject.toml index 9f813a0..2e93470 100644 --- a/packages/developer_mcp_server/pyproject.toml +++ b/packages/developer_mcp_server/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] readme = "README.md" requires-python = ">=3.11" +license = {text = "MIT"} dependencies = [ "fastmcp>=2.0.0", "httpx>=0.24.0", @@ -29,7 +30,11 @@ dependencies = [ "gg-api-core", "uvicorn>=0.27.0" ] -license = {text = "MIT"} + +[project.optional-dependencies] +sentry = [ + "gg-api-core[sentry]", +] [project.scripts] diff --git a/packages/developer_mcp_server/src/developer_mcp_server/server.py b/packages/developer_mcp_server/src/developer_mcp_server/server.py index 77f3760..5bb5492 100644 --- a/packages/developer_mcp_server/src/developer_mcp_server/server.py +++ b/packages/developer_mcp_server/src/developer_mcp_server/server.py @@ -5,6 +5,7 @@ from gg_api_core.mcp_server import get_mcp_server from gg_api_core.scopes import set_developer_scopes +from gg_api_core.sentry_integration import init_sentry from developer_mcp_server.register_tools import DEVELOPER_INSTRUCTIONS, register_developer_tools @@ -29,6 +30,13 @@ def run_mcp_server(): logger.info("Starting Developer MCP server...") + # Initialize Sentry if configured (optional) + sentry_enabled = init_sentry() + if sentry_enabled: + logger.info("Sentry monitoring is enabled") + else: + logger.debug("Sentry monitoring is not configured") + # Check if HTTP/SSE transport is requested via environment variables mcp_port = os.environ.get("MCP_PORT") mcp_host = os.environ.get("MCP_HOST", "127.0.0.1") diff --git a/packages/gg_api_core/pyproject.toml b/packages/gg_api_core/pyproject.toml index 4f8196b..187a514 100644 --- a/packages/gg_api_core/pyproject.toml +++ b/packages/gg_api_core/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] readme = "README.md" requires-python = ">=3.11" +license = {text = "MIT"} dependencies = [ "httpx>=0.28.1", "fastmcp>=2.0.0", @@ -29,7 +30,11 @@ dependencies = [ "pydantic-settings>=2.0.0", "jinja2>=3.1.0", ] -license = {text = "MIT"} + +[project.optional-dependencies] +sentry = [ + "sentry-sdk>=2.0.0", +] [build-system] requires = ["hatchling"] diff --git a/packages/gg_api_core/src/gg_api_core/sentry_integration.py b/packages/gg_api_core/src/gg_api_core/sentry_integration.py new file mode 100644 index 0000000..0322f7f --- /dev/null +++ b/packages/gg_api_core/src/gg_api_core/sentry_integration.py @@ -0,0 +1,161 @@ +"""Optional Sentry integration for error tracking and monitoring. + +This module provides optional Sentry instrumentation that can be enabled +via environment variables. It's designed to be non-invasive and vendor-neutral, +allowing users to opt-in to Sentry monitoring without forcing a dependency. + +Environment Variables: + SENTRY_DSN: Sentry Data Source Name (required to enable Sentry) + SENTRY_ENVIRONMENT: Environment name (e.g., production, development) + SENTRY_RELEASE: Release version or commit SHA + SENTRY_TRACES_SAMPLE_RATE: Sampling rate for performance traces (0.0 to 1.0) + SENTRY_PROFILES_SAMPLE_RATE: Sampling rate for profiling (0.0 to 1.0) +""" + +import logging +import os +from typing import Any + +logger = logging.getLogger(__name__) + + +def init_sentry() -> bool: + """ + Initialize Sentry SDK if configured via environment variables. + + This function attempts to import and configure Sentry SDK only if + SENTRY_DSN is provided. It gracefully handles missing sentry-sdk + installation and logs appropriate messages. + + Returns: + bool: True if Sentry was successfully initialized, False otherwise + + Example: + >>> import os + >>> os.environ["SENTRY_DSN"] = "https://..." + >>> init_sentry() + True + """ + dsn = os.environ.get("SENTRY_DSN") + + if not dsn: + logger.debug("SENTRY_DSN not configured, skipping Sentry initialization") + return False + + try: + import sentry_sdk + from sentry_sdk.integrations.logging import LoggingIntegration + except ImportError: + logger.warning("Sentry SDK not installed") + return False + + # Get optional configuration from environment + environment = os.environ.get("SENTRY_ENVIRONMENT", "production") + release = os.environ.get("SENTRY_RELEASE") + traces_sample_rate = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.1")) + profiles_sample_rate = float(os.environ.get("SENTRY_PROFILES_SAMPLE_RATE", "0.1")) + + # Configure logging integration + logging_integration = LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.ERROR, # Send errors as events + ) + + try: + sentry_sdk.init( + dsn=dsn, + environment=environment, + release=release, + traces_sample_rate=traces_sample_rate, + profiles_sample_rate=profiles_sample_rate, + integrations=[logging_integration], + # Automatically capture unhandled exceptions + send_default_pii=False, # Don't send personally identifiable information by default + ) + + logger.info( + f"Sentry initialized successfully for environment: {environment}" + + (f", release: {release}" if release else "") + ) + return True + + except Exception as e: + logger.error(f"Failed to initialize Sentry: {str(e)}") + return False + + +def set_sentry_context(key: str, value: Any) -> None: + """ + Set additional context for Sentry error reporting. + + This is a convenience wrapper that safely sets context even if + Sentry is not initialized. + + Args: + key: Context key (e.g., "user", "workspace", "api_token") + value: Context value (can be dict, string, etc.) + + Example: + >>> set_sentry_context("workspace", {"id": "123", "name": "acme"}) + """ + try: + import sentry_sdk + + sentry_sdk.set_context(key, value) + except ImportError: + # Sentry not installed, silently skip + pass + except Exception as e: + logger.debug(f"Failed to set Sentry context: {str(e)}") + + +def set_sentry_user(user_info: dict[str, Any]) -> None: + """ + Set user information for Sentry error reporting. + + This is a convenience wrapper that safely sets user info even if + Sentry is not initialized. + + Args: + user_info: Dictionary with user information (id, email, username, etc.) + + Example: + >>> set_sentry_user({"id": "123", "email": "user@example.com"}) + """ + try: + import sentry_sdk + + sentry_sdk.set_user(user_info) + except ImportError: + # Sentry not installed, silently skip + pass + except Exception as e: + logger.debug(f"Failed to set Sentry user: {str(e)}") + + +def capture_exception(exception: Exception, **kwargs) -> None: + """ + Manually capture an exception to Sentry. + + This is useful for handled exceptions that you still want to track. + + Args: + exception: The exception to capture + **kwargs: Additional context to attach to the event + + Example: + >>> try: + ... risky_operation() + ... except ValueError as e: + ... capture_exception(e, extra={"operation": "risky_operation"}) + ... handle_error(e) + """ + try: + import sentry_sdk + + sentry_sdk.capture_exception(exception, **kwargs) + except ImportError: + # Sentry not installed, silently skip + pass + except Exception as e: + logger.debug(f"Failed to capture exception in Sentry: {str(e)}") diff --git a/packages/secops_mcp_server/README.md b/packages/secops_mcp_server/README.md index a1f97d8..1239b4a 100644 --- a/packages/secops_mcp_server/README.md +++ b/packages/secops_mcp_server/README.md @@ -48,6 +48,11 @@ Note: Extended scopes (honeytokens, audit logs, etc.) are omitted for self-hoste |----------|-------------|---------| | `GITGUARDIAN_URL` | GitGuardian base URL | `https://dashboard.gitguardian.com` (SaaS US), `https://dashboard.eu1.gitguardian.com` (SaaS EU), `https://dashboard.gitguardian.mycorp.local` (Self-Hosted) | | `GITGUARDIAN_SCOPES` | Comma-separated list of OAuth scopes | Auto-detected based on instance type | +| `SENTRY_DSN` | Sentry Data Source Name for error tracking (optional) | None | +| `SENTRY_ENVIRONMENT` | Environment name for Sentry (optional) | `production` | +| `SENTRY_RELEASE` | Release version or commit SHA for Sentry (optional) | None | +| `SENTRY_TRACES_SAMPLE_RATE` | Performance traces sampling rate 0.0-1.0 (optional) | `0.1` | +| `SENTRY_PROFILES_SAMPLE_RATE` | Profiling sampling rate 0.0-1.0 (optional) | `0.1` | **OAuth Callback Server**: The OAuth authentication flow uses a local callback server on port range 29170-29998 (same as ggshield). This ensures compatibility with self-hosted GitGuardian instances where the `ggshield_oauth` client is pre-configured with these redirect URIs. @@ -56,3 +61,66 @@ Note: Extended scopes (honeytokens, audit logs, etc.) are omitted for self-hoste - **Self-hosted instances**: `scan,incidents:read,sources:read` (honeytokens omitted to avoid permission issues) To override auto-detection, set `GITGUARDIAN_SCOPES` explicitly in your MCP configuration. + +## Optional Integrations + +### Sentry Error Tracking + +The MCP server supports optional Sentry integration for error tracking and performance monitoring. This is completely optional and designed to avoid vendor lock-in. + +**Installation:** + +```bash +# Install with pip +pip install 'secops-mcp-server[sentry]' + +# Install with uv (in a project) +uv add 'secops-mcp-server[sentry]' + +# Run with uvx (from Git) +uvx --from 'secops-mcp-server[sentry]' --from 'git+https://github.com/GitGuardian/ggmcp.git@main' secops-mcp-server + +# Or install Sentry SDK separately (works with any installation method) +pip install sentry-sdk>=2.0.0 +uv pip install sentry-sdk>=2.0.0 +``` + +**Configuration:** + +Set the `SENTRY_DSN` environment variable to enable Sentry: + +```bash +export SENTRY_DSN="https://your-key@sentry.io/project-id" +export SENTRY_ENVIRONMENT="production" +export SENTRY_RELEASE="1.0.0" + +# Then run the server as usual +secops-mcp-server +# or +uvx --from git+https://github.com/GitGuardian/ggmcp.git@main secops-mcp-server +``` + +**Note:** If you're using `uvx` and want Sentry support, you have two options: + +1. **Include the extra in the --from option:** + ```bash + uvx --from 'git+https://github.com/GitGuardian/ggmcp.git@main#egg=secops-mcp-server[sentry]' secops-mcp-server + ``` + +2. **Install sentry-sdk in the same environment** (simpler approach): + ```bash + # First, ensure sentry-sdk is available + uv pip install sentry-sdk + # Then run the server + uvx --from git+https://github.com/GitGuardian/ggmcp.git@main secops-mcp-server + ``` + +**Features:** + +- Automatic exception tracking +- Performance monitoring with configurable sampling +- Logging integration (INFO+ as breadcrumbs, ERROR+ as events) +- Optional profiling support +- Privacy-focused (PII not sent by default) + +If `SENTRY_DSN` is not set, the server runs normally without any error tracking overhead. diff --git a/packages/secops_mcp_server/pyproject.toml b/packages/secops_mcp_server/pyproject.toml index 44ec601..3ba0120 100644 --- a/packages/secops_mcp_server/pyproject.toml +++ b/packages/secops_mcp_server/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ ] readme = "README.md" requires-python = ">=3.11" +license = {text = "MIT"} dependencies = [ "fastmcp>=2.0.0", "httpx>=0.24.0", @@ -30,7 +31,11 @@ dependencies = [ "gg-api-core", "uvicorn>=0.27.0" ] -license = {text = "MIT"} + +[project.optional-dependencies] +sentry = [ + "gg-api-core[sentry]", +] [project.scripts] secops-mcp-server = "secops_mcp_server.server:run_mcp_server" diff --git a/packages/secops_mcp_server/src/secops_mcp_server/server.py b/packages/secops_mcp_server/src/secops_mcp_server/server.py index acc74f3..219ac83 100644 --- a/packages/secops_mcp_server/src/secops_mcp_server/server.py +++ b/packages/secops_mcp_server/src/secops_mcp_server/server.py @@ -8,6 +8,7 @@ from fastmcp.exceptions import ToolError from gg_api_core.mcp_server import get_mcp_server from gg_api_core.scopes import set_secops_scopes +from gg_api_core.sentry_integration import init_sentry from gg_api_core.tools.assign_incident import assign_incident from gg_api_core.tools.create_code_fix_request import create_code_fix_request from gg_api_core.tools.list_users import list_users @@ -218,6 +219,13 @@ async def get_current_token_info() -> dict[str, Any]: def run_mcp_server(): logger.info("Starting SecOps MCP server...") + # Initialize Sentry if configured (optional) + sentry_enabled = init_sentry() + if sentry_enabled: + logger.info("Sentry monitoring is enabled") + else: + logger.debug("Sentry monitoring is not configured") + # Check if HTTP/SSE transport is requested via environment variables mcp_port = os.environ.get("MCP_PORT") mcp_host = os.environ.get("MCP_HOST", "127.0.0.1") diff --git a/uv.lock b/uv.lock index cf3548c..7690253 100644 --- a/uv.lock +++ b/uv.lock @@ -434,10 +434,16 @@ dependencies = [ { name = "uvicorn" }, ] +[package.optional-dependencies] +sentry = [ + { name = "gg-api-core", extra = ["sentry"] }, +] + [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=2.0.0" }, { name = "gg-api-core", editable = "packages/gg_api_core" }, + { name = "gg-api-core", extras = ["sentry"], marker = "extra == 'sentry'", editable = "packages/gg_api_core" }, { name = "httpx", specifier = ">=0.24.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "uvicorn", specifier = ">=0.27.0" }, @@ -541,6 +547,11 @@ dependencies = [ { name = "python-dotenv" }, ] +[package.optional-dependencies] +sentry = [ + { name = "sentry-sdk" }, +] + [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=2.0.0" }, @@ -548,6 +559,7 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.0.0" }, ] [[package]] @@ -1500,10 +1512,16 @@ dependencies = [ { name = "uvicorn" }, ] +[package.optional-dependencies] +sentry = [ + { name = "gg-api-core", extra = ["sentry"] }, +] + [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=2.0.0" }, { name = "gg-api-core", editable = "packages/gg_api_core" }, + { name = "gg-api-core", extras = ["sentry"], marker = "extra == 'sentry'", editable = "packages/gg_api_core" }, { name = "httpx", specifier = ">=0.24.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "uvicorn", specifier = ">=0.27.0" }, @@ -1522,6 +1540,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272 }, ] +[[package]] +name = "sentry-sdk" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/18/09875b4323b03ca9025bae7e6539797b27e4fc032998a466b4b9c3d24653/sentry_sdk-2.43.0.tar.gz", hash = "sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20", size = 368953 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/31/8228fa962f7fd8814d634e4ebece8780e2cdcfbdf0cd2e14d4a6861a7cd5/sentry_sdk-2.43.0-py2.py3-none-any.whl", hash = "sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d", size = 400997 }, +] + [[package]] name = "sniffio" version = "1.3.1"