You've already forked mariadb-columnstore-engine
mirror of
https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
synced 2025-10-31 18:30:33 +03:00
Support distributed request tracing -Direct dependencies now in requirements[-dev].in, pip-compile generates full requirement[-dev].txt from them
198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
import logging
|
|
import socket
|
|
|
|
import cherrypy
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
|
|
from cmapi_server import helpers
|
|
from cmapi_server.constants import CMAPI_CONF_PATH
|
|
|
|
SENTRY_ACTIVE = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def maybe_init_sentry() -> bool:
|
|
"""Initialize Sentry from CMAPI configuration.
|
|
|
|
Reads config and initializes Sentry only if dsn parameter is present in corresponding section.
|
|
The initialization enables the following integrations:
|
|
- LoggingIntegration: capture warning-level logs as Sentry events and use
|
|
lower-level logs as breadcrumbs.
|
|
- AioHttpIntegration: propagate trace headers for outbound requests made
|
|
with `aiohttp`.
|
|
|
|
The function is a no-op if the DSN is missing.
|
|
|
|
Returns: True if Sentry is initialized, False otherwise.
|
|
"""
|
|
global SENTRY_ACTIVE
|
|
try:
|
|
cfg_parser = helpers.get_config_parser(CMAPI_CONF_PATH)
|
|
dsn = helpers.dequote(
|
|
cfg_parser.get('Sentry', 'dsn', fallback='').strip()
|
|
)
|
|
if not dsn:
|
|
return False
|
|
|
|
environment = helpers.dequote(
|
|
cfg_parser.get('Sentry', 'environment', fallback='development').strip()
|
|
)
|
|
traces_sample_rate_str = helpers.dequote(
|
|
cfg_parser.get('Sentry', 'traces_sample_rate', fallback='1.0').strip()
|
|
)
|
|
except Exception:
|
|
logger.exception('Failed to initialize Sentry.')
|
|
return False
|
|
|
|
try:
|
|
sentry_logging = LoggingIntegration(
|
|
level=logging.INFO,
|
|
event_level=logging.WARNING,
|
|
)
|
|
|
|
try:
|
|
traces_sample_rate = float(traces_sample_rate_str)
|
|
except ValueError:
|
|
logger.error('Invalid traces_sample_rate: %s', traces_sample_rate_str)
|
|
traces_sample_rate = 1.0
|
|
|
|
sentry_sdk.init(
|
|
dsn=dsn,
|
|
environment=environment,
|
|
traces_sample_rate=traces_sample_rate,
|
|
integrations=[sentry_logging, AioHttpIntegration()],
|
|
)
|
|
SENTRY_ACTIVE = True
|
|
logger.info('Sentry initialized for CMAPI via config.')
|
|
except Exception:
|
|
logger.exception('Failed to initialize Sentry.')
|
|
return False
|
|
|
|
logger.info('Sentry successfully initialized.')
|
|
return True
|
|
|
|
def _sentry_on_start_resource():
|
|
"""Start or continue a Sentry transaction for the current CherryPy request.
|
|
|
|
- Continues an incoming distributed trace using Sentry trace headers if
|
|
present; otherwise starts a new transaction with `op='http.server'`.
|
|
- Pushes the transaction into the current Sentry scope and attaches useful
|
|
request metadata as tags and context (HTTP method, path, client IP,
|
|
hostname, request ID, and a filtered subset of headers).
|
|
- Stores the transaction on the CherryPy request object for later finishing
|
|
in `_sentry_on_end_request`.
|
|
"""
|
|
if not SENTRY_ACTIVE:
|
|
return
|
|
try:
|
|
request = cherrypy.request
|
|
headers = dict(getattr(request, 'headers', {}) or {})
|
|
name = f"{request.method} {request.path_info}"
|
|
transaction = sentry_sdk.start_transaction(
|
|
op='http.server', name=name, continue_from_headers=headers
|
|
)
|
|
sentry_sdk.Hub.current.scope.set_span(transaction)
|
|
|
|
# Add request-level context/tags
|
|
scope = sentry_sdk.Hub.current.scope
|
|
scope.set_tag('http.method', request.method)
|
|
scope.set_tag('http.path', request.path_info)
|
|
scope.set_tag('client.ip', getattr(request.remote, 'ip', ''))
|
|
scope.set_tag('instance.hostname', socket.gethostname())
|
|
request_id = getattr(request, 'unique_id', None)
|
|
if request_id:
|
|
scope.set_tag('request.id', request_id)
|
|
# Optionally add headers as context without sensitive values
|
|
safe_headers = {k: v for k, v in headers.items()
|
|
if k.lower() not in {'authorization', 'x-api-key'}}
|
|
scope.set_context('headers', safe_headers)
|
|
|
|
request.sentry_transaction = transaction
|
|
except Exception:
|
|
logger.exception('Failed to start Sentry transaction.')
|
|
|
|
|
|
def _sentry_before_error_response():
|
|
"""Capture the current exception (if any) to Sentry before error response.
|
|
|
|
This hook runs when CherryPy prepares an error response. If an exception is
|
|
available in the current context, it will be sent to Sentry.
|
|
"""
|
|
if not SENTRY_ACTIVE:
|
|
return
|
|
try:
|
|
sentry_sdk.capture_exception()
|
|
except Exception:
|
|
logger.exception('Failed to capture exception to Sentry.')
|
|
|
|
|
|
def _sentry_on_end_request():
|
|
"""Finish the Sentry transaction for the current CherryPy request.
|
|
|
|
Attempts to set the HTTP status code on the active transaction and then
|
|
finishes it. If no transaction was started on this request, the function is
|
|
a no-op.
|
|
"""
|
|
if not SENTRY_ACTIVE:
|
|
return
|
|
try:
|
|
request = cherrypy.request
|
|
transaction = getattr(request, 'sentry_transaction', None)
|
|
if transaction is None:
|
|
return
|
|
status = cherrypy.response.status
|
|
try:
|
|
status_code = int(str(status).split()[0])
|
|
except Exception:
|
|
status_code = None
|
|
try:
|
|
if status_code is not None and hasattr(transaction, 'set_http_status'):
|
|
transaction.set_http_status(status_code)
|
|
except Exception:
|
|
logger.exception('Failed to set HTTP status code on Sentry transaction.')
|
|
transaction.finish()
|
|
except Exception:
|
|
logger.exception('Failed to finish Sentry transaction.')
|
|
|
|
|
|
class SentryTool(cherrypy.Tool):
|
|
"""CherryPy Tool that wires Sentry request lifecycle hooks.
|
|
|
|
The tool attaches handlers for `on_start_resource`, `before_error_response`,
|
|
and `on_end_request` in order to manage Sentry transactions and error
|
|
capture across the request lifecycle.
|
|
"""
|
|
def __init__(self):
|
|
cherrypy.Tool.__init__(self, 'on_start_resource', self._tool_callback, priority=50)
|
|
|
|
@staticmethod
|
|
def _tool_callback():
|
|
"""Attach Sentry lifecycle callbacks to the current CherryPy request."""
|
|
cherrypy.request.hooks.attach(
|
|
'on_start_resource', _sentry_on_start_resource, priority=50
|
|
)
|
|
cherrypy.request.hooks.attach(
|
|
'before_error_response', _sentry_before_error_response, priority=60
|
|
)
|
|
cherrypy.request.hooks.attach(
|
|
'on_end_request', _sentry_on_end_request, priority=70
|
|
)
|
|
|
|
|
|
def register_sentry_cherrypy_tool() -> None:
|
|
"""Register the Sentry CherryPy tool under `tools.sentry`.
|
|
|
|
This function is safe to call multiple times; failures are silently ignored
|
|
to avoid impacting the application startup.
|
|
"""
|
|
if not SENTRY_ACTIVE:
|
|
return
|
|
|
|
try:
|
|
cherrypy.tools.sentry = SentryTool()
|
|
except Exception:
|
|
logger.exception('Failed to register Sentry CherryPy tool.')
|
|
|