示例#1
0
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))))
示例#4
0
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,
        },
    )
示例#5
0
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()
示例#6
0
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},
    )