# -*- coding: utf-8 -*- """ Catchers ======== FÄngare (sv. catchers) is a set of decorators letting the errors happening during a flow run being caught and/or sent to Sentry and Slack. Also there are several additional methods represented here, e.g. for sending messages to Slack and getting a secrets from AWS SecretsManager. """ import base64 import datetime import json import logging import sys from typing import Callable, Tuple, Any, Dict, Union import boto3 import functools import os import requests import sentry_sdk from botocore.exceptions import ClientError def init_sentry(sentry_dsn: str): """Initialises the SDK and optionally integrations. Args: sentry_dsn (str): Sentry data source name to monitor errors. """ if sentry_dsn: sentry_sdk.init( sentry_dsn, traces_sample_rate=1.0 ) def catch_all_and_print(sentry_dsn) -> Callable[[tuple[Any, ...], dict[str, Any]], Any]: def wrap(f): """Decorate a function for catching a failure while run. The decorator watches the errors during the wrapped function run and when they occur, it sends the error message to Sentry. Args: sentry_dsn (str): Sentry data source name to monitor errors. """ @functools.wraps(f) def inner(*args, **kwargs): try: return f(*args, **kwargs) except Exception as ex: if sentry_dsn: sentry_sdk.capture_exception(ex) raise ex return inner return wrap def catch_flow_failure(on_failure, sentry_dsn) -> Callable[[tuple[Any, ...], dict[str, Any]], Any]: def wrap(f): """Decorate a function for catching a failure while run. The decorator watches the errors during the wrapped function run and when they occur, it initiates execution of parametrised function (on_failure) having corresponding on failure actions and sends the error message to Sentry. Args: on_failure (function): desirable on failure actions method. sentry_dsn (str): Sentry data source name to monitor errors. """ @functools.wraps(f) def inner(*args, **kwargs): try: return f(*args, **kwargs) except Exception as ex: on_failure(args[0]) if sentry_dsn: sentry_sdk.capture_exception(ex) raise ex return inner return wrap def get_secret(secret_name: str, flow_name: str, env: str) -> Union[bytes, Any]: """Get secret from AWS Secrets Manager. Args: secret_name (str): secret name. flow_name (str): flow name. env (str): environment. Return: str: requested secret. """ full_secret_name = '{}/{}/{}'.format(env, flow_name, secret_name) region_name = "us-east-1" secrets = boto3.client('secretsmanager', region_name=region_name) try: response = secrets.get_secret_value( SecretId=full_secret_name) except ClientError as error: if error.response['Error']['Code'] in ['DecryptionFailureException', 'InternalServiceErrorException', 'InvalidParameterException', 'InvalidRequestException', 'ResourceNotFoundException']: raise error else: if 'SecretString' in response: return response['SecretString']. \ replace(secret_name, '').replace('"', '').replace(":", ''). \ replace("{", '').replace("}", '') return base64.b64decode(response['SecretBinary']) def sent_a_message_to_slack( title: str, message: str, url: str, username: str, icon_emoji: str, color="#9733EE"): """Send a needed data containing message to Slack channel with specified URL. Args: title (str): a title, header of the message. message (str): a message itself, placed under the title. url (str): Slack webhook URL associated with a channel. username (str): a name of an app, sending the message. icon_emoji (str): the app icon, being visible. color (str): color of a vertical line along the message. """ slack_data = { "username": username, "icon_emoji": icon_emoji, "attachments": [ { "color": color, "fields": [ { "title": title, "value": message, "short": "false", }]}]} byte_length = str(sys.getsizeof(slack_data)) headers = {'Content-Type': "application/json", 'Content-Length': byte_length} response = requests.post(url, data=json.dumps(slack_data), headers=headers) if response.status_code != 200: raise Exception(response.status_code, response.text)