import inspect import typing from functools import wraps from flask import current_app from flask import has_request_context from flask_atlas_auth import current_user from core_notifications.consts import SystemEvents from .processors import BaseAnalyticsProcessor class Collector: """ Main class to abstract and decouple analytics logic. Possible ways for collecting events: - explicit with `log` - tracking functions with `track` decorator `track` method accepts any callables as accessors to data handled with decorated methods. Callables expects the following optional args: - args (method positional args) - kwargs (method named args) - result (method return value) Lets say you need some value from service result, then you cann pass the following callable: `lambda result: result.value.some_field` Examples: collector = Collector() # function collector.log(SystemEvents.some_event, arg1="val1", arg_n="val_n") # decorator @collector.track( SystemEvents.some_event, event_arg0=lambda args: args[0], event_arg1=lambda kwargs: kwargs["arg1"], event_arg_n=lambda result: result["key_n"] ) def audited_function("val0", arg1="val1"): return {"key_n": "val_n"} """ def __init__(self): self._processors = [] def attach_processor(self, processor: BaseAnalyticsProcessor): self._processors.append(processor) def detach_processor(self, processor: BaseAnalyticsProcessor): self._processors.remove(processor) def log(self, event: SystemEvents, **kwargs): for processor in self._processors: processor.process(event, self._get_context(), **kwargs) def track( self, event: SystemEvents, _with_auth=False, **accessors: typing.Callable, ) -> typing.Callable: def wrapped(func): @wraps(func) def decorated_func(*args, **kwargs): result = func(*args, **kwargs) event_data = self._get_values_by_accessors( accessors, args=args, kwargs=kwargs, result=result ) self.log(event, **event_data) return result return decorated_func return wrapped def _get_context(self): context = {} if has_request_context(): context.update( { "env": current_app.config.get("ENV"), "user_id": current_user.id, } ) if current_user.is_authenticated: context["user_name"] = current_user.name return context def _get_values_by_accessors(self, accessors, **context): values = {} for arg_name, arg_callable in accessors.items(): spec = inspect.getfullargspec(arg_callable).args values[arg_name] = arg_callable( **{k: v for k, v in context.items() if k in spec} ) return values