Skip to content

Commit a5b3a5b

Browse files
committed
chore(observability): Integrates with Sentry
1 parent a74e2b2 commit a5b3a5b

File tree

11 files changed

+393
-3
lines changed

11 files changed

+393
-3
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ GITGUARDIAN_URL=https://dashboard.gitguardian.com
1212

1313
# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
1414
LOG_LEVEL=INFO
15+
16+
# Optional: Sentry Integration for Error Tracking
17+
# Sentry provides error tracking and performance monitoring
18+
# Install with: pip install 'secops-mcp-server[sentry]'
19+
# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
20+
# SENTRY_ENVIRONMENT=production
21+
# SENTRY_RELEASE=1.0.0
22+
# SENTRY_TRACES_SAMPLE_RATE=0.1
23+
# SENTRY_PROFILES_SAMPLE_RATE=0.1

DEVELOPMENT.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,45 @@ For all transport modes, you can provide a PAT via environment variable:
172172
GITGUARDIAN_PERSONAL_ACCESS_TOKEN=<your-pat> developer-mcp-server
173173
```
174174

175+
## Optional Dependencies
176+
177+
The project supports optional dependencies (extras) for additional features:
178+
179+
### Installing Optional Dependencies
180+
181+
```bash
182+
# Install with specific extras during development
183+
uv sync --extra sentry
184+
185+
# Install all optional dependencies
186+
uv sync --all-extras
187+
188+
# Add an optional dependency to the project
189+
uv add --optional sentry sentry-sdk
190+
```
191+
192+
### Using Optional Dependencies with uvx
193+
194+
When running the server with `uvx` from Git, you can include optional dependencies:
195+
196+
```bash
197+
# Include extras using the #egg syntax
198+
uvx --from 'git+https://github.com/GitGuardian/ggmcp.git@main#egg=secops-mcp-server[sentry]' secops-mcp-server
199+
200+
# Or install the optional dependency separately
201+
uv pip install sentry-sdk
202+
uvx --from git+https://github.com/GitGuardian/ggmcp.git@main secops-mcp-server
203+
```
204+
205+
### Current Optional Dependencies
206+
207+
- **sentry**: Adds Sentry SDK for error tracking and performance monitoring
208+
- Core package: `gg-api-core[sentry]`
209+
- Available in: `developer-mcp-server[sentry]`, `secops-mcp-server[sentry]`
210+
- Implementation: `gg_api_core/src/gg_api_core/sentry_integration.py`
211+
- Used for: Production error monitoring and alerting
212+
- See individual package READMEs for configuration details
213+
175214
## Testing
176215

177216
Run tests using uv (OAuth is disabled by default in tests):

packages/developer_mcp_server/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Note: Honeytoken scopes are omitted for self-hosted instances as they require th
4444
|----------|-------------|---------|
4545
| `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) |
4646
| `GITGUARDIAN_SCOPES` | Comma-separated list of OAuth scopes | Auto-detected based on instance type |
47+
| `SENTRY_DSN` | Sentry Data Source Name for error tracking (optional) | None |
48+
| `SENTRY_ENVIRONMENT` | Environment name for Sentry (optional) | `production` |
49+
| `SENTRY_RELEASE` | Release version or commit SHA for Sentry (optional) | None |
50+
| `SENTRY_TRACES_SAMPLE_RATE` | Performance traces sampling rate 0.0-1.0 (optional) | `0.1` |
51+
| `SENTRY_PROFILES_SAMPLE_RATE` | Profiling sampling rate 0.0-1.0 (optional) | `0.1` |
4752

4853
**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.
4954

@@ -53,6 +58,52 @@ Note: Honeytoken scopes are omitted for self-hosted instances as they require th
5358

5459
To override auto-detection, set `GITGUARDIAN_SCOPES` explicitly in your MCP configuration.
5560

61+
## Optional Integrations
62+
63+
### Sentry Error Tracking
64+
65+
The MCP server supports optional Sentry integration for error tracking and performance monitoring. This is completely optional and designed to avoid vendor lock-in.
66+
67+
**Installation:**
68+
69+
```bash
70+
# Install with pip
71+
pip install 'developer-mcp-server[sentry]'
72+
73+
# Install with uv (in a project)
74+
uv add 'developer-mcp-server[sentry]'
75+
76+
# Run with uvx (from Git)
77+
uvx --from 'git+https://github.com/GitGuardian/ggmcp.git@main#egg=developer-mcp-server[sentry]' developer-mcp-server
78+
79+
# Or install Sentry SDK separately (works with any installation method)
80+
pip install sentry-sdk>=2.0.0
81+
uv pip install sentry-sdk>=2.0.0
82+
```
83+
84+
**Configuration:**
85+
86+
Set the `SENTRY_DSN` environment variable to enable Sentry:
87+
88+
```bash
89+
export SENTRY_DSN="https://your-key@sentry.io/project-id"
90+
export SENTRY_ENVIRONMENT="development"
91+
export SENTRY_RELEASE="1.0.0"
92+
93+
# Then run the server as usual
94+
developer-mcp-server
95+
```
96+
97+
**Features:**
98+
99+
- Automatic exception tracking
100+
- Performance monitoring with configurable sampling
101+
- Logging integration (INFO+ as breadcrumbs, ERROR+ as events)
102+
- Optional profiling support
103+
- Privacy-focused (PII not sent by default)
104+
105+
If `SENTRY_DSN` is not set, the server runs normally without any error tracking overhead.
106+
56107
## Honeytoken Management
57108

58109
The server provides functions to create and manage honeytokens, which are fake credentials that can be used to detect unauthorized access to your systems.

packages/developer_mcp_server/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@ classifiers = [
2222
]
2323
readme = "README.md"
2424
requires-python = ">=3.11"
25+
license = {text = "MIT"}
2526
dependencies = [
2627
"fastmcp>=2.0.0",
2728
"httpx>=0.24.0",
2829
"pydantic>=2.0.0",
2930
"gg-api-core",
3031
"uvicorn>=0.27.0"
3132
]
32-
license = {text = "MIT"}
33+
34+
[project.optional-dependencies]
35+
sentry = [
36+
"gg-api-core[sentry]",
37+
]
3338

3439

3540
[project.scripts]

packages/developer_mcp_server/src/developer_mcp_server/server.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from gg_api_core.mcp_server import get_mcp_server
77
from gg_api_core.scopes import set_developer_scopes
8+
from gg_api_core.sentry_integration import init_sentry
89

910
from developer_mcp_server.register_tools import DEVELOPER_INSTRUCTIONS, register_developer_tools
1011

@@ -29,6 +30,13 @@
2930
def run_mcp_server():
3031
logger.info("Starting Developer MCP server...")
3132

33+
# Initialize Sentry if configured (optional)
34+
sentry_enabled = init_sentry()
35+
if sentry_enabled:
36+
logger.info("Sentry monitoring is enabled")
37+
else:
38+
logger.debug("Sentry monitoring is not configured")
39+
3240
# Check if HTTP/SSE transport is requested via environment variables
3341
mcp_port = os.environ.get("MCP_PORT")
3442
mcp_host = os.environ.get("MCP_HOST", "127.0.0.1")

packages/gg_api_core/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@ classifiers = [
2222
]
2323
readme = "README.md"
2424
requires-python = ">=3.11"
25+
license = {text = "MIT"}
2526
dependencies = [
2627
"httpx>=0.28.1",
2728
"fastmcp>=2.0.0",
2829
"python-dotenv>=1.0.0",
2930
"pydantic-settings>=2.0.0",
3031
"jinja2>=3.1.0",
3132
]
32-
license = {text = "MIT"}
33+
34+
[project.optional-dependencies]
35+
sentry = [
36+
"sentry-sdk>=2.0.0",
37+
]
3338

3439
[build-system]
3540
requires = ["hatchling"]
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Optional Sentry integration for error tracking and monitoring.
2+
3+
This module provides optional Sentry instrumentation that can be enabled
4+
via environment variables. It's designed to be non-invasive and vendor-neutral,
5+
allowing users to opt-in to Sentry monitoring without forcing a dependency.
6+
7+
Environment Variables:
8+
SENTRY_DSN: Sentry Data Source Name (required to enable Sentry)
9+
SENTRY_ENVIRONMENT: Environment name (e.g., production, development)
10+
SENTRY_RELEASE: Release version or commit SHA
11+
SENTRY_TRACES_SAMPLE_RATE: Sampling rate for performance traces (0.0 to 1.0)
12+
SENTRY_PROFILES_SAMPLE_RATE: Sampling rate for profiling (0.0 to 1.0)
13+
"""
14+
15+
import logging
16+
import os
17+
from typing import Any
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def init_sentry() -> bool:
23+
"""
24+
Initialize Sentry SDK if configured via environment variables.
25+
26+
This function attempts to import and configure Sentry SDK only if
27+
SENTRY_DSN is provided. It gracefully handles missing sentry-sdk
28+
installation and logs appropriate messages.
29+
30+
Returns:
31+
bool: True if Sentry was successfully initialized, False otherwise
32+
33+
Example:
34+
>>> import os
35+
>>> os.environ["SENTRY_DSN"] = "https://..."
36+
>>> init_sentry()
37+
True
38+
"""
39+
dsn = os.environ.get("SENTRY_DSN")
40+
41+
if not dsn:
42+
logger.debug("SENTRY_DSN not configured, skipping Sentry initialization")
43+
return False
44+
45+
try:
46+
import sentry_sdk
47+
from sentry_sdk.integrations.logging import LoggingIntegration
48+
except ImportError:
49+
logger.warning("Sentry SDK not installed")
50+
return False
51+
52+
# Get optional configuration from environment
53+
environment = os.environ.get("SENTRY_ENVIRONMENT", "production")
54+
release = os.environ.get("SENTRY_RELEASE")
55+
traces_sample_rate = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.1"))
56+
profiles_sample_rate = float(os.environ.get("SENTRY_PROFILES_SAMPLE_RATE", "0.1"))
57+
58+
# Configure logging integration
59+
logging_integration = LoggingIntegration(
60+
level=logging.INFO, # Capture info and above as breadcrumbs
61+
event_level=logging.ERROR, # Send errors as events
62+
)
63+
64+
try:
65+
sentry_sdk.init(
66+
dsn=dsn,
67+
environment=environment,
68+
release=release,
69+
traces_sample_rate=traces_sample_rate,
70+
profiles_sample_rate=profiles_sample_rate,
71+
integrations=[logging_integration],
72+
# Automatically capture unhandled exceptions
73+
send_default_pii=False, # Don't send personally identifiable information by default
74+
)
75+
76+
logger.info(
77+
f"Sentry initialized successfully for environment: {environment}"
78+
+ (f", release: {release}" if release else "")
79+
)
80+
return True
81+
82+
except Exception as e:
83+
logger.error(f"Failed to initialize Sentry: {str(e)}")
84+
return False
85+
86+
87+
def set_sentry_context(key: str, value: Any) -> None:
88+
"""
89+
Set additional context for Sentry error reporting.
90+
91+
This is a convenience wrapper that safely sets context even if
92+
Sentry is not initialized.
93+
94+
Args:
95+
key: Context key (e.g., "user", "workspace", "api_token")
96+
value: Context value (can be dict, string, etc.)
97+
98+
Example:
99+
>>> set_sentry_context("workspace", {"id": "123", "name": "acme"})
100+
"""
101+
try:
102+
import sentry_sdk
103+
104+
sentry_sdk.set_context(key, value)
105+
except ImportError:
106+
# Sentry not installed, silently skip
107+
pass
108+
except Exception as e:
109+
logger.debug(f"Failed to set Sentry context: {str(e)}")
110+
111+
112+
def set_sentry_user(user_info: dict[str, Any]) -> None:
113+
"""
114+
Set user information for Sentry error reporting.
115+
116+
This is a convenience wrapper that safely sets user info even if
117+
Sentry is not initialized.
118+
119+
Args:
120+
user_info: Dictionary with user information (id, email, username, etc.)
121+
122+
Example:
123+
>>> set_sentry_user({"id": "123", "email": "user@example.com"})
124+
"""
125+
try:
126+
import sentry_sdk
127+
128+
sentry_sdk.set_user(user_info)
129+
except ImportError:
130+
# Sentry not installed, silently skip
131+
pass
132+
except Exception as e:
133+
logger.debug(f"Failed to set Sentry user: {str(e)}")
134+
135+
136+
def capture_exception(exception: Exception, **kwargs) -> None:
137+
"""
138+
Manually capture an exception to Sentry.
139+
140+
This is useful for handled exceptions that you still want to track.
141+
142+
Args:
143+
exception: The exception to capture
144+
**kwargs: Additional context to attach to the event
145+
146+
Example:
147+
>>> try:
148+
... risky_operation()
149+
... except ValueError as e:
150+
... capture_exception(e, extra={"operation": "risky_operation"})
151+
... handle_error(e)
152+
"""
153+
try:
154+
import sentry_sdk
155+
156+
sentry_sdk.capture_exception(exception, **kwargs)
157+
except ImportError:
158+
# Sentry not installed, silently skip
159+
pass
160+
except Exception as e:
161+
logger.debug(f"Failed to capture exception in Sentry: {str(e)}")

0 commit comments

Comments
 (0)