Why Logging Is Better Than Print?
Compared to print, logging provides more control over what information is captured in the logs and how it is formatted.
With logging, you can log different levels (debug, info, warning, error) and selectively enable/disable log levels for different situations, allowing you to have more granular control over the log output.
For example, in the following code:
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
you can set the log level to “debug” to include all log levels:
2023-07-16 16:18:51.769 | DEBUG | __main__:main:9 - This is a debug message
2023-07-16 16:18:51.769 | INFO | __main__:main:10 - This is an info message
2023-07-16 16:18:51.769 | WARNING | __main__:main:11 - This is a warning message
2023-07-16 16:18:51.769 | ERROR | __main__:main:12 - This is an error message
or set it to “info” to include only info and higher levels:
2023-07-16 16:19:42.805 | INFO | __main__:main:10 - This is an info message
2023-07-16 16:19:42.806 | WARNING | __main__:main:11 - This is a warning message
2023-07-16 16:19:42.806 | ERROR | __main__:main:12 - This is an error message
You can also direct log output to a file and include timestamps, which help track the sequence of events in the logs.
# example.log
2023-07-16 09:50:24 | INFO | logging_example:main:17 - This is an info message
2023-07-16 09:50:24 | WARNING | logging_example:main:18 - This is a warning message
2023-07-16 09:50:24 | ERROR | logging_example:main:19 - This is an error message
2023-07-16 09:55:37 | INFO | logging_example:main:17 - This is an info message
2023-07-16 09:55:37 | WARNING | logging_example:main:18 - This is a warning message
2023-07-16 09:55:37 | ERROR | logging_example:main:19 - This is an error message
Why Many People Are Not Using Logging?
Many developers still prefer using print statements over logging because print is simpler and doesn’t require as much setup. For small scripts and one-off tasks, the overhead of setting up a logging framework seems unnecessary.
import logging
# Require initial set up
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s"
datefmt="%Y-%m-%d %H:%M:%S",
)
def main():
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
if __name__ == "__main__":
main()
Wouldn’t it be nice if there is a library that allows you to leverage the power of logging while making the experience as simple as print?
That is when Loguru comes in handy. This article will show some Loguru features that make it a great alternative to the standard logging library.
Feel free to play and fork the source code of this article here:
View on GitHubElegant Out-of-the-Box Functionality
By default, logging gives boring and no very useful logs:
import logging
logger = logging.getLogger(__name__)
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
Output:
WARNING:root:This is a warning message
ERROR:root:This is an error message
In contrast, Loguru generates informative and vibrant logs by default.
from loguru import logger
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
Customize Logs
Format Logs
Formatting logs allows you to add useful information to logs such as timestamps, log levels, module names, function names, and line numbers.
The traditional logging approach uses the % formatting, which is not intuitive to use and maintain:
import logging
# Create a logger and set the logging level
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
Output:
2023-07-16 14:48:17 | INFO | logging_example:main:13 - This is an info message
2023-07-16 14:48:17 | WARNING | logging_example:main:14 - This is a warning message
2023-07-16 14:48:17 | ERROR | logging_example:main:15 - This is an error message
In contrast, Loguru uses the {}
formatting, which is much more readable and easy to use:
from loguru import logger
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}",
)
Save Logs to Files
Saving logs to files and printing them to the terminal using the traditional logging module requires two extra classes FileHandler
and StreamHandler
.
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.FileHandler(filename="info.log", level=logging.INFO),
logging.StreamHandler(level=logging.DEBUG),
],
)
logger = logging.getLogger(__name__)
def main():
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
if __name__ == "__main__":
main()
However, with Loguru, you can attain the same functionality with just the add
method.
from loguru import logger
logger.add(
'info.log',
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}",
level="INFO",
)
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
Rotate Logs
Rotating logs prevents the size of log files from getting too large by periodically creating new log files and archiving or removing older ones.
In the logging library, rotating logs requires an additional class called TimedRotatingFileHandler
. The following code switches to a new log file every week (when="WO", interval=1
) and retains up to 4 weeks’ worth of log files (backupCount=4
).
import logging
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create a formatter with the desired log format
formatter = logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler = TimedRotatingFileHandler(
filename="debug2.log", when="WO", interval=1, backupCount=4
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
In Loguru, you can replicate this behavior by adding the rotation
and retention
arguments to the add
method. The syntax for specifying these arguments is readable and easy to use.
from loguru import logger
logger.add("debug.log", level="INFO", rotation="1 week", retention="4 weeks")
def main():
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
if __name__ == "__main__":
main()
Filter
Filtering in logging allows you to selectively control which log records should be output based on specific criteria.
In the logging library, filtering logs requires creating a custom logging filter class.
import logging
logging.basicConfig(
filename="hello.log",
format="%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
level=logging.INFO,
)
class CustomFilter(logging.Filter):
def filter(self, record):
return "Hello" in record.msg
# Create a custom logging filter
custom_filter = CustomFilter()
# Get the root logger and add the custom filter to it
logger = logging.getLogger()
logger.addFilter(custom_filter)
def main():
logger.info("Hello World")
logger.info("Bye World")
if __name__ == "__main__":
main()
In Loguru, you can simply use a lambda function to filter logs.
from loguru import logger
logger.add("hello.log", filter=lambda x: "Hello" in x["message"], level="INFO")
def main():
logger.info("Hello World")
logger.info("Bye World")
if __name__ == "__main__":
main()
Catch Exceptions
Conventional logs for exceptions can be ambiguous and challenging to debug:
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def division(a, b):
return a / b
def nested(c):
try:
division(1, c)
except ZeroDivisionError:
logging.exception("ZeroDivisionError")
if __name__ == "__main__":
nested(0)
Traceback (most recent call last):
File "/Users/khuyentran/Data-science/productive_tools/logging_tools/catch_exceptions/logging_example.py", line 16, in nested
division(1, c)
File "/Users/khuyentran/Data-science/productive_tools/logging_tools/catch_exceptions/logging_example.py", line 11, in division
return a / b
ZeroDivisionError: division by zero
The exceptions displayed above are not very helpful as they don’t provide information about the values of c
that triggered the exceptions.
Loguru enhances error identification by displaying the entire stack trace, including the values of variables:
from loguru import logger
def division(a, b):
return a / b
def nested(c):
try:
division(1, c)
except ZeroDivisionError:
logger.exception("ZeroDivisionError")
if __name__ == "__main__":
nested(0)
Loguru’s catch
decorator allows you to catch any errors within a function. This decorator also identifies the thread on which the error occurs.
from loguru import logger
def division(a, b):
return a / b
@logger.catch
def nested(c):
division(1, c)
if __name__ == "__main__":
nested(0)
But I don’t want to add more dependencies to my Python project
Although incorporating Loguru into your project requires installing an additional library, it is remarkably lightweight and occupies minimal disk space. Moreover, it helps in reducing boilerplate code, making your project easier to maintain and significantly reducing the friction associated with using logging.
I love writing about data science concepts and playing with different data science tools. You can stay up-to-date with my latest posts by:
- Subscribing to my newsletter on Data Science Simplified.
- Connect with me on LinkedIn and Twitter.
2 thoughts on “Loguru: Simple as Print, Flexible as Logging”
This is fantastic and very relevant for my current project
I’m glad to hear that!
Comments are closed.