"""Lambda shared module.""" from contextlib import contextmanager from functools import wraps import time from datadog import api from datadog import initialize import fakeredis import pymysql import redis from owslogger import logger as ows_logger import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration from .common_config import DATADOG_API_KEY from .common_config import DATADOG_APP_KEY from .common_config import ENVIRONMENT from .common_config import logger from .common_config import SENTRY_DSN from .common_config import AR_DB_CREDENTIALS from .common_config import DD_DB_CREDENTIALS from .common_config import LOG_CONNECTION def say_hello(): """Generate high SLA Orchard greeting and logs it.""" logger.info('Hello, world!') @contextmanager def mysql_connection(host, user, password, database, connect_timeout=5, port=3306): """Context manager for mysql connection objects. Args: host (str): hostname of database server user (str): user name password (str): password database (str): database name connect_timeout (int): time to wait for db connection port (int): the port of the sql connection Yields: pymysql.connections.Connection: connection to direct delivery db host """ conn = None try: start_time = time.time() try: # TODO: remove deprecated `connect` call with the positional `host` argument # It's hard to guarantee if the direct change won't break any existing lambda setup # It is possible in case when PyMySQL is pinned to an old version, lambda-common is # unpinned and pip installer won't catch the conflict conn = pymysql.connect( host, user=user, passwd=password, db=database, connect_timeout=connect_timeout, port=port, cursorclass=pymysql.cursors.DictCursor, autocommit=True, ) except TypeError: conn = pymysql.connect( host=host, user=user, passwd=password, db=database, connect_timeout=connect_timeout, port=port, cursorclass=pymysql.cursors.DictCursor, autocommit=True, ) end_time = time.time() if LOG_CONNECTION: log_connection_id(conn, end_time - start_time) yield conn finally: if conn: conn.close() def log_connection_id(connection, time_taken): """Context manager for mysql connection objects. Args: connection (pymysql.connections.Connection): Connection to Mysql DB time_taken (float): how long it took for connection to be made """ with connection.cursor() as cursor: cursor.execute('SELECT CONNECTION_ID() as conn_id') result = cursor.fetchone() logger.info('New DB Connection {} ({})'.format(result['conn_id'], time_taken)) @contextmanager def dd_connection(connection_info=None): """Context manager for dd connection objects. Args: connection_info (dict): Connection dictionary especially when testing Yields: pymysql.connections.Connection: connection to direct delivery db host """ if not connection_info: connection_info = DD_DB_CREDENTIALS original_conn = mysql_connection(**connection_info) with original_conn as conn: yield conn @contextmanager def ar_connection(connection_info=None): """Context manager for ar connection objects. Args: connection_info (dict): Connection dictionary especially when testing Yields: pymysql.connections.Connection: Connection to art relations db host """ if not connection_info: connection_info = AR_DB_CREDENTIALS original_conn = mysql_connection(**connection_info) with original_conn as conn: yield conn @contextmanager def redis_connection(host=None, port=None): """Context manager for redis cache connection objects. Args: host (str): hostname of cache server port (int): port number of cache server Yields: redis.StrictRedis: connection to redis cache """ if not host: redis_client = fakeredis.FakeStrictRedis() else: redis_client = redis.StrictRedis(host=host, port=port, db=0, socket_timeout=1) yield redis_client def check_arguments_before_call(handler): """Check arguments of function before calling it. Args: handler: function to check arguments on. Returns: function: wrapped function. """ @wraps(handler) def argument_handler(*args): """Argument handler checks arguments. Args: *args: passed to wrapped function Returns: any: wrapped handler return value. """ if len(args) != 2: print('not 2 arguments {}'.format(args)) else: return handler(*args) return argument_handler @contextmanager def datadog_connection(api_key=None, app_key=None): """Context manager for datadog connection objects. Args: api_key (str): API key to connect to your datadog account. app_key (str): The application key from your datadog account. Yields: datadog.api: connection to datadog api """ if not api_key: api_key = DATADOG_API_KEY if not app_key: app_key = DATADOG_APP_KEY initialize(api_key=api_key, app_key=app_key) yield api def setup_ows_logger( environment, logger_name, logger_level, service_name, service_version, correlation_id=None, dsn=None, ): """Set up logging. If the correlation id is provided, this will create a logger (if not already created) and an adapter. Args: dsn (str): the data source name. environment (str): the application's environment. logger_name (str): name of the logger. logger_level (str): logging level of the logger. service_name (str): the service name. service_version (str): the service version. correlation_id (str or int): optional correlation id. Returns: ows_logger.setup: Ows logger """ return ows_logger.setup( environment, logger_name, logger_level, service_name, service_version, correlation_id=correlation_id, dsn=dsn, ) def init_sentry_for_lambda( logging_integration=None, before_send=None, sensitive_fields=None ): """Initialize sentry sdk for lambda integration. Default logging integration: sentry_logging = LoggingIntegration( level=logging.INFO, # Capture info and above as breadcrumbs event_level=logging.ERROR # Send errors as events ) Args: logging_integration ( sentry_sdk.integrations.logging.LoggingIntegration) before_send: callback function for event filtering before it is sent to sentry sensitive_fields: list of field names to scrub from events """ if before_send is not None and not callable(before_send): raise ValueError('before_send must be callable') if sensitive_fields is not None and not isinstance(sensitive_fields, list): raise ValueError('sensitive_fields must be a list') integrations = [AwsLambdaIntegration()] if logging_integration: integrations.append(logging_integration) sentry_config = { 'dsn': SENTRY_DSN, 'environment': ENVIRONMENT, 'integrations': integrations, } if before_send is not None: sentry_config['before_send'] = before_send if sensitive_fields: from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber sentry_config['event_scrubber'] = EventScrubber( denylist=DEFAULT_DENYLIST + sensitive_fields, recursive=True ) if before_send is None: from .sentry import SentryScrubber scrubber = SentryScrubber(sensitive_fields=sensitive_fields) sentry_config['before_send'] = scrubber.before_send_handler sentry_sdk.init(**sentry_config)