diff --git a/.env.example b/.env.example index 11b46a8..096154d 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,10 @@ # Add env variables here -TESTING= +DATASET_ID= +DATASET_LOCATION= DEBUG= -SLACK_TOKEN= -SLACK_CHANNEL= ENVIRONMENT= -DATASET_ID= -TABLE_ID= GOOGLE_APPLICATION_CREDENTIALS= -DATASET_LOCATION= +SLACK_CHANNEL= +SLACK_TOKEN= +TABLE_ID= +TESTING= diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1f8cf88..64f243a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,6 +16,7 @@ Steps to reproduce the behavior: 2. Trigger webhook '....' 3. See the error + **Expected behavior** A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..48d5f81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 796cafd..4d40039 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -19,7 +19,6 @@ A clear and concise description of any alternative solutions or features you've **Additional context** Add any other context or screenshots about the feature request here. - **Acceptance Criteria** Add acceptance criteria here. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 892115c..25af7bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,7 @@ +exclude: "^\ + (third-party/.*)\ + " + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -25,3 +29,38 @@ repos: rev: v1.7.5 hooks: - id: docformatter + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files # prevents giant files from being committed. + - id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems. + - id: check-merge-conflict # checks for files that contain merge conflict strings. + - id: check-yaml # checks yaml files for parseable syntax. + - id: detect-private-key # detects the presence of private keys. + - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline. + - id: fix-byte-order-marker # removes utf-8 byte order marker. + - id: mixed-line-ending # replaces or checks mixed line ending. + - id: requirements-txt-fixer # sorts entries in requirements.txt. + - id: trailing-whitespace # trims trailing whitespace. + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + files: \.(js|ts|jsx|tsx|css|less|html|json|markdown|md|yaml|yml)$ + + - repo: https://github.com/psf/black + rev: 24.2.0 + hooks: + - id: black + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: [--profile=black] + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v17.0.6 + hooks: + - id: clang-format diff --git a/README.md b/README.md index fc316d6..618e3d6 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,50 @@ # gcp-budget-alerts-service -The micro-service is for tracking and monitoring the spending amount at the GCP service. This micro-service elevates an alert whenever the overall budget surpasses or meets predefined thresholds. + +The microservice is for tracking and monitoring the spending amount at the GCP service. This microservice elevates an alert whenever the overall budget surpasses or meets predefined thresholds. ## Architecture ![_ Expected architecture for the Alert service](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/86e87e12-824a-46a9-ba95-07edca694285) -
Architecture for the Alert Service
+
Architecture for the Alert Service
## Installation Guideline ### Prerequisite + 1. pyenv 2. python 3.8 3. A Slack App for delivering messages on the channel. [Setup Guideline](./SLACKAPP.md) ### Steps + 1. Clone the repository - ```sh - git clone https://github.com/Sachinbisht27/gcp-budget-alerts-service.git - ``` + ```sh + git clone https://github.com/Sachinbisht27/gcp-budget-alerts-service.git + ``` 2. Switch to project folder and setup the vertual environment - ```sh - cd gcp-alerts - python -m venv venv - ``` + ```sh + cd gcp-alerts + python -m venv venv + ``` 3. Activate the virtual environment - ```sh - source ./venv/bin/activate - ``` + ```sh + source ./venv/bin/activate + ``` 4. Install the dependencies: - ```sh - pip install -r requirements-dev.txt - ``` + ```sh + pip install -r requirements-dev.txt + ``` 5. Set up your .env file by copying .env.example - ```sh - cp .env.example .env - ``` + ```sh + cp .env.example .env + ``` 6. Add/update variables in your `.env` file for your environment. 7. Run the following command to get started with pre-commit - ```sh - pre-commit install - ``` + ```sh + pre-commit install + ``` 8. Start the server by following command - ```sh - functions_framework --target=handle --debug - ``` + ```sh + functions_framework --target=handle --debug + ``` diff --git a/SLACKAPP.md b/SLACKAPP.md index 06b77ef..ef26a98 100644 --- a/SLACKAPP.md +++ b/SLACKAPP.md @@ -1,13 +1,12 @@ # Create a Slack App -# Steps to create an app on Slack +## Steps to create an app on Slack 1. Go to the channel on which you want to install the app 2. Go to the view all members icon on the top right of the channel ![image](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/13cc0d92-076e-4fd5-b985-fb5102efac79) - 3. Click on **add app** option 4. Click on **View app directory** 5. Click on the **build** option at the top right of the menu @@ -17,42 +16,34 @@ ![image (9)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/252b96f9-ce02-4c74-bcdc-0527b01c3742) - 9. Under the scope section, give the app permission to write messages ![image (10)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/6183d4aa-4577-4fe8-af35-5c8baf28d93c) - 10. Install the app to the workspace ![image (11)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/4b75f0a7-0c34-4abe-b31d-120ed99d8679) - 11. Give permission to the app ![image (12)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/1ef99851-9258-4c87-88f3-22dd7971d665) - 12. Once the app is installed copy the OAuth Token as it will be used for authentication. ![image (14)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/355d7d13-9cef-4d6b-9a6d-894c96685e95) - 13. Go to the channel where you want to add app. 14. Click on the `channel name` . 15. Click on the integrations tab here. ![Untitled](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/cc301091-2403-4541-ad55-305224bc7f7c) - 16. Then click on the `Add apps`. ![Untitled (2)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/1cf34c76-a38a-4f85-86c7-dd4ef0241246) - 17. Then recognize the newly created app and add to the channel. ![Untitled (3)](https://github.com/Sachinbisht27/gcp-budget-alerts-service/assets/96137915/17cf0b6f-d8bb-4033-81f4-8bc64e543a8e) - You are done installing the app. Enjoy! diff --git a/app/helpers/bigquery_helper/bigquery_helper.py b/app/helpers/bigquery_helper/bigquery_helper.py index c606a8d..d8e5a4b 100644 --- a/app/helpers/bigquery_helper/bigquery_helper.py +++ b/app/helpers/bigquery_helper/bigquery_helper.py @@ -1,6 +1,5 @@ from google.cloud import bigquery - client = bigquery.Client() diff --git a/app/helpers/notification_helper.py b/app/helpers/notification_helper.py index c0e1dfd..10fbb7b 100644 --- a/app/helpers/notification_helper.py +++ b/app/helpers/notification_helper.py @@ -1,3 +1,9 @@ +""" +Notification Helper Module + +This module provides functions for sending notifications. +""" + from app import services diff --git a/app/services/__init__.py b/app/services/__init__.py index 01ffcf5..4edc291 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,2 +1,2 @@ -from .slack_service import * from .budget_service import * +from .slack_service import * diff --git a/app/services/budget_service.py b/app/services/budget_service.py index 28d5f80..c10afff 100644 --- a/app/services/budget_service.py +++ b/app/services/budget_service.py @@ -1,39 +1,31 @@ -import config import datetime -from google.cloud import bigquery - -from app.services import templates -from app.helpers import bigquery_helper -from app.helpers import notification_helper -from app.services.queries import budget_table +from google.cloud import bigquery +from services import templates +from services.queries import budget_table +import config +from app.helpers import bigquery_helper, notification_helper client = bigquery.Client() class BudgetService: + """Handles budget-related operations.""" + def __init__(self): self.dataset_id = config.DATASET_ID self.table_id = config.ALERT_THRESHOLD_TABLE_ID def handle(self, alert_attrs, alert_data): - threshold = float(alert_data.get("alertThresholdExceeded")) * 100 - if not self.is_new_threshold_greater(threshold): + """Handles budget alerts.""" + threshold = self._get_threshold(alert_data) + if not self._is_new_threshold_greater(threshold): return - interval = datetime.datetime.strptime( - alert_data.get("costIntervalStart"), "%Y-%m-%dT%H:%M:%S%z" - ) - interval_str = interval.strftime("%Y-%m-%d %H:%M") - - current_year_month = datetime.datetime.utcnow().strftime("%Y-%m") - year_month_of_budget_interval = interval.strftime("%Y-%m") + interval_str = self._parse_interval(alert_data) - # Check if the alert is for the previous month - if year_month_of_budget_interval != current_year_month: - # The pub/sub is sending us alerts for the previous month on the first day of the new month. - # To handle this condition, we compare the Month-Year of the budget alert interval with the current Month-Year. + if not self._is_current_month(interval_str): return billing_id = alert_attrs.get("billingAccountId") @@ -45,12 +37,31 @@ def handle(self, alert_attrs, alert_data): billing_id, threshold, budget, budget_name, interval_str ) - notify = notification_helper.notify(slack_block) + if notification_helper.notify(slack_block): + self._insert_new_threshold(cost, budget, budget_name, threshold) + + def _get_threshold(self, alert_data): + """Extracts and converts threshold from alert data.""" + return float(alert_data.get("alertThresholdExceeded")) * 100 + + def _parse_interval(self, alert_data): + """Parses interval from alert data.""" + interval = datetime.datetime.strptime( + alert_data.get("costIntervalStart"), "%Y-%m-%dT%H:%M:%S%z" + ) + return interval.strftime("%Y-%m-%d %H:%M") if notify: self.insert_new_threshold(cost, budget, budget_name, threshold) - def is_new_threshold_greater(self, threshold): + def _is_current_month(self, interval_str): + """Checks if the interval falls within the current month.""" + current_year_month = datetime.datetime.utcnow().strftime("%Y-%m") + year_month_of_budget_interval = interval_str[:7] # Extract year-month + return year_month_of_budget_interval == current_year_month + + def _is_new_threshold_greater(self, threshold): + """Checks if the new threshold is greater.""" query_to_get_existing_threshold = budget_table.get_existing_threshold_query( client, self.dataset_id, self.table_id ) @@ -59,7 +70,8 @@ def is_new_threshold_greater(self, threshold): ) return last_existing_threshold is None or threshold > last_existing_threshold - def insert_new_threshold(self, cost, budget, budget_name, threshold): + def _insert_new_threshold(self, cost, budget, budget_name, threshold): + """Inserts a new threshold into the database.""" query_to_insert_threshold = budget_table.get_insert_threshold_query( client, self.dataset_id, self.table_id, cost, budget, budget_name, threshold ) diff --git a/app/services/slack_service.py b/app/services/slack_service.py index 493e1b5..e106ad4 100644 --- a/app/services/slack_service.py +++ b/app/services/slack_service.py @@ -1,7 +1,9 @@ -import config +from ssl import SSLContext + import slack + +import config from utils import logger -from ssl import SSLContext class SlackService: @@ -16,7 +18,7 @@ def __init__(self): def send_alert(self, slack_block): try: - response = self.slack_client.chat_postMessage( + self.slack_client.chat_postMessage( channel=self.channel_name, blocks=slack_block["blocks"], ) diff --git a/config.py b/config.py index b0bacab..6c85d6c 100644 --- a/config.py +++ b/config.py @@ -4,6 +4,7 @@ if ENVIRONMENT == "development": from os import path + from dotenv import load_dotenv basedir = path.abspath(path.dirname(__file__)) diff --git a/main.py b/main.py index 2367faa..23a3a38 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,26 @@ -import json +""" +Module: main +Description: Contains the main function for handling billing alerts. +""" + import base64 +import json -from utils import logger from app import services +from utils import logger -# Endpoint of the cloud function. def handle(payload, context): + """ + Function to handle billing alerts. + + Args: + payload (dict): Payload containing alert attributes and data. + context (dict): Context information. + + Returns: + dict: Response message and status code. + """ try: alert_attrs = payload.get("attributes") alert_data = json.loads(base64.b64decode(payload.get("data")).decode("utf-8")) @@ -16,11 +30,10 @@ def handle(payload, context): alert_attrs, alert_data, ) - budget_service = services.BudgetService() budget_service.handle(alert_attrs, alert_data) except Exception as e: - logger.error(f"Error in main function: {e}") + logger.error("Error while handling the payload for billing alert: %s", e) return {"message": "Something went wrong!"}, 400 return {"message": "Success"}, 200 diff --git a/requirements-dev.txt b/requirements-dev.txt index a7ba2bd..395be22 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,9 @@ functions-framework==3.2.0 google-cloud-logging==3.5.0 google_cloud_bigquery==3.11.4 +werkzeug>=3.0.3 +aiohttp>=3.9.4 +gunicorn>=22.0.0 pre-commit==2.20.0 python-dotenv==0.20.0 slackclient==2.9.4 diff --git a/requirements.txt b/requirements.txt index fc8bf2a..a7601b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ functions-framework==3.2.0 google-cloud-logging==3.5.0 google_cloud_bigquery==3.11.4 +Werkzeug==3.0.3 +pre-commit==3.6.2 slackclient==2.9.4 Werkzeug==2.3.7 +aiohttp>=3.9.4 +gunicorn>=22.0.0 diff --git a/utils/loggingutils.py b/utils/loggingutils.py index fc1ec54..af3eb04 100644 --- a/utils/loggingutils.py +++ b/utils/loggingutils.py @@ -1,7 +1,9 @@ import logging -import config + from google.cloud import logging as gcloud_logging +import config + logger = logging.getLogger() logging.basicConfig(level=logging.DEBUG)