Combating Legacy Code

I wrote some notes about to write a post about a software project I worked on a year and a half ago, that I think is pretty cool, but I was on writing hiatus. Even better the specific code in question is now no longer in use. But I think it serves as a useful parable, but I will attempt to reflect.

Go's logging [2] support in standard library works, and it successfully achieves its goals on its own terms. The problem is that it's incredibly simple and lacks a number of features that are standard in most logging systems. [3] So as a result, I'm not surprised that most applications of consequence either use a couple of more fully featured logging packages or end up writing a large number of logging wrappers.

The fact that my project at work was using a special logging library is not particularly surprising, particularly because the project is old for a Go project. The logging library in question is a log4j-inspired package, that had been developed by a different group internally, but was no longer being used by that group. It worked, but there were a host of problems. [4]

I'd also written a logging package myself which was a definite improvement on the state of the art. I had two chief problems:

  • how to convince teammates to make the change,
  • how to make the change without disrupting ongoing work or the functioning of the system which had to be always deploy-able.

Here's what I did...

First, I learned as much as I could about the existing system, it's history and how we used it. I read a lot of code, documentation (such as it was,) and also related bug reports, feature requests, and history.

Second, I implemented wrappers for my system that (mostly) cloned the interfaces for the existing library in my own package. It's called slogger, and it's still there, though I hope to delete it soon. I wanted to make it possible to make the switch [1] in the project initialization without needing to change every last logging statement. [5]

Then, we actually made the change so that logging used the new code internally but wrapped by the old interfaces. I think there were a couple of very obvious bugs early on, but frankly none of them are so memorable that I could describe them any more.

Finally, we went through and updated all of the logging statements. It was a big change, and impacted all of the code, but it happened quite late in the process and there were no bugs, because it was the least interesting or radical part of the project.

And then we had a new logger. It's been great. With the new tool we've been able to easily add support for more structured approaches to logging and collecting log output in a variety of third party services.

In summary:

  • replacing legacy subsystems can be a good way to improve the functionality of your project.
  • change is hard, but there are ways to make changes easier and less disruptive. They often involve doing even more work.*
  • write code to facilitate transitions, and then delete it later.
  • the larger a change is, the less risky it should be. While there are lots of small-and-low risk changes you can make, the inverse should be true as rarely as possible.
[1]Potentially this should have been behind a feature flag, though I think I didn't actually use a feature flag.
[2]This is to say, application logging facility.
[3]This includes filtering by log level, different formatting options, (semi) structured logging, conditional logging, buffering, and other options.
[4]Hilariously something in the way we were using the logger was tripping up the race detector. While the logger did a decent job of providing the file name and line number of the logging statement, it was pretty focused on printing content to a file/standard output.
[5]The short version here is, "interfaces are great."
comments powered by Disqus