From ca7c9a52261b1c05b62f9be11465ce2fa6ebef69 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sun, 21 Apr 2024 15:35:53 +0100 Subject: [PATCH 1/5] Add APM --- .env.default | 3 +++ app/commands/submit_task.py | 17 +++++++++++------ app/main.py | 13 +++++++++++++ app/utils/apm.py | 16 ++++++++++++++++ app/utils/config.py | 17 +++++++++++++++++ app/utils/n8n.py | 37 +++++++++++++++++++++---------------- requirements.txt | 1 + test.sh | 2 +- 8 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 app/utils/apm.py diff --git a/.env.default b/.env.default index 2991382..f28208d 100644 --- a/.env.default +++ b/.env.default @@ -1,3 +1,6 @@ +APP_LIFECYCLE="dev" +SENTRY_ENABLED="False" +SENTRY_DSN="" ADMIN_EMAIL="" ADMIN_FIRST_NAME="" BOT_NAME="" diff --git a/app/commands/submit_task.py b/app/commands/submit_task.py index 6a19ff0..70ef360 100644 --- a/app/commands/submit_task.py +++ b/app/commands/submit_task.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import logging +import sentry_sdk from webex_bot.models.command import Command from webex_bot.models.response import Response, response_from_adaptive_card @@ -113,7 +114,8 @@ class SubmitTaskCommand(Command): Submit(title="Cancel", data={"command_keyword": "exit"}), ], ) - return response_from_adaptive_card(card) + with sentry_sdk.start_transaction(name="submit_task_command"): + return response_from_adaptive_card(card) class SubmitTaskCallback(Command): @@ -147,7 +149,8 @@ class SubmitTaskCallback(Command): ) def execute(self, message, attachment_actions, activity) -> str: - return self.msg + with sentry_sdk.start_transaction(name="submit_task_callback"): + return self.msg class MyTasksCallback(Command): @@ -158,11 +161,13 @@ class MyTasksCallback(Command): self.msg: str = "" def pre_execute(self, message, attachment_actions, activity) -> str: - return "Getting your tasks..." + with sentry_sdk.start_transaction(name="my_tasks_preexec"): + return "Getting your tasks..." def execute(self, message, attachment_actions, activity) -> str | None: sender: str = attachment_actions.inputs.get("sender") result: bool = get_tasks(requestor=sender) - if not result: - return "Failed to get tasks. Please try again." - return + with sentry_sdk.start_transaction(name="my_tasks_exec"): + if not result: + return "Failed to get tasks. Please try again." + return diff --git a/app/main.py b/app/main.py index b84f46a..738d852 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +import sentry_sdk +from sentry_sdk.integrations.stdlib import StdlibIntegration + from webex_bot.webex_bot import WebexBot from app.commands.exit import ExitCommand @@ -7,6 +10,16 @@ from app.commands.submit_task import SubmitTaskCommand from app.utils.config import config +if config.sentry_enabled: + apm = sentry_sdk.init( + dsn=config.sentry_dsn, + enable_tracing=True, + environment=config.environment, + integrations=[StdlibIntegration()], + spotlight=True + ) + + def create_bot() -> WebexBot: # Create a Bot Object webex_bot: WebexBot = WebexBot( diff --git a/app/utils/apm.py b/app/utils/apm.py new file mode 100644 index 0000000..3e34fd5 --- /dev/null +++ b/app/utils/apm.py @@ -0,0 +1,16 @@ +import sentry_sdk +from sentry_sdk.integrations.stdlib import StdlibIntegration + +from app.utils.config import config + + +if config.sentry_enabled: + apm = sentry_sdk.init( + dsn=config.sentry_dsn, + enable_tracing=True, + environment=config.environment, + integrations=[StdlibIntegration()], + spotlight=True + ) +else: + apm = None diff --git a/app/utils/config.py b/app/utils/config.py index 27f6e32..a62d235 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -5,12 +5,29 @@ import os class Config: def __init__(self) -> None: + self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() self.__bot_name: str = os.environ["BOT_NAME"] self.__webex_token: str = os.environ["WEBEX_API_KEY"] self.__admin_first_name: str = os.environ["ADMIN_FIRST_NAME"] self.__admin_emails: list = os.environ["ADMIN_EMAIL"].split(",") self.__n8n_webhook_url: str = os.environ["N8N_WEBHOOK_URL"] + self.__sentry_dsn: str = os.environ.get("SENTRY_DSN", "") + self.__sentry_enabled: bool = True if (os.environ.get("SENTRY_ENABLED").upper() == "TRUE" and self.__sentry_dsn != "") else False + @property + def environment(self) -> str: + return self.__environment + + @property + def sentry_enabled(self) -> bool: + return self.__sentry_enabled + + @property + def sentry_dsn(self) -> str: + if not self.__sentry_enabled: + return "" + return self.__sentry_dsn + @property def bot_name(self) -> str: return self.__bot_name diff --git a/app/utils/n8n.py b/app/utils/n8n.py index e1d8e31..e9ca514 100644 --- a/app/utils/n8n.py +++ b/app/utils/n8n.py @@ -1,4 +1,5 @@ import requests +import sentry_sdk from app.utils.config import config @@ -16,22 +17,26 @@ def __n8n_post(data: dict) -> bool: def submit_task(summary, description, completion_date, requestor) -> bool: - data: dict = { - "requestor": requestor, - "title": summary, - "description": description, - "completiondate": completion_date, - } - return __n8n_post(data=data) + with sentry_sdk.start_transaction(name="submit_task"): + data: dict = { + "requestor": requestor, + "title": summary, + "description": description, + "completiondate": completion_date, + } + _data = __n8n_post(data=data) + return _data def get_tasks(requestor) -> bool: - headers: dict = {"Content-Type": "application/json"} - resp: requests.Response = requests.get( - url=config.n8n_webhook_url, - headers=headers, - timeout=10, - verify=False, - params={"requestor": requestor}, - ) - return bool(resp.status_code == 200) + with sentry_sdk.start_transaction(name="get_tasks"): + headers: dict = {"Content-Type": "application/json"} + resp: requests.Response = requests.get( + url=config.n8n_webhook_url, + headers=headers, + timeout=10, + verify=False, + params={"requestor": requestor}, + ) + _data = bool(resp.status_code == 200) + return _data diff --git a/requirements.txt b/requirements.txt index ba6e896..f1aee33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ python-dotenv==1.0.1 PyYAML==6.0.1 requests==2.31.0 requests-toolbelt==1.0.0 +sentry-sdk==1.45.0 six==1.16.0 toml==0.10.2 tomli==2.0.1 diff --git a/test.sh b/test.sh index b0f95fc..1fcaf08 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ export $(cat .env | xargs) python -B -m app.main -unset BOT_NAME WEBEX_API_KEY ADMIN_FIRST_NAME ADMIN_EMAIL N8N_WEBHOOK_URL \ No newline at end of file +unset APP_LIFECYCLE SENTRY_ENABLED SENTRY_DSN BOT_NAME WEBEX_API_KEY ADMIN_FIRST_NAME ADMIN_EMAIL N8N_WEBHOOK_URL -- 2.45.2 From aff7535c03f6b1b8e93b04abaf1c883f29323558 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sun, 21 Apr 2024 15:41:35 +0100 Subject: [PATCH 2/5] Fix unit tests --- app/utils/config.py | 2 +- tests/__init__.py | 0 tests/test_config.py | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py diff --git a/app/utils/config.py b/app/utils/config.py index a62d235..5152b20 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -12,7 +12,7 @@ class Config: self.__admin_emails: list = os.environ["ADMIN_EMAIL"].split(",") self.__n8n_webhook_url: str = os.environ["N8N_WEBHOOK_URL"] self.__sentry_dsn: str = os.environ.get("SENTRY_DSN", "") - self.__sentry_enabled: bool = True if (os.environ.get("SENTRY_ENABLED").upper() == "TRUE" and self.__sentry_dsn != "") else False + self.__sentry_enabled: bool = True if (os.environ.get("SENTRY_ENABLED", "False").upper() == "TRUE" and self.__sentry_dsn != "") else False @property def environment(self) -> str: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/test_config.py index ab41d77..ebebd1c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,6 +11,8 @@ vars: dict = { "ADMIN_FIRST_NAME": "Test", "ADMIN_EMAIL": "test@test.com", "N8N_WEBHOOK_URL": "https://n8n.test.com/webhook/abcdefg", + "SENTRY_ENABLED": "false", + "SENTRY_DSN": "http://localhost" } -- 2.45.2 From e09960e21f12ca3ae3441b0498302de589b108c2 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sun, 21 Apr 2024 15:49:03 +0100 Subject: [PATCH 3/5] Remove apm.py --- app/utils/apm.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/utils/apm.py diff --git a/app/utils/apm.py b/app/utils/apm.py deleted file mode 100644 index 3e34fd5..0000000 --- a/app/utils/apm.py +++ /dev/null @@ -1,16 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations.stdlib import StdlibIntegration - -from app.utils.config import config - - -if config.sentry_enabled: - apm = sentry_sdk.init( - dsn=config.sentry_dsn, - enable_tracing=True, - environment=config.environment, - integrations=[StdlibIntegration()], - spotlight=True - ) -else: - apm = None -- 2.45.2 From 8cb6f104a937fb162dae46980932f1267e8490d7 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sun, 21 Apr 2024 16:03:39 +0100 Subject: [PATCH 4/5] Add SonarCloud recommendations --- app/main.py | 2 +- app/utils/config.py | 24 +++++++++++++++++++----- app/utils/datetime.py | 13 ++++++++++++- app/utils/n8n.py | 27 +++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/app/main.py b/app/main.py index 738d852..4affc14 100644 --- a/app/main.py +++ b/app/main.py @@ -21,7 +21,7 @@ if config.sentry_enabled: def create_bot() -> WebexBot: - # Create a Bot Object + """Create and return a Webex Bot object.""" webex_bot: WebexBot = WebexBot( bot_name=config.bot_name, teams_bot_token=config.webex_token, diff --git a/app/utils/config.py b/app/utils/config.py index 5152b20..f12eb7a 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,10 +1,12 @@ -#!/usr/bin/env python3 +"""Configuration module.""" import os class Config: + """Configuration module.""" def __init__(self) -> None: + """Configuration module.""" self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() self.__bot_name: str = os.environ["BOT_NAME"] self.__webex_token: str = os.environ["WEBEX_API_KEY"] @@ -12,40 +14,52 @@ class Config: self.__admin_emails: list = os.environ["ADMIN_EMAIL"].split(",") self.__n8n_webhook_url: str = os.environ["N8N_WEBHOOK_URL"] self.__sentry_dsn: str = os.environ.get("SENTRY_DSN", "") - self.__sentry_enabled: bool = True if (os.environ.get("SENTRY_ENABLED", "False").upper() == "TRUE" and self.__sentry_dsn != "") else False + self.__sentry_enabled: bool = bool( + os.environ.get("SENTRY_ENABLED", "False").upper() == "TRUE" + and self.__sentry_dsn != "" + ) @property def environment(self) -> str: + """Returns the current app lifecycle.""" return self.__environment - + @property def sentry_enabled(self) -> bool: + """Returns True if Sentry SDK is enabled, else False.""" return self.__sentry_enabled - + @property def sentry_dsn(self) -> str: + """Returns the Sentry DSN value if Sentry SDK is enabled AND + Sentry DSN is set, else blank string.""" if not self.__sentry_enabled: return "" return self.__sentry_dsn - + @property def bot_name(self) -> str: + """Returns the bot name.""" return self.__bot_name @property def webex_token(self) -> str: + """Returns the Webex API key.""" return self.__webex_token @property def admin_first_name(self) -> str: + """Returns the first name of the bot admin.""" return self.__admin_first_name @property def admin_emails(self) -> list: + """Returns a list of admin email addresses.""" return self.__admin_emails @property def n8n_webhook_url(self) -> str: + """Returns the n8n webhook URL.""" return self.__n8n_webhook_url diff --git a/app/utils/datetime.py b/app/utils/datetime.py index 8526bb1..76cdd64 100644 --- a/app/utils/datetime.py +++ b/app/utils/datetime.py @@ -1,7 +1,18 @@ #!/usr/bin/env python3 +import pytz + from datetime import datetime def timestamp_to_date(timestamp: int) -> str: - return datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d") + """Convert timestamp to date. + + Args: + timestamp (int): Timestamp to convert. + + Returns: + str: Date in the format YYYY-MM-DD. + """ + return datetime.fromtimestamp(timestamp=timestamp, tz=pytz.utc).strftime("%Y-%m-%d") + diff --git a/app/utils/n8n.py b/app/utils/n8n.py index e9ca514..4c0f84b 100644 --- a/app/utils/n8n.py +++ b/app/utils/n8n.py @@ -5,6 +5,14 @@ from app.utils.config import config def __n8n_post(data: dict) -> bool: + """Post data to N8N webhook URL. + + Args: + data (dict): Data to post to webhook URL. + + Returns: + bool: True if successful, else False. + """ headers: dict = {"Content-Type": "application/json"} resp: requests.Response = requests.post( url=config.n8n_webhook_url, @@ -17,6 +25,17 @@ def __n8n_post(data: dict) -> bool: def submit_task(summary, description, completion_date, requestor) -> bool: + """Submit task to N8N webhook URL. + + Args: + summary (str): Summary of task. + description (str): Description of task. + completion_date (str): Completion date of task. + requestor (str): Requestor of task. + + Returns: + bool: True if successful, else False. + """ with sentry_sdk.start_transaction(name="submit_task"): data: dict = { "requestor": requestor, @@ -29,6 +48,14 @@ def submit_task(summary, description, completion_date, requestor) -> bool: def get_tasks(requestor) -> bool: + """Get tasks from N8N webhook URL. + + Args: + requestor (str): Requestor of tasks. + + Returns: + bool: True if successful, else False. + """ with sentry_sdk.start_transaction(name="get_tasks"): headers: dict = {"Content-Type": "application/json"} resp: requests.Response = requests.get( diff --git a/requirements.txt b/requirements.txt index f1aee33..b5aadcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ PyJWT==2.8.0 pyparsing==3.1.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 +pytz==2024.1 PyYAML==6.0.1 requests==2.31.0 requests-toolbelt==1.0.0 -- 2.45.2 From e9321704d5a2dee29ab9cb861ed26127da7f8433 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sun, 21 Apr 2024 16:09:40 +0100 Subject: [PATCH 5/5] SonarCloud python:S6890 --- app/utils/datetime.py | 4 ++-- requirements.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/utils/datetime.py b/app/utils/datetime.py index 76cdd64..472371c 100644 --- a/app/utils/datetime.py +++ b/app/utils/datetime.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import pytz +from zoneinfo import ZoneInfo from datetime import datetime @@ -14,5 +14,5 @@ def timestamp_to_date(timestamp: int) -> str: Returns: str: Date in the format YYYY-MM-DD. """ - return datetime.fromtimestamp(timestamp=timestamp, tz=pytz.utc).strftime("%Y-%m-%d") + return datetime.fromtimestamp(timestamp=timestamp, tz=ZoneInfo("UTC")).strftime("%Y-%m-%d") diff --git a/requirements.txt b/requirements.txt index b5aadcd..f1aee33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,6 @@ PyJWT==2.8.0 pyparsing==3.1.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -pytz==2024.1 PyYAML==6.0.1 requests==2.31.0 requests-toolbelt==1.0.0 -- 2.45.2