def get_or_create_model_event(instance, operation: Operation, force=False, extra=False) -> [Any, bool]: """ Get or create the ModelEvent of an instance. This function will also populate the event with the current information. :param instance: instance to derive an event from :param operation: specified operation that is done :param force: force creation of new event? :param extra: extra information inserted? :return: [event, created?] """ from automated_logging.models import ( ModelEvent, ModelEntry, ModelMirror, Application, ) from automated_logging.settings import settings get_or_create_meta(instance) if hasattr(instance._meta.dal, 'event') and not force: return instance._meta.dal.event, False instance._meta.dal.event = None event = ModelEvent() event.user = AutomatedLoggingMiddleware.get_current_user() if settings.model.snapshot and extra: event.snapshot = instance if (settings.model.performance and hasattr(instance._meta.dal, 'performance') and extra): event.performance = datetime.now() - instance._meta.dal.performance instance._meta.dal.performance = None event.operation = operation event.entry = ModelEntry() event.entry.mirror = ModelMirror() event.entry.mirror.name = instance.__class__.__name__ event.entry.mirror.application = Application(name=instance._meta.app_label) event.entry.value = repr(instance) or str(instance) event.entry.primary_key = instance.pk instance._meta.dal.event = event return instance._meta.dal.event, True
def unspecified(self, record: LogRecord) -> None: """ This is for messages that are not sent from django-automated-logging. The option to still save these log messages is there. We create the event in the handler and then save them. :param record: :return: """ from automated_logging.models import UnspecifiedEvent, Application from automated_logging.signals import unspecified_exclusion from django.apps import apps event = UnspecifiedEvent() if hasattr(record, 'message'): event.message = record.message event.level = record.levelno event.line = record.lineno event.file = Path(record.pathname) # this is semi-reliable, but I am unsure of a better way to do this. applications = apps.app_configs.keys() path = Path(record.pathname) candidates = [p for p in path.parts if p in applications] if candidates: # use the last candidate (closest to file) event.application = Application(name=candidates[-1]) elif record.module in applications: # if we cannot find the application, we use the module as application event.application = Application(name=record.module) else: # if we cannot determine the application from the application # or from the module we presume that the application is unknown event.application = Application(name=None) if not unspecified_exclusion(event): self.prepare_save(event) self.save(event)
def test_mock_sender(self): from django.conf import settings from automated_logging.settings import settings as conf class MockModel: __module__ = '[TEST]' __name__ = 'MockModel' class MockMeta: app_label = None settings.AUTOMATED_LOGGING['model']['exclude']['unknown'] = True conf.load.cache_clear() self.assertTrue(model_exclusion(MockModel, MockMeta, Operation.CREATE)) settings.AUTOMATED_LOGGING['request']['exclude']['unknown'] = True conf.load.cache_clear() self.assertTrue( request_exclusion( RequestEvent(application=Application(name=None))))
def post_processor(sender, instance, model, operation, targets): """ if the change is in reverse or not, the processing of the changes is still the same, so we have this method to take care of constructing the changes :param sender: :param instance: :param model: :param operation: :param targets: :return: """ relationships = [] m2m_rel = find_m2m_rel(sender, model) if not m2m_rel: logger.warning( f'[DAL] save[m2m] could not find ManyToManyField for {instance}') return field = ModelField() field.name = m2m_rel.name field.mirror = ModelMirror( name=model.__name__, application=Application(name=instance._meta.app_label)) field.type = m2m_rel.__class__.__name__ # there is the possibility that a pre_clear occurred, if that is the case # extend the targets and pop the list of affected instances from the attached # field get_or_create_meta(instance) if (hasattr(instance._meta.dal, 'm2m_pre_clear') and field.name in instance._meta.dal.m2m_pre_clear and operation == Operation.DELETE): cleared = instance._meta.dal.m2m_pre_clear[field.name] targets.extend(cleared) instance._meta.dal.m2m_pre_clear.pop(field.name) for target in targets: relationship = ModelRelationshipModification() relationship.operation = operation relationship.field = field mirror = ModelMirror() mirror.name = target.__class__.__name__ mirror.application = Application(name=target._meta.app_label) relationship.entry = ModelEntry(mirror=mirror, value=repr(target), primary_key=target.pk) relationships.append(relationship) if len(relationships) == 0: # there was no actual change, so we're not propagating the event return event, _ = get_or_create_model_event(instance, operation) user = None logger.log( settings.model.loglevel, f'{user or "Anonymous"} modified field ' f'{field.name} | Model: ' f'{field.mirror.application}.{field.mirror} ' f'| Modifications: {", ".join([r.short() for r in relationships])}', extra={ 'action': 'model[m2m]', 'data': { 'instance': instance, 'sender': sender }, 'relationships': relationships, 'event': event, }, )
def pre_save_signal(sender, instance, **kwargs) -> None: """ Compares the current instance and old instance (fetched via the pk) and generates a dictionary of changes :param sender: :param instance: :param kwargs: :return: None """ get_or_create_meta(instance) # clear the event to be sure instance._meta.dal.event = None operation = Operation.MODIFY try: pre = sender.objects.get(pk=instance.pk) except ObjectDoesNotExist: # __dict__ is used on pre, therefore we need to create a function # that uses __dict__ too, but returns nothing. pre = lambda _: None operation = Operation.CREATE excluded = lazy_model_exclusion(instance, operation, instance.__class__) if excluded: return old, new = pre.__dict__, instance.__dict__ previously = set(k for k in old.keys() if not k.startswith('_') and old[k] is not None) currently = set(k for k in new.keys() if not k.startswith('_') and new[k] is not None) added = currently.difference(previously) deleted = previously.difference(currently) changed = ( set(k for k, v in set( # generate a set from the dict of old values # exclude protected and magic attributes (k, v) for k, v in old.items() if not k.startswith('_') ).difference( set( # generate a set from the dict of new values # also exclude the protected and magic attributes (k, v) for k, v in new.items() if not k.startswith('_'))) # the difference between the two sets # because the keys are the same, but values are different # will result in a change set of changed values # ('a', 'b') in old and new will get eliminated # ('a', 'b') in old and ('a', 'c') in new will # result in ('a', 'b') in the new set as that is # different. ) # remove all added and deleted attributes from the changelist # they would still be present, because None -> Value .difference(added).difference(deleted)) summary = [ *({ 'operation': Operation.CREATE, 'previous': None, 'current': new[k], 'key': k, } for k in added), *({ 'operation': Operation.DELETE, 'previous': old[k], 'current': None, 'key': k, } for k in deleted), *({ 'operation': Operation.MODIFY, 'previous': old[k], 'current': new[k], 'key': k, } for k in changed), ] # exclude fields not present in _meta.get_fields fields = {f.name: f for f in instance._meta.get_fields()} extra = { f.attname: f for f in instance._meta.get_fields() if hasattr(f, 'attname') } fields = {**extra, **fields} summary = [s for s in summary if s['key'] in fields.keys()] # field exclusion summary = [ s for s in summary if not field_exclusion(s['key'], instance, instance.__class__) ] model = ModelMirror() model.name = sender.__name__ model.application = Application(name=instance._meta.app_label) modifications = [] for entry in summary: field = ModelField() field.name = entry['key'] field.mirror = model field.type = fields[entry['key']].__class__.__name__ modification = ModelValueModification() modification.operation = entry['operation'] modification.field = field modification.previous = normalize_save_value(entry['previous']) modification.current = normalize_save_value(entry['current']) modifications.append(modification) instance._meta.dal.modifications = modifications if settings.model.performance: instance._meta.dal.performance = datetime.now()
def request_finished_signal(sender, **kwargs) -> None: """ This signal gets the environment from the local thread and sends a logging message, that message will be processed by the handler later on. This is a simple redirection. :return: - """ level = settings.request.loglevel environ = AutomatedLoggingMiddleware.get_current_environ() if not environ: if settings.request.log_request_was_not_recorded: logger.info( "Environment for request couldn't be determined. " "Request was not recorded." ) return request = RequestEvent() request.user = AutomatedLoggingMiddleware.get_current_user(environ) request.uri = environ.request.get_full_path() if not settings.request.data.query: request.uri = urllib.parse.urlparse(request.uri).path if 'request' in settings.request.data.enabled: request_context = RequestContext() request_context.content = environ.request.body request_context.type = environ.request.content_type request.request = request_context if 'response' in settings.request.data.enabled: response_context = RequestContext() response_context.content = environ.response.content response_context.type = environ.response['Content-Type'] request.response = response_context # TODO: context parsing, masking and removal if get_client_ip and settings.request.ip: request.ip, _ = get_client_ip(environ.request) request.status = environ.response.status_code if environ.response else None request.method = environ.request.method.upper() request.context_type = environ.request.content_type try: function = resolve(environ.request.path).func except Http404: function = None request.application = Application(name=None) if function: application = function.__module__.split('.')[0] request.application = Application(name=application) if request_exclusion(request, function): return logger.log( level, f'[{request.method}] [{request.status}] ' f'{getattr(request, "user", None) or "Anonymous"} ' f'at {request.uri}', extra={'action': 'request', 'event': request}, )