Skip to content

Commit dd1c7d2

Browse files
committed
move execution_id to global context
1 parent d20b60e commit dd1c7d2

File tree

8 files changed

+327
-11
lines changed

8 files changed

+327
-11
lines changed

superset/reports/commands/execute.py

+51-7
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
from superset.utils.celery import session_scope
7474
from superset.utils.core import HeaderDataType, override_user
7575
from superset.utils.csv import get_chart_csv_data, get_chart_dataframe
76+
from superset.utils.decorators import context
7677
from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot
7778
from superset.utils.urls import get_url_path
7879

@@ -83,6 +84,7 @@ class BaseReportState:
8384
current_states: list[ReportState] = []
8485
initial: bool = False
8586

87+
@context()
8688
def __init__(
8789
self,
8890
session: Session,
@@ -234,7 +236,12 @@ def _get_screenshots(self) -> list[bytes]:
234236
try:
235237
image = screenshot.get_screenshot(user=user)
236238
except SoftTimeLimitExceeded as ex:
237-
logger.warning("A timeout occurred while taking a screenshot.")
239+
logger.warning(
240+
"A timeout occurred while taking a screenshot.",
241+
extra={
242+
"execution_id": self._execution_id,
243+
},
244+
)
238245
raise ReportScheduleScreenshotTimeout() from ex
239246
except Exception as ex:
240247
raise ReportScheduleScreenshotFailedError(
@@ -254,11 +261,23 @@ def _get_csv_data(self) -> bytes:
254261
auth_cookies = machine_auth_provider_factory.instance.get_auth_cookies(user)
255262

256263
if self._report_schedule.chart.query_context is None:
257-
logger.warning("No query context found, taking a screenshot to generate it")
264+
logger.warning(
265+
"No query context found, taking a screenshot to generate it",
266+
extra={
267+
"execution_id": self._execution_id,
268+
},
269+
)
258270
self._update_query_context()
259271

260272
try:
261-
logger.info("Getting chart from %s as user %s", url, user.username)
273+
logger.info(
274+
"Getting chart from %s as user %s",
275+
url,
276+
user.username,
277+
extra={
278+
"execution_id": self._execution_id,
279+
},
280+
)
262281
csv_data = get_chart_csv_data(chart_url=url, auth_cookies=auth_cookies)
263282
except SoftTimeLimitExceeded as ex:
264283
raise ReportScheduleCsvTimeout() from ex
@@ -283,11 +302,23 @@ def _get_embedded_data(self) -> pd.DataFrame:
283302
auth_cookies = machine_auth_provider_factory.instance.get_auth_cookies(user)
284303

285304
if self._report_schedule.chart.query_context is None:
286-
logger.warning("No query context found, taking a screenshot to generate it")
305+
logger.warning(
306+
"No query context found, taking a screenshot to generate it",
307+
extra={
308+
"execution_id": self._execution_id,
309+
},
310+
)
287311
self._update_query_context()
288312

289313
try:
290-
logger.info("Getting chart from %s as user %s", url, user.username)
314+
logger.info(
315+
"Getting chart from %s as user %s",
316+
url,
317+
user.username,
318+
extra={
319+
"execution_id": self._execution_id,
320+
},
321+
)
291322
dataframe = get_chart_dataframe(url, auth_cookies)
292323
except SoftTimeLimitExceeded as ex:
293324
raise ReportScheduleDataFrameTimeout() from ex
@@ -440,7 +471,12 @@ def _send(
440471
if notification_errors:
441472
# log all errors but raise based on the most severe
442473
for error in notification_errors:
443-
logger.warning(str(error))
474+
logger.warning(
475+
str(error),
476+
extra={
477+
"execution_id": self._execution_id,
478+
},
479+
)
444480

445481
if any(error.level == ErrorLevel.ERROR for error in notification_errors):
446482
raise ReportScheduleSystemErrorsException(errors=notification_errors)
@@ -466,7 +502,9 @@ def send_error(self, name: str, message: str) -> None:
466502
logger.info(
467503
"header_data in notifications for alerts and reports %s, taskid, %s",
468504
header_data,
469-
self._execution_id,
505+
extra={
506+
"execution_id": self._execution_id,
507+
},
470508
)
471509
notification_content = NotificationContent(
472510
name=name, text=message, header_data=header_data
@@ -725,6 +763,9 @@ def run(self) -> None:
725763
"Running report schedule %s as user %s",
726764
self._execution_id,
727765
username,
766+
extra={
767+
"execution_id": self._execution_id,
768+
},
728769
)
729770
ReportScheduleStateMachine(
730771
session, self._execution_id, self._model, self._scheduled_dttm
@@ -740,6 +781,9 @@ def validate(self, session: Session = None) -> None:
740781
"session is validated: id %s, executionid: %s",
741782
self._model_id,
742783
self._execution_id,
784+
extra={
785+
"execution_id": self._execution_id,
786+
},
743787
)
744788
self._model = (
745789
session.query(ReportSchedule).filter_by(id=self._model_id).one_or_none()

superset/reports/notifications/email.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from typing import Any, Optional
2323

2424
import nh3
25+
from flask import g
2526
from flask_babel import gettext as __
2627

2728
from superset import app
@@ -190,6 +191,7 @@ def send(self) -> None:
190191
subject = self._get_subject()
191192
content = self._get_content()
192193
to = self._get_to()
194+
global_context = getattr(g, "context", {}) or {}
193195
try:
194196
send_email_smtp(
195197
to,
@@ -205,7 +207,11 @@ def send(self) -> None:
205207
header_data=content.header_data,
206208
)
207209
logger.info(
208-
"Report sent to email, notification content is %s", content.header_data
210+
"Report sent to email, notification content is %s",
211+
content.header_data,
212+
extra={
213+
"execution_id": global_context.get("execution_id"),
214+
},
209215
)
210216
except SupersetErrorsException as ex:
211217
raise NotificationError(

superset/reports/notifications/slack.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import backoff
2424
import pandas as pd
25+
from flask import g
2526
from flask_babel import gettext as __
2627
from slack_sdk import WebClient
2728
from slack_sdk.errors import (
@@ -166,6 +167,8 @@ def send(self) -> None:
166167
channel = self._get_channel()
167168
body = self._get_body()
168169
file_type = "csv" if self._content.csv else "png"
170+
global_context = getattr(g, "context", {}) or {}
171+
169172
try:
170173
token = app.config["SLACK_API_TOKEN"]
171174
if callable(token):
@@ -183,7 +186,12 @@ def send(self) -> None:
183186
)
184187
else:
185188
client.chat_postMessage(channel=channel, text=body)
186-
logger.info("Report sent to slack")
189+
logger.info(
190+
"Report sent to slack",
191+
extra={
192+
"execution_id": global_context.get("execution_id"),
193+
},
194+
)
187195
except (
188196
BotUserAccessError,
189197
SlackRequestError,

superset/utils/decorators.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
from collections.abc import Iterator
2121
from contextlib import contextmanager
2222
from typing import Any, Callable, TYPE_CHECKING
23+
from uuid import UUID
2324

24-
from flask import current_app, Response
25+
from flask import current_app, g, Response
2526

2627
from superset.utils import core as utils
2728
from superset.utils.dates import now_as_float
@@ -111,3 +112,38 @@ def wrapped(*args: Any, **kwargs: Any) -> Any:
111112

112113
def on_security_exception(self: Any, ex: Exception) -> Response:
113114
return self.response(403, **{"message": utils.error_msg_from_exception(ex)})
115+
116+
117+
def context(
118+
slice_id: int | None = None,
119+
dashboard_id: int | None = None,
120+
execution_id: str | UUID | None = None,
121+
) -> Callable[..., Any]:
122+
"""
123+
Takes arguments and adds them to the global context.
124+
This is for logging purposes only and values should not be relied on or mutated
125+
"""
126+
127+
def decorate(f: Callable[..., Any]) -> Callable[..., Any]:
128+
def wrapped(*args: Any, **kwargs: Any) -> Any:
129+
if not hasattr(g, "context"):
130+
g.context = {}
131+
available_context_values = ["slice_id", "dashboard_id", "execution_id"]
132+
context_data = {
133+
key: val
134+
for key, val in kwargs.items()
135+
if key in available_context_values
136+
}
137+
138+
# if values are passed in to decorator directly, add them to context
139+
# by overriding values from kwargs
140+
for val in available_context_values:
141+
if locals().get(val) is not None:
142+
context_data[val] = locals()[val]
143+
144+
g.context.update(context_data)
145+
return f(*args, **kwargs)
146+
147+
return wrapped
148+
149+
return decorate

superset/utils/webdriver.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from time import sleep
2424
from typing import Any, TYPE_CHECKING
2525

26-
from flask import current_app
26+
from flask import current_app, g
2727
from selenium.common.exceptions import (
2828
StaleElementReferenceException,
2929
TimeoutException,
@@ -227,6 +227,9 @@ def get_screenshot(self, url: str, element_name: str, user: User) -> bytes | Non
227227
"Taking a PNG screenshot of url %s as user %s",
228228
url,
229229
user.username,
230+
extra={
231+
"execution_id": getattr(g, "context", {}).get("execution_id"),
232+
},
230233
)
231234
if current_app.config["SCREENSHOT_REPLACE_UNEXPECTED_ERRORS"]:
232235
unexpected_errors = WebDriverPlaywright.find_unexpected_errors(page)

tests/unit_tests/notifications/email_tests.py

+83
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
import uuid
18+
from unittest.mock import MagicMock, patch, PropertyMock
19+
1720
import pandas as pd
21+
from flask import g
1822

1923

2024
def test_render_description_with_html() -> None:
@@ -55,3 +59,82 @@ def test_render_description_with_html() -> None:
5559
in email_body
5660
)
5761
assert '<td>&lt;a href="http://www.example.com"&gt;333&lt;/a&gt;</td>' in email_body
62+
63+
64+
@patch("superset.reports.notifications.email.send_email_smtp")
65+
@patch("superset.reports.notifications.email.logger")
66+
def test_send_email(
67+
logger_mock: MagicMock,
68+
send_email_mock: MagicMock,
69+
) -> None:
70+
# `superset.models.helpers`, a dependency of following imports,
71+
# requires app context
72+
from superset.reports.models import ReportRecipients, ReportRecipientType
73+
from superset.reports.notifications.base import NotificationContent
74+
from superset.reports.notifications.email import EmailNotification
75+
76+
execution_id = uuid.uuid4()
77+
g.context = {"execution_id": execution_id}
78+
content = NotificationContent(
79+
name="test alert",
80+
embedded_data=pd.DataFrame(
81+
{
82+
"A": [1, 2, 3],
83+
"B": [4, 5, 6],
84+
"C": ["111", "222", '<a href="http://www.example.com">333</a>'],
85+
}
86+
),
87+
description='<p>This is <a href="#">a test</a> alert</p><br />',
88+
header_data={
89+
"notification_format": "PNG",
90+
"notification_type": "Alert",
91+
"owners": [1],
92+
"notification_source": None,
93+
"chart_id": None,
94+
"dashboard_id": None,
95+
},
96+
)
97+
EmailNotification(
98+
recipient=ReportRecipients(
99+
type=ReportRecipientType.EMAIL,
100+
recipient_config_json='{"target": "[email protected]"}',
101+
),
102+
content=content,
103+
).send()
104+
105+
logger_mock.info.assert_called_with(
106+
"Report sent to email, notification content is %s",
107+
{
108+
"notification_format": "PNG",
109+
"notification_type": "Alert",
110+
"owners": [1],
111+
"notification_source": None,
112+
"chart_id": None,
113+
"dashboard_id": None,
114+
},
115+
extra={"execution_id": execution_id},
116+
)
117+
# clear logger_mock and g.context
118+
logger_mock.reset_mock()
119+
g.context = None
120+
121+
# test with no execution_id
122+
EmailNotification(
123+
recipient=ReportRecipients(
124+
type=ReportRecipientType.EMAIL,
125+
recipient_config_json='{"target": "[email protected]"}',
126+
),
127+
content=content,
128+
).send()
129+
logger_mock.info.assert_called_with(
130+
"Report sent to email, notification content is %s",
131+
{
132+
"notification_format": "PNG",
133+
"notification_type": "Alert",
134+
"owners": [1],
135+
"notification_source": None,
136+
"chart_id": None,
137+
"dashboard_id": None,
138+
},
139+
extra={"execution_id": None},
140+
)

0 commit comments

Comments
 (0)