Solving Logs Woes: A Small Dive into Singleton Design Pattern

Kfir - Apr 28 - - Dev Community

kid thinking

Originaly published on Python in Plain English Medium Publication


Recently, I’ve been working on a new feature for a Python software project. To make sure it’s solid, I thought it’d be a good idea to add logs using the “logging” Python package. It’s been really helpful because I can see what’s happening at every step of my program right in the console. It’s made tracking down bugs a lot easier.

Initially, I thought incorporating logging would be straightforward — simply import the library and start logs. However, I hit a roadblock when I couldn’t get it to write logs from files. Despite trying various solutions like checking the logging configuration file and creating new instances or modules, none of them worked. Then, a colleague suggested a solution I had overlooked: the Singleton Design Pattern. This approach turned out to be the key to solving my problem. Now, I’ll explain what it is and how to implement it, so you’ll feel confident using it in the future if similar issues arise.

What Is Singleton Design Approach

The Singleton design pattern is a simple yet powerful concept often used in software development. Imagine you have a class (let’s call it Logger) and you want only one instance of this class to exist throughout your program’s execution.

In simpler terms, think of the Singleton as a guardian that ensures there’s only one instance of a particular class. It’s like having a single key to a treasure chest — no matter how many times you ask for the key, you’ll always get the same one.

Here’s a breakdown:

  • One and Only One: The Singleton ensures that there’s only one instance of a class. It’s like having a single instance of your favorite game running — you can’t open two instances of the same game simultaneously.

  • Global Access: Once the Singleton is created, it provides a global point of access to that instance. Just like a lighthouse guiding ships in the night, the Singleton guides your program to the same instance every time you need it.

  • Common Use Cases: Singletons are handy for resources that should be shared across the entire application, such as a database connection, a logger, or a configuration manager.

  • Implementation: Typically, Singletons are implemented using a static method (like getInstance()) that either creates a new instance if none exists or returns the existing instance if it does.

Here’s a simple example in Python:

class Singleton:
 _instance = None

 @staticmethod
 def getInstance():
     if Singleton._instance is None:
         Singleton._instance = Singleton()
     return Singleton._instance

# Usage:
singleton_instance1 = Singleton.getInstance()
singleton_instance2 = Singleton.getInstance()

print(singleton_instance1 == singleton_instance2)  # Output: True
Enter fullscreen mode Exit fullscreen mode
  • Benefits: Singletons help conserve resources by ensuring that only one instance of a class exists. They also simplify global state management by providing a centralized point of access.

  • Caveats: While Singletons are useful, they can sometimes be overused, leading to tight coupling and global state-related issues. It’s essential to use them judiciously and consider other alternatives when appropriate.

How Singleton Design Resolved My Issue

Now, let’s delve into how I utilized the Singleton pattern to address my issue in the code:

class Logger:
 _instance = None

 def __new__(cls):
     if cls._instance is None:
         cls._instance = super().__new__(cls)
         cls._instance.setup_logger()
     return cls._instance

 def setup_logger(self):
     # logging configuration here
Enter fullscreen mode Exit fullscreen mode
  1. _instance Attribute: This attribute keeps track of the single instance of the Logger class.

  2. __new__ Method: Python’s special __new__ method is harnessed here to manage object instantiation. If _instance is None, indicating no instance exists, a new one is created via super().__new__(cls) and setup_logger() is invoked to configure the logger. Subsequent calls to __new__ will return the same instance stored in _instance.

  3. setup_logger Method: Responsible for configuring the logger. It’s executed only once during the creation of the singleton instance.

How It Functions:

  • Whenever you create an instance of Logger (logger_root = Logger()), __new__ kicks in.

  • If _instance is None, implying no instance exists, a fresh one is created and stored in _instance.

  • Subsequent calls to Logger() return the same instance stored in _instance, ensuring the singular existence of the Logger class across your program.

Pros and Considerations:

  • Universal Access: Access the logger instance globally via logger_root.

  • Thread Safety: This implementation doesn’t address thread safety. For multithreaded applications, consider synchronizing access to the singleton instance.

Double Checking

I won’t delve too deeply into the double-checking pattern that I integrated into my __new__ function. Essentially, I added double-checking within the __new__ method by first checking if the _instance is None, then acquiring a lock to ensure thread safety, and finally rechecking if _instance is still None before creating a new instance. If you’re interested in learning more about it, you can check out this link.

Conclusion

By integrating the Singleton pattern, you ensure a streamlined, centralized logging mechanism within your project.


References

https://medium.com/@emredalc/singleton-design-pattern-ce7e2f153aa0

https://medium.com/geekculture/introduction-to-design-patterns-understanding-singleton-design-pattern-5a4d49960444

https://refactoring.guru/design-patterns/singleton

https://www.geeksforgeeks.org/singleton-pattern-in-python-a-complete-guide

https://docs.python.org/3/library/logging.html


Photos credit: Saydung89 & Hitesh Choudhary

. . . . . . . . . . . .