Add an env-var-gated, off-by-default Sentry SDK integration so MCP runtime errors, fail-closed workflow blockers, lease/terminal-lock/stale-runtime collisions, and recurring watchdog check-ins are visible in a self-hosted Sentry at https://sentry.prgs.cc/. Gitea stays the source of truth; Sentry is observe-only. New module `sentry_observability.py` mirrors the `gitea_audit` conventions (env-gated, best-effort, redacting): - Config from MCP_SENTRY_ENABLED / SENTRY_DSN / SENTRY_ENVIRONMENT / SENTRY_RELEASE / MCP_SENTRY_TRACES_SAMPLE_RATE / MCP_SENTRY_ENABLE_LOGS. Active only when enabled AND a DSN is present; otherwise a hard no-op. - Fail OPEN for observability (never blocks a tool success path) and fail CLOSED for redaction (drop a field rather than risk leaking it). - `scrub_event` before_send/before_send_log hook + allowlisted tags: no tokens/passwords/keychain ids/DSNs/cookies, no raw session-state or full prompt bodies (session_id -> 12-char hash), no full filesystem paths (worktree path -> coarse category). Reuses incident_bridge + gitea_audit scrubbers. - capture_exception, capture_workflow_blocker (with canonical next action), and monitor_checkin with six stable cron slugs (stale lease scan, terminal lock scan, allocator health, namespace health, dashboard freshness, reconciler cleanup). - `sentry_sdk` is a lazily-imported optional dependency; the module imports and no-ops cleanly when the package is absent. Wiring in gitea_mcp_server.py (additive, guarded, best-effort): - init_sentry() in __main__ before mcp.run. - capture_exception in the `_audited` failure path; capture_workflow_blocker in `_audit_pr_result` BLOCKED/FAILED path. - allocator + namespace-health watchdog check-ins at their MCP tool sites (domain modules left pure). Also: pin `sentry-sdk==2.20.0` (optional), document the six env vars in `.env.example`, and add `docs/observability/sentry-integration.md` covering project creation in https://sentry.prgs.cc/, DSN handling, local/dev/prod config, redaction guarantees, and coexistence with the #612 incident bridge. Tests: tests/test_sentry_observability.py (36 cases) cover disabled / enabled / missing-DSN / missing-SDK, redaction, exception capture, workflow-blocker capture, and cron check-in behaviour. Full suite: 2632 passed, 6 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
6.7 KiB
Self-hosted Sentry observability for the Gitea MCP server (#606)
Optional, off-by-default instrumentation that reports MCP runtime errors,
fail-closed workflow blockers, lease / terminal-lock / stale-runtime
collisions, and recurring watchdog check-ins to a self-hosted Sentry at
https://sentry.prgs.cc/.
Gitea remains the source of truth. Sentry is observe-only. It never approves, merges, closes, or otherwise mutates Gitea workflow state, and it never bypasses leases, #332, workflow roles, or the MCP gates. Sentry alerts may only feed the sanctioned Gitea issue/comment path via the #612 incident bridge — never a direct write.
Implemented by sentry_observability.py.
1. Create the Sentry project
- Sign in to the self-hosted Sentry at
https://sentry.prgs.cc/(this is not Sentry Cloud — do not use*.ingest.sentry.io). - Create a new Python project named
gitea-tools-mcp. - Open Settings → Projects → gitea-tools-mcp → Client Keys (DSN) and copy
the DSN. It looks like
https://<publickey>@sentry.prgs.cc/<project-id>. - Never commit the DSN. It is a runtime secret supplied via env var only.
2. Configure the environment
All configuration is env-var driven (see .env.example):
| Variable | Purpose | Default |
|---|---|---|
MCP_SENTRY_ENABLED |
Master gate (1/true/yes/on). Required. |
off |
SENTRY_DSN |
Self-hosted DSN. Required. | (empty) |
SENTRY_ENVIRONMENT |
local / dev / prod tag. |
development |
SENTRY_RELEASE |
Release id (git SHA or version). | (none) |
MCP_SENTRY_TRACES_SAMPLE_RATE |
Perf-trace sample rate 0.0–1.0 (clamped). |
0.0 |
MCP_SENTRY_ENABLE_LOGS |
Forward Python logs as structured logs. | off |
The feature stays completely off unless MCP_SENTRY_ENABLED is truthy and
SENTRY_DSN is non-empty. With either missing, init_sentry() is a no-op,
the SDK is never initialised, and no events are sent — existing tool behaviour
and API-call patterns are unchanged.
Per-environment examples
# local (quiet: capture errors/blockers, no traces)
export MCP_SENTRY_ENABLED=1
export SENTRY_DSN="https://<key>@sentry.prgs.cc/<id>"
export SENTRY_ENVIRONMENT=local
# dev (light tracing + logs)
export MCP_SENTRY_ENABLED=1
export SENTRY_DSN="https://<key>@sentry.prgs.cc/<id>"
export SENTRY_ENVIRONMENT=dev
export MCP_SENTRY_TRACES_SAMPLE_RATE=0.2
export MCP_SENTRY_ENABLE_LOGS=1
# prod (errors/blockers + low-rate tracing, release-tagged)
export MCP_SENTRY_ENABLED=1
export SENTRY_DSN="https://<key>@sentry.prgs.cc/<id>"
export SENTRY_ENVIRONMENT=prod
export SENTRY_RELEASE="$(git rev-parse --short HEAD)"
export MCP_SENTRY_TRACES_SAMPLE_RATE=0.05
The optional SDK is pinned in requirements.txt
(sentry-sdk==2.20.0). It is imported lazily: if the package is absent, the
module still imports and every entry point is a safe no-op.
3. What is instrumented
| Signal | Where | Notes |
|---|---|---|
| Startup init | gitea_mcp_server.py __main__, before mcp.run |
Prints a redaction-safe status line to stderr. |
| Failing mutations (exceptions) | _audited(...) context manager |
capture_exception with scrubbed tags. |
| Fail-closed blockers / failed mutations | _audit_pr_result(...) (BLOCKED/FAILED) |
Structured capture_workflow_blocker event incl. the canonical next action when available (criterion 7). |
| Allocator watchdog check-ins | gitea_allocate_next_work tool |
allocator_health, stale_lease_scan, terminal_lock_scan. |
| Namespace-health check-in | gitea_assess_mcp_namespace_health tool |
namespace_health. |
All capture paths are best-effort / fail open: a Sentry outage or capture error never breaks an MCP tool success path.
4. Cron / watchdog monitors
sentry_observability.MONITOR_SLUGS defines stable check-in slugs:
| Registry key | Sentry monitor slug | Wired at |
|---|---|---|
stale_lease_scan |
gitea-mcp-stale-lease-scan |
allocator run (global lease expiry) |
terminal_lock_scan |
gitea-mcp-terminal-lock-scan |
allocator run (terminal-lock lookup) |
allocator_health |
gitea-mcp-allocator-health |
allocator run |
namespace_health |
gitea-mcp-namespace-health |
namespace-health probe |
dashboard_freshness |
gitea-mcp-dashboard-freshness |
call monitor_checkin("dashboard_freshness", ...) from the dashboard refresh job (#605) |
reconciler_cleanup |
gitea-mcp-reconciler-cleanup |
call monitor_checkin("reconciler_cleanup", ...) from the reconciler cleanup entrypoint |
Create matching Cron monitors in Sentry with those slugs. Emit an
in_progress check-in at job start and ok/error at completion via
sentry_observability.monitor_checkin(slug_key, status).
5. Redaction guarantees (fail closed)
Redaction fails closed: if a field cannot be proven safe it is dropped rather
than sent. The before_send (and before_send_log) hook scrub_event
recursively redacts every outgoing event; on any error it drops the event
entirely. Guarantees, proven by tests/test_sentry_observability.py:
- No tokens, passwords, keychain IDs, DSNs, cookies, or
user:pass@host. - No raw session-state or full prompt/comment bodies —
session_idis only ever surfaced as a 12-charsession_id_hash. - No private config contents or raw credential headers.
- No full local filesystem paths — a worktree path collapses to a coarse
worktree_category(author/reviewer/merger/reconciler/branches/root/other). - Only the allowlisted tag keys in
ALLOWED_TAG_KEYSare ever attached.
6. Coexistence with GlitchTip / the #612 incident bridge
This is the outbound path (MCP → Sentry SDK). It complements — it does not
replace — the inbound incident_bridge.py
(#612), which turns Sentry/GlitchTip observations into durable Gitea issues
and incident_links rows.
- Prefer one observability path per environment. Point the MCP server's
SENTRY_DSNat the same self-hostedgitea-tools-mcpproject that the #612 bridge reconciles from, so an MCP-reported error and its Gitea issue line up. - GlitchTip is Sentry-protocol compatible; if an existing GlitchTip DSN is in
use, either migrate it to
https://sentry.prgs.cc/or document the split (MCP → Sentry, legacy → GlitchTip) explicitly for operators. - The bridge remains the only sanctioned route from an alert back into Gitea workflow state.
7. Non-goals
- Sentry must not become the workflow source of truth.
- Sentry must not approve, merge, close, or mutate Gitea workflow state.
- Sentry must not bypass leases, #332, workflow roles, or the MCP gates.