Skip to content

Commit baeb152

Browse files
MCOL-6018: throw error on overflows
Now we are more in line with server's behavior, if strict mode is enabled.
1 parent 14c1bd8 commit baeb152

17 files changed

+681
-56
lines changed

build/bootstrap_mcs.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ run_unit_tests() {
634634

635635
message "Running unittests"
636636
cd $MARIA_BUILD_PATH
637+
# Config is needed for Unittests from buildroot
638+
if [[ $BUILD_PACKAGES = true ]]; then
639+
message "Storing Columnstore.xml to oam/etc/Columnstore.xml for unittests"
640+
cp $COLUMSNTORE_SOURCE_PATH/oam/etc/Columnstore.xml /etc/columnstore/Columnstore.xml
641+
fi
637642
${CTEST_BIN_NAME} . -R columnstore: -j $(nproc) --output-on-failure
638643
exit_code=$?
639644
cd - >/dev/null

cmapi/cmapi_server/sentry.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import logging
2+
import socket
3+
4+
import cherrypy
5+
import sentry_sdk
6+
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
7+
from sentry_sdk.integrations.logging import LoggingIntegration
8+
9+
from cmapi_server import helpers
10+
from cmapi_server.constants import CMAPI_CONF_PATH
11+
12+
SENTRY_ACTIVE = False
13+
14+
logger = logging.getLogger(__name__)
15+
16+
def maybe_init_sentry() -> bool:
17+
"""Initialize Sentry from CMAPI configuration.
18+
19+
Reads config and initializes Sentry only if dsn parameter is present in corresponding section.
20+
The initialization enables the following integrations:
21+
- LoggingIntegration: capture warning-level logs as Sentry events and use
22+
lower-level logs as breadcrumbs.
23+
- AioHttpIntegration: propagate trace headers for outbound requests made
24+
with `aiohttp`.
25+
26+
The function is a no-op if the DSN is missing.
27+
28+
Returns: True if Sentry is initialized, False otherwise.
29+
"""
30+
global SENTRY_ACTIVE
31+
try:
32+
cfg_parser = helpers.get_config_parser(CMAPI_CONF_PATH)
33+
dsn = helpers.dequote(
34+
cfg_parser.get('Sentry', 'dsn', fallback='').strip()
35+
)
36+
if not dsn:
37+
return False
38+
39+
environment = helpers.dequote(
40+
cfg_parser.get('Sentry', 'environment', fallback='development').strip()
41+
)
42+
traces_sample_rate_str = helpers.dequote(
43+
cfg_parser.get('Sentry', 'traces_sample_rate', fallback='1.0').strip()
44+
)
45+
except Exception:
46+
logger.exception('Failed to initialize Sentry.')
47+
return False
48+
49+
try:
50+
sentry_logging = LoggingIntegration(
51+
level=logging.INFO,
52+
event_level=logging.WARNING,
53+
)
54+
55+
try:
56+
traces_sample_rate = float(traces_sample_rate_str)
57+
except ValueError:
58+
logger.error('Invalid traces_sample_rate: %s', traces_sample_rate_str)
59+
traces_sample_rate = 1.0
60+
61+
sentry_sdk.init(
62+
dsn=dsn,
63+
environment=environment,
64+
traces_sample_rate=traces_sample_rate,
65+
integrations=[sentry_logging, AioHttpIntegration()],
66+
)
67+
SENTRY_ACTIVE = True
68+
logger.info('Sentry initialized for CMAPI via config.')
69+
except Exception:
70+
logger.exception('Failed to initialize Sentry.')
71+
return False
72+
73+
logger.info('Sentry successfully initialized.')
74+
return True
75+
76+
def _sentry_on_start_resource():
77+
"""Start or continue a Sentry transaction for the current CherryPy request.
78+
79+
- Continues an incoming distributed trace using Sentry trace headers if
80+
present; otherwise starts a new transaction with `op='http.server'`.
81+
- Pushes the transaction into the current Sentry scope and attaches useful
82+
request metadata as tags and context (HTTP method, path, client IP,
83+
hostname, request ID, and a filtered subset of headers).
84+
- Stores the transaction on the CherryPy request object for later finishing
85+
in `_sentry_on_end_request`.
86+
"""
87+
if not SENTRY_ACTIVE:
88+
return
89+
try:
90+
request = cherrypy.request
91+
headers = dict(getattr(request, 'headers', {}) or {})
92+
name = f"{request.method} {request.path_info}"
93+
transaction = sentry_sdk.start_transaction(
94+
op='http.server', name=name, continue_from_headers=headers
95+
)
96+
sentry_sdk.Hub.current.scope.set_span(transaction)
97+
98+
# Add request-level context/tags
99+
scope = sentry_sdk.Hub.current.scope
100+
scope.set_tag('http.method', request.method)
101+
scope.set_tag('http.path', request.path_info)
102+
scope.set_tag('client.ip', getattr(request.remote, 'ip', ''))
103+
scope.set_tag('instance.hostname', socket.gethostname())
104+
request_id = getattr(request, 'unique_id', None)
105+
if request_id:
106+
scope.set_tag('request.id', request_id)
107+
# Optionally add headers as context without sensitive values
108+
safe_headers = {k: v for k, v in headers.items()
109+
if k.lower() not in {'authorization', 'x-api-key'}}
110+
scope.set_context('headers', safe_headers)
111+
112+
request.sentry_transaction = transaction
113+
except Exception:
114+
logger.exception('Failed to start Sentry transaction.')
115+
116+
117+
def _sentry_before_error_response():
118+
"""Capture the current exception (if any) to Sentry before error response.
119+
120+
This hook runs when CherryPy prepares an error response. If an exception is
121+
available in the current context, it will be sent to Sentry.
122+
"""
123+
if not SENTRY_ACTIVE:
124+
return
125+
try:
126+
sentry_sdk.capture_exception()
127+
except Exception:
128+
logger.exception('Failed to capture exception to Sentry.')
129+
130+
131+
def _sentry_on_end_request():
132+
"""Finish the Sentry transaction for the current CherryPy request.
133+
134+
Attempts to set the HTTP status code on the active transaction and then
135+
finishes it. If no transaction was started on this request, the function is
136+
a no-op.
137+
"""
138+
if not SENTRY_ACTIVE:
139+
return
140+
try:
141+
request = cherrypy.request
142+
transaction = getattr(request, 'sentry_transaction', None)
143+
if transaction is None:
144+
return
145+
status = cherrypy.response.status
146+
try:
147+
status_code = int(str(status).split()[0])
148+
except Exception:
149+
status_code = None
150+
try:
151+
if status_code is not None and hasattr(transaction, 'set_http_status'):
152+
transaction.set_http_status(status_code)
153+
except Exception:
154+
logger.exception('Failed to set HTTP status code on Sentry transaction.')
155+
transaction.finish()
156+
except Exception:
157+
logger.exception('Failed to finish Sentry transaction.')
158+
159+
160+
class SentryTool(cherrypy.Tool):
161+
"""CherryPy Tool that wires Sentry request lifecycle hooks.
162+
163+
The tool attaches handlers for `on_start_resource`, `before_error_response`,
164+
and `on_end_request` in order to manage Sentry transactions and error
165+
capture across the request lifecycle.
166+
"""
167+
def __init__(self):
168+
cherrypy.Tool.__init__(self, 'on_start_resource', self._tool_callback, priority=50)
169+
170+
@staticmethod
171+
def _tool_callback():
172+
"""Attach Sentry lifecycle callbacks to the current CherryPy request."""
173+
cherrypy.request.hooks.attach(
174+
'on_start_resource', _sentry_on_start_resource, priority=50
175+
)
176+
cherrypy.request.hooks.attach(
177+
'before_error_response', _sentry_before_error_response, priority=60
178+
)
179+
cherrypy.request.hooks.attach(
180+
'on_end_request', _sentry_on_end_request, priority=70
181+
)
182+
183+
184+
def register_sentry_cherrypy_tool() -> None:
185+
"""Register the Sentry CherryPy tool under `tools.sentry`.
186+
187+
This function is safe to call multiple times; failures are silently ignored
188+
to avoid impacting the application startup.
189+
"""
190+
if not SENTRY_ACTIVE:
191+
return
192+
193+
try:
194+
cherrypy.tools.sentry = SentryTool()
195+
except Exception:
196+
logger.exception('Failed to register Sentry CherryPy tool.')
197+

0 commit comments

Comments
 (0)