The Not-So-Subtle Art of Debugging

Blog Detail

Debugging is a crucial aspect of software development that ensures the quality and functionality of the final product. In this post, we provide valuable tips and tricks in the context of Nautobot to help streamline the debugging process and maximize efficiency.

Introduction

The development workflow of an app:

  1. Put some code together
  2. Publish
  3. Receive praise for your amazing job

Right?

Well, not really. Programming is a complex task that requires careful planning and, more often than not, some iteration. Even with many controls and care, it will still present challenges along the way. As a Nautobot App developer, you want the tools to understand what is happening in those situations where things are not behaving as expected. Here, we present one of the tools you need in your toolbox as a Nautobot App developer: debugging. What we offer here is not specific to Nautobot, but we want to present it in the context of Nautobot App development and provide some value by reviewing these tools in the proper context for you.

One of the fantastic characteristics of Nautobot is its extension capabilities. This includes custom fields, computed fields, tags, etc. However, its power goes beyond that if you are willing to put some extra code together. Nautobot Apps are a powerful option that has virtually no limit to what you can do. The Developer Guide is an excellent place to start.

While developing your app, you have to spend so many hours with your code that sometimes you receive an error report from a user, or you see an error in a log, and right away, you know where the error is. Sometimes, you can see the actual line of code and the change you need to make just in your head. Other times, you have no idea.

Debugging can be helpful for those cases where you need to investigate what is happening. Programming is sometimes like a puzzle and debugging helps you to figure it out. Here, we want to provide some introductory guidance as well as some practical tips on debugging Nautobot and Nautobot Apps.

What Is Debugging?

Debugging is the process of finding and resolving bugs (i.e., errors in the code). Here, however, we narrow down the conversation to using the debugger, a software tool that allows us to monitor and understand the execution of our code. This is also known as interactive debugging, but we will call it just debugging for simplicity.

The Python ecosystem provides a standard debugger: pdb. This debugger is part of the standard library and will help us navigate through our code. The most important part of the process is the ability to decide where in your code you want to stop and have the possibility to look around. You can, for example, see which variables are available, which values they have, and much more.

Before moving into a more practical section, we just want to emphasize that debugging is one of the tools you need to have under your belt, but it is definitely not the only one. We see many engineers tackling their issues with tools such as loggers, profilers, static code analysis, etc. As always, the right tool for the right problem should be the guiding principle.

For some previous examples on the use of the debugger, you can check our previous post “pdb – How to Debug Your Code Like a Pro”.

Debugging Apps

Regardless of where in your app you are facing a problem, as long as you have Python code, you can just drop a breakpoint() and you are ready to look around. It could be a view, a model, or a test. You have your debugger available at all times.

A nice-to-have tool is pdb++. The standard pdb will give you all you need to work on your troubleshooting, but pdb++ is a drop-in replacement that will support all pdb commands and will give you some extra niceties, such as formatting and a sticky mode so you always know what is happening. There is, of course, the possibility of using an IDE debugging option. Even if that is your preferred choice, remember that it is usually a wrapper around pdb. You will find that in some situations, the IDE tools will not be available. And because they are GUI-based, IDE tools do not have as much flexibility.

You can install pdb++ easily with pip.

pip install pdbpp

Once you have pdb or pdb++ and you insert your breakpoint, you just run your app, and you are there. Let’s see, as an example, some code on the clean() method of a custom model with a breakpoint on the first line.

def clean (self):
    """Validate data key name is unique and it's in snake case."""
    breakpoint()
    super().clean()

    if not is_string_snake_case(self.data_key_name):
        raise ValidationError(
            {
                "data_key_name": "Value must only contain lowercase characters,"
                "words separated by underscore(_), and cannot start with a digit."
            }
    	)

When executing your code, you will have your interactive prompt.

It is recommended to enable the sticky mode if you are using pdb++; that way, the screen auto-refreshes whenever you move.

At this point, it is important to get familiar with the pdb commands. The basic ones include s(step), n(next), j(jump), c(continue). The official documentation of pdb will provide a comprehensive list of commands, but these basic ones can take you a long way down the road.

In the following sections, we will provide practical tips to make debugging easier and more effective in some specific cases.

Debugging Jobs

One typical case when building Nautobot Apps is the creation of jobs. Jobs are incredible tools because you are free to implement any kind of processing that you might need. You can process data within the application, integrate it with other systems, run compliance routines, etc. The important consideration in debugging is understanding that jobs run in a worker process that takes advantage of Celery. This means that, by default, the processing happens outside of the HTTP request lifecycle, which, of course, is what we want when our apps are running in production. However, when trying to understand the code and debug, it would be interesting to have a way to step through the code in the same manner we do it with other parts of our code.

The nautobot_config.py file allows you to add a configuration line that helps you accomplish this in your development environment. Needless to say, we should avoid this in production deployments.

CELERY_TASK_ALWAYS_EAGER=True

After this small configuration change, you can use breakpoint() in your jobs as you can anywhere else in the application.

A common situation when debugging is that you will need to repeat the process a couple of times while you create a mental model of what is happening. To do this with other parts of the application, you usually repeat the process. If you are testing the clean() method of a particular model, for example, you will need to create a couple of objects from the UI before you eventually figure it out. In the case of jobs, you will also execute your debugger multiple times. For this reason, an excellent time-saver option is to run your job from the command line. It might take a couple of minutes to put together the specific command with all the input parameters; but after you have it, you can use it multiple times, and it will give you back some time. The syntax for this is presented in the following command:

nautobot-server run_job <grouping_name>/<module_name>/<JobClassName>

You can also use

nautobot-server run_job --local <grouping_name>/<module_name>/<JobClassName>

The extra argument --local allows you to avoid running on a worker.

Debugging Unit Tests

Projects of significant size with actual stakes require that we follow best software engineering practices. One of those dreaded ones is the utilization of unit tests. We all hate unit tests, but we make peace with the idea because of its importance. Unit tests are essential in order to provide reliable, non-breaking software.

Unit tests are also code, and that means that we face problems similar to those that our app itself presents. Thus, it is not uncommon to have misbehaving unit tests that we need to debug and correct.

In nautobot, we can execute the full battery of tests with one single command:

invoke test

This presents two challenges from a debugging perspective:

  1. It runs it all, when we usually want to analyze one particular problematic test.
  2. It always creates and deletes the database.

These two points result in long waiting times, making the debugging much more cumbersome. Luckily, we have ways to work around this.

First, we can execute a specific unit test using the following syntax:

nautobot-server test nautobot_app_name.tests.test_file.MyTestCase.test_method

This gives us the control to execute only the necessary code, which makes it easier to replicate and reduces waiting time.

Second, most of the time, we can skip the costly process of creating and deleting the database—reducing the time it takes to run our tests. When running our tests, we can accomplish that using the additional flag --keepdb. For example, we can adapt our previous command to look like this:

nautobot-server test --keepdb nautobot_app_name.tests.test_file.MyTestCase.test_method

We usually use these two tips together to reduce the waiting time as much as possible to provide a better debugging experience.

Running all your tests is a must to make sure that your project is working as expected; this will usually also be tested in some CI/CD pipeline. But, regardless of how you run your tests, it is important to run the full battery before your deployment. The two tips we provide in this section are to troubleshoot, but that does not mean that it replaces practices and procedures for deployment.

Making Debugging Easier

Not all code is the same when it comes to debugging. Some patterns are harder and more cumbersome to understand when working with your debugger. You should try to make your code easier to understand from the get-go. The good news is that making your code more amenable for debugging does not require anything that you are not aware of already. It all boils down to following good practices and patterns to have clean code.

We would encourage you to have a couple of things in mind when coding that can make your debugging experience more enjoyable.

  1. Prefer linear flows (debugging multiple nested expressions is cumbersome).
  2. Keep your functions small (being able to see the right context is important when debugging).

These two simple things will help your app in general and are not specific to debugging, but they will make a big difference when you are chasing down those nasty bugs.


Conclusion

We all like coding, particularly building those sweet Nautobot Apps that will tremendously help our organization in its network automation efforts. We will definitely find ourselves in some situations where things go south; and having tools such as a debugger in our toolset could be the difference between rapid and efficient troubleshooting and a big headache. We hope that you feel energized about these tools and that you take these tips with you for the next time you face some coding challenges. This is your quick intro to the not-so-subtle art of debugging.

-Israel Pineda



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!