-
-
Notifications
You must be signed in to change notification settings - Fork 1
Telemetry
Observability documentation for the servicenow-devtools-mcp server. Currently, Sentry is the sole observability integration.
See also: Architecture for server internals, Development for setup.
MCP servers run as child processes of AI agents via stdio transport - the user never sees stdout or stderr. This makes traditional logging insufficient for error visibility. Sentry provides structured error capture, contextual data, and alerting for production deployments.
Note: OpenTelemetry was previously available but has been removed from the project. See Historical Note at the end of this page.
The Sentry integration lives in sentry.py and follows a graceful-degradation pattern - all public functions are safe to call regardless of whether Sentry is active.
sentry-sdk is a core dependency (always installed, not an optional extra). However, Sentry only activates when the SENTRY_DSN environment variable is set to a non-empty value. There is no separate SENTRY_ENABLED flag - the DSN presence is the gate.
| Variable | Default | Purpose |
|---|---|---|
SENTRY_DSN |
"" (disabled) |
Sentry Data Source Name - activates error tracking when set |
SENTRY_ENVIRONMENT |
Falls back to SERVICENOW_ENV
|
Environment label shown in Sentry UI |
Both values are defined in the Settings class (config.py) and loaded from environment variables or .env/.env.local files.
The module uses a two-level import guard to handle environments where sentry-sdk might not be installed and SDK versions that may or may not include the MCP integration:
# Primary guard
HAS_SENTRY: bool
try:
import sentry_sdk
HAS_SENTRY = True
except ImportError:
HAS_SENTRY = False
# Secondary guard for newer SDK versions
_HAS_MCP_INTEGRATION = False
if HAS_SENTRY:
try:
from sentry_sdk.integrations.mcp import MCPIntegration
_HAS_MCP_INTEGRATION = True
except ImportError:
passWhen HAS_SENTRY is False, all public functions no-op immediately - zero overhead. The secondary guard allows the optional MCPIntegration to be used when available in newer SDK versions without breaking on older ones.
When activated, Sentry is initialized with these settings:
sentry_sdk.init(
dsn=dsn,
environment=environment,
release=_RELEASE, # e.g. "servicenow-devtools-mcp@0.9.0"
send_default_pii=True,
integrations=integrations, # Includes MCPIntegration when available
traces_sample_rate=1.0, # All transactions sampled
profiles_sample_rate=None, # Profiling disabled
)The release string is dynamically derived from package metadata using importlib.metadata.version():
try:
_RELEASE = f"servicenow-devtools-mcp@{pkg_version('servicenow-devtools-mcp')}"
except Exception:
_RELEASE = "servicenow-devtools-mcp@unknown"Sentry context and exception capture is woven through the codebase at key points:
Every tool invocation sets:
-
Tags (indexed, searchable):
-
tool.name- The tool function name (e.g.,"record_list") -
tool.correlation_id- UUID4 for request tracing
-
-
Context (structured data):
-
"tool"context withname,correlation_id, andargs(withcorrelation_idexcluded from the args dict)
-
The error boundary that wraps all tool executions:
- Catches
ForbiddenError- captures to Sentry, returns ACL denial error envelope - Catches generic
Exception- captures to Sentry, returns generic error envelope
Both paths call capture_exception(e) before returning the serialized error response.
Captures TOON encoding failures to Sentry before falling back to JSON serialization.
Before raising HTTP-related exceptions, sets "http" Sentry context with:
-
status_code- The HTTP status code -
method- The HTTP method (GET, POST, etc.) -
url- The request URL
Captures persistent sys_choice fetch failures when the registry cannot load choice data from the ServiceNow instance.
- Sets
"server"Sentry context with instance hostname (extracted from URL), environment,is_productionflag, and tool package name - Captures
ImportErrorexceptions when tool group modules fail to load
| Function | Signature | Purpose |
|---|---|---|
setup_sentry |
(settings: Settings) -> None |
Initialize SDK with DSN gating. No-ops on re-call. |
capture_exception |
(exc: BaseException | None) -> None |
Capture exception (or current sys.exc_info() if None) |
set_sentry_tag |
(key: str, value: str) -> None |
Set indexed tag on current isolation scope |
set_sentry_context |
(key: str, data: dict[str, Any]) -> None |
Set structured context dict on current isolation scope |
shutdown_sentry |
() -> None |
Flush pending events (2s timeout) and close client (2s timeout) |
All functions no-op when either HAS_SENTRY is False or _initialized is False.
setup_sentry(settings) is called early in create_mcp_server(), before tool registration. The function uses a triple gate to prevent double-initialization:
-
_initializedcheck - Short-circuits if already called -
HAS_SENTRYcheck - Short-circuits ifsentry-sdkis not installed (sets_initialized = True) -
DSN check - Short-circuits if
sentry_dsnis empty (sets_initialized = True)
In all three cases, _initialized is set to True so future calls no-op immediately.
shutdown_sentry() is called in the finally block of main():
def main() -> None:
mcp = create_mcp_server()
try:
mcp.run(transport="stdio")
finally:
shutdown_sentry()The shutdown sequence:
- Checks
HAS_SENTRY and _initialized - Gets the active Sentry client
- If the client is active: flushes pending events (2s timeout), then closes the client (2s timeout)
- Resets
_initialized = False - Wraps everything in try/except to prevent shutdown errors from propagating
Tests use an autouse fixture (_disable_sentry_capture in tests/conftest.py) that resets _initialized = False before and after each test, preventing real Sentry SDK calls during test runs:
@pytest.fixture(autouse=True)
def _disable_sentry_capture():
import servicenow_mcp.sentry as _sentry_mod
_sentry_mod._initialized = False
yield
_sentry_mod._initialized = FalseOpenTelemetry was previously integrated via a telemetry.py module that provided distributed tracing with spans for tool invocations and HTTP requests. It supported OTLP and console exporters, W3C traceparent header injection, and trace context in response envelopes.
The OTel integration was removed in v0.9.0 (commit b72cee3, PR #68) to resolve SonarCloud issues and simplify the observability stack. The .env.example file may still contain stale OTel-related variables (OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT, etc.) - these are ignored by the server and have no effect.