Przeglądaj źródła

Merge pull request #4315 from netbox-community/4195-application-logging

Closes #4195: Application logging
Jeremy Stretch 6 lat temu
rodzic
commit
2bd3f1fcc3

+ 8 - 0
docs/configuration/optional-settings.md

@@ -183,6 +183,14 @@ LOGGING = {
 }
 }
 ```
 ```
 
 
+### Available Loggers
+
+* `netbox.auth.*` - Authentication events
+* `netbox.api.views.*` - Views which handle business logic for the REST API
+* `netbox.reports.*` - Report execution (`module.name`)
+* `netbox.scripts.*` - Custom script execution (`module.name`)
+* `netbox.views.*` - Views which handle business logic for the web UI
+
 ---
 ---
 
 
 ## LOGIN_REQUIRED
 ## LOGIN_REQUIRED

+ 4 - 0
docs/release-notes/version-2.8.md

@@ -1,5 +1,9 @@
 # v2.8.0 (FUTURE)
 # v2.8.0 (FUTURE)
 
 
+## Enhancements
+
+* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](../configuration/optional-settings.md#logging))
+
 ## API Changes
 ## API Changes
 
 
 * dcim.Rack: The `/api/dcim/racks/<pk>/units/` endpoint has been replaced with `/api/dcim/racks/<pk>/elevation/`.
 * dcim.Rack: The `/api/dcim/racks/<pk>/units/` endpoint has been replaced with `/api/dcim/racks/<pk>/elevation/`.

+ 1 - 0
mkdocs.yml

@@ -63,6 +63,7 @@ nav:
         - Release Checklist: 'development/release-checklist.md'
         - Release Checklist: 'development/release-checklist.md'
         - Squashing Migrations: 'development/squashing-migrations.md'
         - Squashing Migrations: 'development/squashing-migrations.md'
     - Release Notes:
     - Release Notes:
+        - Version 2.8: 'release-notes/version-2.8.md'
         - Version 2.7: 'release-notes/version-2.7.md'
         - Version 2.7: 'release-notes/version-2.7.md'
         - Version 2.6: 'release-notes/version-2.6.md'
         - Version 2.6: 'release-notes/version-2.6.md'
         - Version 2.5: 'release-notes/version-2.5.md'
         - Version 2.5: 'release-notes/version-2.5.md'

+ 15 - 0
netbox/extras/reports.py

@@ -1,5 +1,6 @@
 import importlib
 import importlib
 import inspect
 import inspect
+import logging
 import pkgutil
 import pkgutil
 from collections import OrderedDict
 from collections import OrderedDict
 
 
@@ -91,6 +92,8 @@ class Report(object):
         self.active_test = None
         self.active_test = None
         self.failed = False
         self.failed = False
 
 
+        self.logger = logging.getLogger(f"netbox.reports.{self.module}.{self.name}")
+
         # Compile test methods and initialize results skeleton
         # Compile test methods and initialize results skeleton
         test_methods = []
         test_methods = []
         for method in dir(self):
         for method in dir(self):
@@ -138,6 +141,7 @@ class Report(object):
         Log a message which is not associated with a particular object.
         Log a message which is not associated with a particular object.
         """
         """
         self._log(None, message, level=LOG_DEFAULT)
         self._log(None, message, level=LOG_DEFAULT)
+        self.logger.info(message)
 
 
     def log_success(self, obj, message=None):
     def log_success(self, obj, message=None):
         """
         """
@@ -146,6 +150,7 @@ class Report(object):
         if message:
         if message:
             self._log(obj, message, level=LOG_SUCCESS)
             self._log(obj, message, level=LOG_SUCCESS)
         self._results[self.active_test]['success'] += 1
         self._results[self.active_test]['success'] += 1
+        self.logger.info(f"Success | {obj}: {message}")
 
 
     def log_info(self, obj, message):
     def log_info(self, obj, message):
         """
         """
@@ -153,6 +158,7 @@ class Report(object):
         """
         """
         self._log(obj, message, level=LOG_INFO)
         self._log(obj, message, level=LOG_INFO)
         self._results[self.active_test]['info'] += 1
         self._results[self.active_test]['info'] += 1
+        self.logger.info(f"Info | {obj}: {message}")
 
 
     def log_warning(self, obj, message):
     def log_warning(self, obj, message):
         """
         """
@@ -160,6 +166,7 @@ class Report(object):
         """
         """
         self._log(obj, message, level=LOG_WARNING)
         self._log(obj, message, level=LOG_WARNING)
         self._results[self.active_test]['warning'] += 1
         self._results[self.active_test]['warning'] += 1
+        self.logger.info(f"Warning | {obj}: {message}")
 
 
     def log_failure(self, obj, message):
     def log_failure(self, obj, message):
         """
         """
@@ -167,12 +174,15 @@ class Report(object):
         """
         """
         self._log(obj, message, level=LOG_FAILURE)
         self._log(obj, message, level=LOG_FAILURE)
         self._results[self.active_test]['failure'] += 1
         self._results[self.active_test]['failure'] += 1
+        self.logger.info(f"Failure | {obj}: {message}")
         self.failed = True
         self.failed = True
 
 
     def run(self):
     def run(self):
         """
         """
         Run the report and return its results. Each test method will be executed in order.
         Run the report and return its results. Each test method will be executed in order.
         """
         """
+        self.logger.info(f"Running report")
+
         for method_name in self.test_methods:
         for method_name in self.test_methods:
             self.active_test = method_name
             self.active_test = method_name
             test_method = getattr(self, method_name)
             test_method = getattr(self, method_name)
@@ -184,6 +194,11 @@ class Report(object):
         result.save()
         result.save()
         self.result = result
         self.result = result
 
 
+        if self.failed:
+            self.logger.warning("Report failed")
+        else:
+            self.logger.info("Report completed successfully")
+
         # Perform any post-run tasks
         # Perform any post-run tasks
         self.post_run()
         self.post_run()
 
 

+ 13 - 0
netbox/extras/scripts.py

@@ -1,5 +1,6 @@
 import inspect
 import inspect
 import json
 import json
+import logging
 import os
 import os
 import pkgutil
 import pkgutil
 import time
 import time
@@ -254,6 +255,7 @@ class BaseScript:
     def __init__(self):
     def __init__(self):
 
 
         # Initiate the log
         # Initiate the log
+        self.logger = logging.getLogger(f"netbox.scripts.{self.module()}.{self.__class__.__name__}")
         self.log = []
         self.log = []
 
 
         # Declare the placeholder for the current request
         # Declare the placeholder for the current request
@@ -301,18 +303,23 @@ class BaseScript:
     # Logging
     # Logging
 
 
     def log_debug(self, message):
     def log_debug(self, message):
+        self.logger.log(logging.DEBUG, message)
         self.log.append((LOG_DEFAULT, message))
         self.log.append((LOG_DEFAULT, message))
 
 
     def log_success(self, message):
     def log_success(self, message):
+        self.logger.log(logging.INFO, message)  # No syslog equivalent for SUCCESS
         self.log.append((LOG_SUCCESS, message))
         self.log.append((LOG_SUCCESS, message))
 
 
     def log_info(self, message):
     def log_info(self, message):
+        self.logger.log(logging.INFO, message)
         self.log.append((LOG_INFO, message))
         self.log.append((LOG_INFO, message))
 
 
     def log_warning(self, message):
     def log_warning(self, message):
+        self.logger.log(logging.WARNING, message)
         self.log.append((LOG_WARNING, message))
         self.log.append((LOG_WARNING, message))
 
 
     def log_failure(self, message):
     def log_failure(self, message):
+        self.logger.log(logging.ERROR, message)
         self.log.append((LOG_FAILURE, message))
         self.log.append((LOG_FAILURE, message))
 
 
     # Convenience functions
     # Convenience functions
@@ -375,6 +382,10 @@ def run_script(script, data, request, commit=True):
     start_time = None
     start_time = None
     end_time = None
     end_time = None
 
 
+    script_name = script.__class__.__name__
+    logger = logging.getLogger(f"netbox.scripts.{script.module()}.{script_name}")
+    logger.info(f"Running script (commit={commit})")
+
     # Add files to form data
     # Add files to form data
     files = request.FILES
     files = request.FILES
     for field_name, fileobj in files.items():
     for field_name, fileobj in files.items():
@@ -404,6 +415,7 @@ def run_script(script, data, request, commit=True):
         script.log_failure(
         script.log_failure(
             "An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace)
             "An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace)
         )
         )
+        logger.error(f"Exception raised during script execution: {e}")
         commit = False
         commit = False
     finally:
     finally:
         if not commit:
         if not commit:
@@ -416,6 +428,7 @@ def run_script(script, data, request, commit=True):
     # Calculate execution time
     # Calculate execution time
     if end_time is not None:
     if end_time is not None:
         execution_time = end_time - start_time
         execution_time = end_time - start_time
+        logger.info(f"Script completed in {execution_time:.4f} seconds")
     else:
     else:
         execution_time = None
         execution_time = None
 
 

+ 23 - 3
netbox/users/views.py

@@ -1,3 +1,5 @@
+import logging
+
 from django.conf import settings
 from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
 from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
@@ -24,6 +26,9 @@ from .models import Token
 #
 #
 
 
 class LoginView(View):
 class LoginView(View):
+    """
+    Perform user authentication via the web UI.
+    """
     template_name = 'login.html'
     template_name = 'login.html'
 
 
     @method_decorator(sensitive_post_parameters('password'))
     @method_decorator(sensitive_post_parameters('password'))
@@ -38,36 +43,51 @@ class LoginView(View):
         })
         })
 
 
     def post(self, request):
     def post(self, request):
+        logger = logging.getLogger('netbox.auth.login')
         form = LoginForm(request, data=request.POST)
         form = LoginForm(request, data=request.POST)
+
         if form.is_valid():
         if form.is_valid():
+            logger.debug("Login form validation was successful")
 
 
             # Determine where to direct user after successful login
             # Determine where to direct user after successful login
-            redirect_to = request.POST.get('next', '')
-            if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
+            redirect_to = request.POST.get('next')
+            if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
+                logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
                 redirect_to = reverse('home')
                 redirect_to = reverse('home')
 
 
             # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
             # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
             # last_login time upon authentication.
             # last_login time upon authentication.
             if settings.MAINTENANCE_MODE:
             if settings.MAINTENANCE_MODE:
+                logger.warning("Maintenance mode enabled: disabling update of most recent login time")
                 user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
                 user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
 
 
             # Authenticate user
             # Authenticate user
             auth_login(request, form.get_user())
             auth_login(request, form.get_user())
+            logger.info(f"User {request.user} successfully authenticated")
             messages.info(request, "Logged in as {}.".format(request.user))
             messages.info(request, "Logged in as {}.".format(request.user))
 
 
+            logger.debug(f"Redirecting user to {redirect_to}")
             return HttpResponseRedirect(redirect_to)
             return HttpResponseRedirect(redirect_to)
 
 
+        else:
+            logger.debug("Login form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
         })
         })
 
 
 
 
 class LogoutView(View):
 class LogoutView(View):
-
+    """
+    Deauthenticate a web user.
+    """
     def get(self, request):
     def get(self, request):
+        logger = logging.getLogger('netbox.auth.logout')
 
 
         # Log out the user
         # Log out the user
+        username = request.user
         auth_logout(request)
         auth_logout(request)
+        logger.info(f"User {username} has logged out")
         messages.info(request, "You have logged out.")
         messages.info(request, "You have logged out.")
 
 
         # Delete session key cookie (if set) upon logout
         # Delete session key cookie (if set) upon logout

+ 34 - 3
netbox/utilities/api.py

@@ -1,3 +1,4 @@
+import logging
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 import pytz
 import pytz
@@ -303,25 +304,35 @@ class ModelViewSet(_ModelViewSet):
         return super().get_serializer(*args, **kwargs)
         return super().get_serializer(*args, **kwargs)
 
 
     def get_serializer_class(self):
     def get_serializer_class(self):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
 
 
         # If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
         # If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
         # exists
         # exists
         request = self.get_serializer_context()['request']
         request = self.get_serializer_context()['request']
-        if request.query_params.get('brief', False):
+        if request.query_params.get('brief'):
+            logger.debug("Request is for 'brief' format; initializing nested serializer")
             try:
             try:
-                return get_serializer_for_model(self.queryset.model, prefix='Nested')
+                serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
+                logger.debug(f"Using serializer {serializer}")
+                return serializer
             except SerializerNotFound:
             except SerializerNotFound:
                 pass
                 pass
 
 
         # Fall back to the hard-coded serializer class
         # Fall back to the hard-coded serializer class
+        logger.debug(f"Using serializer {self.serializer_class}")
         return self.serializer_class
         return self.serializer_class
 
 
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+
         try:
         try:
             return super().dispatch(request, *args, **kwargs)
             return super().dispatch(request, *args, **kwargs)
         except ProtectedError as e:
         except ProtectedError as e:
-            models = ['{} ({})'.format(o, o._meta) for o in e.protected_objects.all()]
+            models = [
+                '{} ({})'.format(o, o._meta) for o in e.protected_objects.all()
+            ]
             msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
             msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
+            logger.warning(msg)
             return self.finalize_response(
             return self.finalize_response(
                 request,
                 request,
                 Response({'detail': msg}, status=409),
                 Response({'detail': msg}, status=409),
@@ -341,6 +352,26 @@ class ModelViewSet(_ModelViewSet):
         """
         """
         return super().retrieve(*args, **kwargs)
         return super().retrieve(*args, **kwargs)
 
 
+    #
+    # Logging
+    #
+
+    def perform_create(self, serializer):
+        model = serializer.child.Meta.model if hasattr(serializer, 'many') else serializer.Meta.model
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Creating new {model._meta.verbose_name}")
+        return super().perform_create(serializer)
+
+    def perform_update(self, serializer):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Updating {serializer.instance} (PK: {serializer.instance.pk})")
+        return super().perform_update(serializer)
+
+    def perform_destroy(self, instance):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Deleting {instance} (PK: {instance.pk})")
+        return super().perform_destroy(instance)
+
 
 
 class FieldChoicesViewSet(ViewSet):
 class FieldChoicesViewSet(ViewSet):
     """
     """

+ 80 - 33
netbox/utilities/views.py

@@ -1,3 +1,4 @@
+import logging
 import sys
 import sys
 from copy import deepcopy
 from copy import deepcopy
 
 
@@ -219,35 +220,36 @@ class ObjectEditView(GetReturnURLMixin, View):
         # given some parameter from the request URL.
         # given some parameter from the request URL.
         return obj
         return obj
 
 
-    def get(self, request, *args, **kwargs):
+    def dispatch(self, request, *args, **kwargs):
+        self.obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
 
 
-        obj = self.get_object(kwargs)
-        obj = self.alter_obj(obj, request, args, kwargs)
+        return super().dispatch(request, *args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
         # Parse initial data manually to avoid setting field values as lists
         # Parse initial data manually to avoid setting field values as lists
         initial_data = {k: request.GET[k] for k in request.GET}
         initial_data = {k: request.GET[k] for k in request.GET}
-        form = self.model_form(instance=obj, initial=initial_data)
+        form = self.model_form(instance=self.obj, initial=initial_data)
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
-            'obj': obj,
+            'obj': self.obj,
             'obj_type': self.model._meta.verbose_name,
             'obj_type': self.model._meta.verbose_name,
             'form': form,
             'form': form,
-            'return_url': self.get_return_url(request, obj),
+            'return_url': self.get_return_url(request, self.obj),
         })
         })
 
 
     def post(self, request, *args, **kwargs):
     def post(self, request, *args, **kwargs):
-
-        obj = self.get_object(kwargs)
-        obj = self.alter_obj(obj, request, args, kwargs)
-        form = self.model_form(request.POST, request.FILES, instance=obj)
+        logger = logging.getLogger('netbox.views.ObjectEditView')
+        form = self.model_form(request.POST, request.FILES, instance=self.obj)
 
 
         if form.is_valid():
         if form.is_valid():
-            obj_created = not form.instance.pk
-            obj = form.save()
+            logger.debug("Form validation was successful")
 
 
+            obj = form.save()
             msg = '{} {}'.format(
             msg = '{} {}'.format(
-                'Created' if obj_created else 'Modified',
+                'Created' if not form.instance.pk else 'Modified',
                 self.model._meta.verbose_name
                 self.model._meta.verbose_name
             )
             )
+            logger.info(f"{msg} {obj} (PK: {obj.pk})")
             if hasattr(obj, 'get_absolute_url'):
             if hasattr(obj, 'get_absolute_url'):
                 msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
                 msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
             else:
             else:
@@ -269,11 +271,14 @@ class ObjectEditView(GetReturnURLMixin, View):
             else:
             else:
                 return redirect(self.get_return_url(request, obj))
                 return redirect(self.get_return_url(request, obj))
 
 
+        else:
+            logger.debug("Form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
-            'obj': obj,
+            'obj': self.obj,
             'obj_type': self.model._meta.verbose_name,
             'obj_type': self.model._meta.verbose_name,
             'form': form,
             'form': form,
-            'return_url': self.get_return_url(request, obj),
+            'return_url': self.get_return_url(request, self.obj),
         })
         })
 
 
 
 
@@ -295,7 +300,6 @@ class ObjectDeleteView(GetReturnURLMixin, View):
             return get_object_or_404(self.model, pk=kwargs['pk'])
             return get_object_or_404(self.model, pk=kwargs['pk'])
 
 
     def get(self, request, **kwargs):
     def get(self, request, **kwargs):
-
         obj = self.get_object(kwargs)
         obj = self.get_object(kwargs)
         form = ConfirmationForm(initial=request.GET)
         form = ConfirmationForm(initial=request.GET)
 
 
@@ -307,18 +311,22 @@ class ObjectDeleteView(GetReturnURLMixin, View):
         })
         })
 
 
     def post(self, request, **kwargs):
     def post(self, request, **kwargs):
-
+        logger = logging.getLogger('netbox.views.ObjectDeleteView')
         obj = self.get_object(kwargs)
         obj = self.get_object(kwargs)
         form = ConfirmationForm(request.POST)
         form = ConfirmationForm(request.POST)
+
         if form.is_valid():
         if form.is_valid():
+            logger.debug("Form validation was successful")
 
 
             try:
             try:
                 obj.delete()
                 obj.delete()
             except ProtectedError as e:
             except ProtectedError as e:
+                logger.info("Caught ProtectedError while attempting to delete object")
                 handle_protectederror(obj, request, e)
                 handle_protectederror(obj, request, e)
                 return redirect(obj.get_absolute_url())
                 return redirect(obj.get_absolute_url())
 
 
             msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
             msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
+            logger.info(msg)
             messages.success(request, msg)
             messages.success(request, msg)
 
 
             return_url = form.cleaned_data.get('return_url')
             return_url = form.cleaned_data.get('return_url')
@@ -327,6 +335,9 @@ class ObjectDeleteView(GetReturnURLMixin, View):
             else:
             else:
                 return redirect(self.get_return_url(request, obj))
                 return redirect(self.get_return_url(request, obj))
 
 
+        else:
+            logger.debug("Form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'obj': obj,
             'obj': obj,
             'form': form,
             'form': form,
@@ -350,7 +361,6 @@ class BulkCreateView(GetReturnURLMixin, View):
     template_name = None
     template_name = None
 
 
     def get(self, request):
     def get(self, request):
-
         # Set initial values for visible form fields from query args
         # Set initial values for visible form fields from query args
         initial = {}
         initial = {}
         for field in getattr(self.model_form._meta, 'fields', []):
         for field in getattr(self.model_form._meta, 'fields', []):
@@ -368,13 +378,13 @@ class BulkCreateView(GetReturnURLMixin, View):
         })
         })
 
 
     def post(self, request):
     def post(self, request):
-
+        logger = logging.getLogger('netbox.views.BulkCreateView')
         model = self.model_form._meta.model
         model = self.model_form._meta.model
         form = self.form(request.POST)
         form = self.form(request.POST)
         model_form = self.model_form(request.POST)
         model_form = self.model_form(request.POST)
 
 
         if form.is_valid():
         if form.is_valid():
-
+            logger.debug("Form validation was successful")
             pattern = form.cleaned_data['pattern']
             pattern = form.cleaned_data['pattern']
             new_objs = []
             new_objs = []
 
 
@@ -392,6 +402,7 @@ class BulkCreateView(GetReturnURLMixin, View):
                         # Validate each new object independently.
                         # Validate each new object independently.
                         if model_form.is_valid():
                         if model_form.is_valid():
                             obj = model_form.save()
                             obj = model_form.save()
+                            logger.debug(f"Created {obj} (PK: {obj.pk})")
                             new_objs.append(obj)
                             new_objs.append(obj)
                         else:
                         else:
                             # Copy any errors on the pattern target field to the pattern form.
                             # Copy any errors on the pattern target field to the pattern form.
@@ -403,6 +414,7 @@ class BulkCreateView(GetReturnURLMixin, View):
 
 
                     # If we make it to this point, validation has succeeded on all new objects.
                     # If we make it to this point, validation has succeeded on all new objects.
                     msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
                     msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
+                    logger.info(msg)
                     messages.success(request, msg)
                     messages.success(request, msg)
 
 
                     if '_addanother' in request.POST:
                     if '_addanother' in request.POST:
@@ -412,6 +424,9 @@ class BulkCreateView(GetReturnURLMixin, View):
             except IntegrityError:
             except IntegrityError:
                 pass
                 pass
 
 
+        else:
+            logger.debug("Form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
             'model_form': model_form,
             'model_form': model_form,
@@ -430,7 +445,6 @@ class ObjectImportView(GetReturnURLMixin, View):
     template_name = 'utilities/obj_import.html'
     template_name = 'utilities/obj_import.html'
 
 
     def get(self, request):
     def get(self, request):
-
         form = ImportForm()
         form = ImportForm()
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
@@ -440,9 +454,11 @@ class ObjectImportView(GetReturnURLMixin, View):
         })
         })
 
 
     def post(self, request):
     def post(self, request):
-
+        logger = logging.getLogger('netbox.views.ObjectImportView')
         form = ImportForm(request.POST)
         form = ImportForm(request.POST)
+
         if form.is_valid():
         if form.is_valid():
+            logger.debug("Import form validation was successful")
 
 
             # Initialize model form
             # Initialize model form
             data = form.cleaned_data['data']
             data = form.cleaned_data['data']
@@ -463,9 +479,11 @@ class ObjectImportView(GetReturnURLMixin, View):
 
 
                         # Save the primary object
                         # Save the primary object
                         obj = model_form.save()
                         obj = model_form.save()
+                        logger.debug(f"Created {obj} (PK: {obj.pk})")
 
 
                         # Iterate through the related object forms (if any), validating and saving each instance.
                         # Iterate through the related object forms (if any), validating and saving each instance.
                         for field_name, related_object_form in self.related_object_forms.items():
                         for field_name, related_object_form in self.related_object_forms.items():
+                            logger.debug("Processing form for related objects: {related_object_form}")
 
 
                             for i, rel_obj_data in enumerate(data.get(field_name, list())):
                             for i, rel_obj_data in enumerate(data.get(field_name, list())):
 
 
@@ -489,7 +507,7 @@ class ObjectImportView(GetReturnURLMixin, View):
                     pass
                     pass
 
 
             if not model_form.errors:
             if not model_form.errors:
-
+                logger.info(f"Import object {obj} (PK: {obj.pk})")
                 messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
                 messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
                     obj.get_absolute_url(), obj
                     obj.get_absolute_url(), obj
                 )))
                 )))
@@ -504,6 +522,7 @@ class ObjectImportView(GetReturnURLMixin, View):
                     return redirect(self.get_return_url(request, obj))
                     return redirect(self.get_return_url(request, obj))
 
 
             else:
             else:
+                logger.debug("Model form validation failed")
 
 
                 # Replicate model form errors for display
                 # Replicate model form errors for display
                 for field, errors in model_form.errors.items():
                 for field, errors in model_form.errors.items():
@@ -513,6 +532,9 @@ class ObjectImportView(GetReturnURLMixin, View):
                         else:
                         else:
                             form.add_error(None, "{}: {}".format(field, err))
                             form.add_error(None, "{}: {}".format(field, err))
 
 
+        else:
+            logger.debug("Import form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
             'obj_type': self.model._meta.verbose_name,
             'obj_type': self.model._meta.verbose_name,
@@ -560,14 +582,14 @@ class BulkImportView(GetReturnURLMixin, View):
         })
         })
 
 
     def post(self, request):
     def post(self, request):
-
+        logger = logging.getLogger('netbox.views.BulkImportView')
         new_objs = []
         new_objs = []
         form = self._import_form(request.POST)
         form = self._import_form(request.POST)
 
 
         if form.is_valid():
         if form.is_valid():
+            logger.debug("Form validation was successful")
 
 
             try:
             try:
-
                 # Iterate through CSV data and bind each row to a new model form instance.
                 # Iterate through CSV data and bind each row to a new model form instance.
                 with transaction.atomic():
                 with transaction.atomic():
                     for row, data in enumerate(form.cleaned_data['csv'], start=1):
                     for row, data in enumerate(form.cleaned_data['csv'], start=1):
@@ -585,6 +607,7 @@ class BulkImportView(GetReturnURLMixin, View):
 
 
                 if new_objs:
                 if new_objs:
                     msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
                     msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
+                    logger.info(msg)
                     messages.success(request, msg)
                     messages.success(request, msg)
 
 
                     return render(request, "import_success.html", {
                     return render(request, "import_success.html", {
@@ -595,6 +618,9 @@ class BulkImportView(GetReturnURLMixin, View):
             except ValidationError:
             except ValidationError:
                 pass
                 pass
 
 
+        else:
+            logger.debug("Form validation failed")
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
             'fields': self.model_form().fields,
             'fields': self.model_form().fields,
@@ -623,7 +649,7 @@ class BulkEditView(GetReturnURLMixin, View):
         return redirect(self.get_return_url(request))
         return redirect(self.get_return_url(request))
 
 
     def post(self, request, **kwargs):
     def post(self, request, **kwargs):
-
+        logger = logging.getLogger('netbox.views.BulkEditView')
         model = self.queryset.model
         model = self.queryset.model
 
 
         # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
         # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
@@ -636,8 +662,9 @@ class BulkEditView(GetReturnURLMixin, View):
 
 
         if '_apply' in request.POST:
         if '_apply' in request.POST:
             form = self.form(model, request.POST)
             form = self.form(model, request.POST)
-            if form.is_valid():
 
 
+            if form.is_valid():
+                logger.debug("Form validation was successful")
                 custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
                 custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
                 standard_fields = [
                 standard_fields = [
                     field for field in form.fields if field not in custom_fields + ['pk']
                     field for field in form.fields if field not in custom_fields + ['pk']
@@ -677,6 +704,7 @@ class BulkEditView(GetReturnURLMixin, View):
 
 
                             obj.full_clean()
                             obj.full_clean()
                             obj.save()
                             obj.save()
+                            logger.debug(f"Saved {obj} (PK: {obj.pk})")
 
 
                             # Update custom fields
                             # Update custom fields
                             obj_type = ContentType.objects.get_for_model(model)
                             obj_type = ContentType.objects.get_for_model(model)
@@ -697,6 +725,7 @@ class BulkEditView(GetReturnURLMixin, View):
                                         )
                                         )
                                     cfv.value = form.cleaned_data[name]
                                     cfv.value = form.cleaned_data[name]
                                     cfv.save()
                                     cfv.save()
+                            logger.debug(f"Saved custom fields for {obj} (PK: {obj.pk})")
 
 
                             # Add/remove tags
                             # Add/remove tags
                             if form.cleaned_data.get('add_tags', None):
                             if form.cleaned_data.get('add_tags', None):
@@ -708,6 +737,7 @@ class BulkEditView(GetReturnURLMixin, View):
 
 
                     if updated_count:
                     if updated_count:
                         msg = 'Updated {} {}'.format(updated_count, model._meta.verbose_name_plural)
                         msg = 'Updated {} {}'.format(updated_count, model._meta.verbose_name_plural)
+                        logger.info(msg)
                         messages.success(self.request, msg)
                         messages.success(self.request, msg)
 
 
                     return redirect(self.get_return_url(request))
                     return redirect(self.get_return_url(request))
@@ -715,6 +745,9 @@ class BulkEditView(GetReturnURLMixin, View):
                 except ValidationError as e:
                 except ValidationError as e:
                     messages.error(self.request, "{} failed validation: {}".format(obj, e))
                     messages.error(self.request, "{} failed validation: {}".format(obj, e))
 
 
+            else:
+                logger.debug("Form validation failed")
+
         else:
         else:
             # Include the PK list as initial data for the form
             # Include the PK list as initial data for the form
             initial_data = {'pk': pk_list}
             initial_data = {'pk': pk_list}
@@ -761,7 +794,7 @@ class BulkDeleteView(GetReturnURLMixin, View):
         return redirect(self.get_return_url(request))
         return redirect(self.get_return_url(request))
 
 
     def post(self, request, **kwargs):
     def post(self, request, **kwargs):
-
+        logger = logging.getLogger('netbox.views.BulkDeleteView')
         model = self.queryset.model
         model = self.queryset.model
 
 
         # Are we deleting *all* objects in the queryset or just a selected subset?
         # Are we deleting *all* objects in the queryset or just a selected subset?
@@ -778,19 +811,25 @@ class BulkDeleteView(GetReturnURLMixin, View):
         if '_confirm' in request.POST:
         if '_confirm' in request.POST:
             form = form_cls(request.POST)
             form = form_cls(request.POST)
             if form.is_valid():
             if form.is_valid():
+                logger.debug("Form validation was successful")
 
 
                 # Delete objects
                 # Delete objects
                 queryset = model.objects.filter(pk__in=pk_list)
                 queryset = model.objects.filter(pk__in=pk_list)
                 try:
                 try:
                     deleted_count = queryset.delete()[1][model._meta.label]
                     deleted_count = queryset.delete()[1][model._meta.label]
                 except ProtectedError as e:
                 except ProtectedError as e:
+                    logger.info("Caught ProtectedError while attempting to delete objects")
                     handle_protectederror(list(queryset), request, e)
                     handle_protectederror(list(queryset), request, e)
                     return redirect(self.get_return_url(request))
                     return redirect(self.get_return_url(request))
 
 
                 msg = 'Deleted {} {}'.format(deleted_count, model._meta.verbose_name_plural)
                 msg = 'Deleted {} {}'.format(deleted_count, model._meta.verbose_name_plural)
+                logger.info(msg)
                 messages.success(request, msg)
                 messages.success(request, msg)
                 return redirect(self.get_return_url(request))
                 return redirect(self.get_return_url(request))
 
 
+            else:
+                logger.debug("Form validation failed")
+
         else:
         else:
             form = form_cls(initial={
             form = form_cls(initial={
                 'pk': pk_list,
                 'pk': pk_list,
@@ -814,12 +853,12 @@ class BulkDeleteView(GetReturnURLMixin, View):
         """
         """
         Provide a standard bulk delete form if none has been specified for the view
         Provide a standard bulk delete form if none has been specified for the view
         """
         """
-
         class BulkDeleteForm(ConfirmationForm):
         class BulkDeleteForm(ConfirmationForm):
             pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
             pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
 
 
         if self.form:
         if self.form:
             return self.form
             return self.form
+
         return BulkDeleteForm
         return BulkDeleteForm
 
 
 
 
@@ -908,7 +947,7 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
     template_name = 'utilities/obj_bulk_add_component.html'
     template_name = 'utilities/obj_bulk_add_component.html'
 
 
     def post(self, request):
     def post(self, request):
-
+        logger = logging.getLogger('netbox.views.BulkComponentCreateView')
         parent_model_name = self.parent_model._meta.verbose_name_plural
         parent_model_name = self.parent_model._meta.verbose_name_plural
         model_name = self.model._meta.verbose_name_plural
         model_name = self.model._meta.verbose_name_plural
 
 
@@ -926,10 +965,13 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
 
 
         if '_create' in request.POST:
         if '_create' in request.POST:
             form = self.form(request.POST)
             form = self.form(request.POST)
+
             if form.is_valid():
             if form.is_valid():
+                logger.debug("Form validation was successful")
 
 
                 new_components = []
                 new_components = []
                 data = deepcopy(form.cleaned_data)
                 data = deepcopy(form.cleaned_data)
+
                 for obj in data['pk']:
                 for obj in data['pk']:
 
 
                     names = data['name_pattern']
                     names = data['name_pattern']
@@ -949,15 +991,20 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
 
 
                 if not form.errors:
                 if not form.errors:
                     self.model.objects.bulk_create(new_components)
                     self.model.objects.bulk_create(new_components)
-
-                    messages.success(request, "Added {} {} to {} {}.".format(
+                    msg = "Added {} {} to {} {}.".format(
                         len(new_components),
                         len(new_components),
                         model_name,
                         model_name,
                         len(form.cleaned_data['pk']),
                         len(form.cleaned_data['pk']),
                         parent_model_name
                         parent_model_name
-                    ))
+                    )
+                    logger.info(msg)
+                    messages.success(request, msg)
+
                     return redirect(self.get_return_url(request))
                     return redirect(self.get_return_url(request))
 
 
+            else:
+                logger.debug("Form validation failed")
+
         else:
         else:
             form = self.form(initial={'pk': pk_list})
             form = self.form(initial={'pk': pk_list})