Contents
Introduction
Logging Module
Configurations
Basic Configuration
File Configuration
Logging from Multiple Modules
Logging from Multiple Threads
Context Logging and Filter
Best Practices
Conclusion
References
Introduction
Logging is an important part of software development, which can help endless hours of debugging. If used it right we can use it for different flow analysis, how and what to scale, health checks, issue analysis, performance checks, security scan, audit and so on.
Calling log calls from the program, indicates certain events has occurred during the timestamp, from where, may be IP address and other additional information. Event will hold the descriptive message, eg: dynamic or static values, importance of message (debug, info, warn, error).
Python provides a logging system as a part of its standard library, so you can quickly add logging to your application.
Logging Module
The logging module readily available, adding logs to your module is as easy as below.
Code:
importlogginglogging.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')logging.critical('This is a critical message')
Output:
WARNING:root:This is a warning messageERROR:root:This is an error messageCRITICAL:root:This is a critical message
As seen above by default, there are 5 levels of logging, which indicate the severity of events. Each levels have corresponding severity associated. Below is the levels defined based on the severity in increasing order:
Level | Used For |
DEBUG | Detailed messages, usually for diagnosing problems |
INFO | Confirmation or Facts of working as expected |
WARNING | Alarming for future breaks, but until now the application is working |
ERROR | Due to serious problem in the function |
CRITICAL | Due to serious problem, the program failed to continue to execute |
Table 1: Usage of each Log Levels
From the above you have noticed the debug and info messages are not logged. Because the default severity is set to WARNING. This will have messages printed of the level WARNING and above.
We will see below how to change the severity levels by changing the configurations. Also later how to change in format of the output.
Configurations
There are can be several configurations done. Such as,
Parameter | Description |
level | Root logger will be set to the specified severity level |
filename | Name of the file logs should be stored |
filemode | Mode in which the log file should be opened, default is “a” append mode |
format | Pattern the log message to be written |
datefmt | Format in which date should be written |
Table 2: Commonly used log parameters
Basic Configuration
We can use the below method to configure the logging:
Code:
import logging logging.basicConfig(level=logging.DEBUG, filename='/project/log/app.log', filemode='w', format='%(asctime)s-%(message)s', datefmt='%d-%b-%y %H:%M:%S')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')logging.critical('This is a critical message')
Output: (from app.log)
27-Nov-21 07:28:09 - This is a debug message27-Nov-21 07:28:09 - This is an info message27-Nov-21 07:28:09 - This is a warning message27-Nov-21 07:28:09 - This is an error message27-Nov-21 07:28:09 - This is a critical message
Here we could see below points:
Debug and info message is logged, as the logging level is DEBUG and above
Log contents are not displayed in stdout, rather saved in the file called app.log
Message format is different from before, it works as per the format given by us in the format parameter
Same applies to the date format
File Configuration
Before we get to know about file configuration, lets look how logging works:
Logging Flow:
So far, we have seen the default logger named root, which is used by the logging module whenever its functions are called directly like this: logging.debug(). But we can or should define your own logger.
The logging module, follows the modular approach and has split the functionalities in below components:
Logger:
This is exposed to application code to directly use.
Logging is performed by calling methods on instances of the
Loggerclass.Logger class performs three duties
Expose several methods to application code so that applications can log messages at runtime. A good convention is using the module name to name the loggers, as intuitively obvious where events are logged just from the logger name.
logger = logging.getLogger(__name__)
Logger objects determine which log messages to act upon based upon severity or filter objects.
Logger.setLevel()Logger.addFilter()andLogger.removeFilter()
Logger objects pass along relevant log messages to all interested log handlers.
Logger.addFilter()andLogger.removeFilter()
LogRecord:
Loggers automatically create
LogRecordobjects that have all the information related to the event being logged, like the name of the logger, the function, the line number, the message, and more.
Filter:
Provide a finer grained facility for determining which log records to output. Later we will see an example using Filters
Handler:
Sends the LogRecords (created by loggers) to the appropriate destination.
For example it could be file, stdout, http, etc.
For each destination Handler is have sub class: such as file: FileHandler, stdout: StreamHandler, http: HTTPHandler.
Loggerobjects can add zero or more handler objects to themselves with anaddHandler()method.
Formatter:Specify the format of LogRecord, by mentioning in the list attributes needs to be logged to be written in the final output.
Fig1: Logging Flow
Credits: https://docs.python.org/3/howto/logging.html
We can configure logging in three ways:
Creating loggers, handlers, and formatters explicitly using Python code that calls the configuration methods.
In the below example:
Created a logger with the same module name.
Create a console handler with the debug level set
Create formatter with time, module name, level and message
Code:
import logging # create logger logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # add formatter to ch ch.setFormatter(formatter) # add ch to logger logger.addHandler(ch) # 'application' code logger.debug('debug message') logger.info('info message') logger.warning('warn message') logger.error('error message') logger.critical('critical message')
Output:
$ python handlerconfig.py2021-11-28 14:45:45,096 - __main__ - DEBUG - debug message2021-11-28 14:45:45,097 - __main__ - INFO - info message2021-11-28 14:45:45,097 - __main__ - WARNING - warn message2021-11-28 14:45:45,097 - __main__ - ERROR - error message2021-11-28 14:45:45,097 - __main__ - CRITICAL - critical message
Creating a logging config file and reading it using the
fileConfig()function.In the below example:
Created a logger with the 'simpleExample' logger name.
Create a console handler with the debug level set, and file handler with info level set hence the debug log will not be printed
Both the configurations are taken from the logging.conf file
Its important to have the handler and logger set for 'root'
In file handler:
The
propagateentry is set to 1 to indicate that messages must propagate to handlers higher up the logger hierarchy from this logger, or 0 to indicate that messages are not propagated to handlers up the hierarchy.The
qualnameentry is the hierarchical channel name of the logger, that is to say the name used by the application to get the logger.
FileRotateHandler
Here we have used file rotate handler, to avoid logging all the content in a single huge file
We have configured maxBytes=10, about 10bytes written the will be rotated
backupcount=5, it says how many rotated files needs to be kept rest will be cleaned.
Create formatter with time, module name, level and message
Code:
fileconfig.py
import logging import logging.config logging.config.fileConfig('logging.conf') # create logger logger1 = logging.getLogger('simpleExample') logger = logging.getLogger() # 'application' code logger.debug('debug message') logger1.info('info message') logger1.warning('warn message') logger1.error('error message') logger1.critical('critical message')
loggin.conf
[loggers] keys=root,simpleExample [handlers] keys=consoleHandler,simpleFileHandler [formatters] keys=simpleFormatter [logger_root] level=DEBUG handlers=consoleHandler [logger_simpleExample] level=INFO handlers=simpleFileHandler qualname=simpleExample propagate=0 [handler_consoleHandler] class=StreamHandler formatter=simpleFormatter args=(sys.stdout,) [handler_simpleFileHandler] class=handlers.RotatingFileHandler formatter=simpleFormatter maxBytes=10 args=('test.log','a',10,5) [formatter_simpleFormatter] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
Output:
$ python fileconfig.py 2021-11-28 17:47:26,858 - root - DEBUG - debug message $ tail test.log*
==> test.log <==
2021-11-28 18:48:38,520 - simpleExample - CRITICAL - critical message
==> test.log.1 <==
2021-11-28 18:48:38,519 - simpleExample - ERROR - error message
==> test.log.2 <==
2021-11-28 18:48:38,518 - simpleExample - WARNING - warn message
==> test.log.3 <==
2021-11-28 18:48:38,517 - simpleExample - INFO - info message
Creating a dictionary of configuration information and passing it to the
dictConfig()function.This is the simple example by using yaml format as configuration
The dictConfig expects in input to be in the dict object, so we can have dict defined and call the logger method.
Below example creates a logger with the same module name.
Create a console handler with the debug level set
Create formatter with time, module name, level and message
Code:
dict_filelog.py
import logging
import logging.config
import yaml
with open('dict_config.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.debug('This is a debug message')
logger.info('This is a info message')
logger.error('This is a error message')
dict_config.yaml
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: simple
stream: ext://sys.stdout
loggers:
sampleLogger:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
Output:
2021-11-28 19:25:00,508 - __main__ - DEBUG - This is a debug message
2021-11-28 19:25:00,508 - __main__ - INFO - This is a info message
2021-11-28 19:25:00,509 - __main__ - ERROR - This is a error message
Note:
When no configuration is provided, the events are output using a handler of last resport, which is stored in logging.lastResort.
This acts as StreamHandler, which writes the messages to sys.stderr.
Handler's level is set to WARNING
The behaviour of the logging package in these circumstances is dependent on the Python version. We are seeing here for python 3.2 and above.
Logging from Multiple Modules
Calling multiple times same getLogger(“NAME”) will return a reference to the same object.
Can be used across modules to get the same object but it requires to be same Python interpreter process.
Also the application code can define and configure a parent logger in one module and create (but not configure) a child logger in a separate module, and all logger calls to the child will pass up to the parent.
The parent and child mapping is done by the name of the logger, please find the example below in the getLogger method
parent: getLogger('test_app')
child: getLogger('test_app.sub_app') # here all the configuration is inherited from the parent
Code:
File: main_app.py
import sys
import logging
import sub_app
# Create a custom logger with "test_app"
logger = logging.getLogger("test_app")
logger.setLevel(logging.DEBUG)
# Create handlers
# create console handler with debug log level
c_handler = logging.StreamHandler(sys.stdout)
# create file handler with error log level
f_handler = logging.FileHandler('file.log', 'w')
f_handler.setLevel(logging.WARNING)
# Create formatters and add it to handlers
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
print(c_handler)
print(logger)
logger.debug('This is a debug from main module')
logger.info('This is a info from main module')
logger.warning('This is a warning from main module')
logger.error('This is an error from main module')
logger.info("Before calling the sub_app_function")
sub_app.sub_app_function()
logger.info("After calling the sub_app_function")
File: sub_app.py
import logging
# create logger
module_logger = logging.getLogger('test_app.sub_app')
def sub_app_function():
module_logger.error("Debug message from sub_app")
Output:
% python multiple_modules_logger.py
<StreamHandler <stdout> (NOTSET)>
<Logger test_app (DEBUG)>
test_app - DEBUG - This is a debug from main module
test_app - INFO - This is a info from main module
test_app - WARNING - This is a warning from main module
test_app - ERROR - This is an error from main module
test_app - INFO - Before calling the sub_app_function
test_app.sub_app - ERROR - Debug message from sub_app
test_app - INFO - After calling the sub_app_function
% cat file.log
2021-11-28 23:26:04,332 - test_app - WARNING - This is a warning from main module
2021-11-28 23:26:04,332 - test_app - ERROR - This is an error from main module
2021-11-28 23:26:04,332 - test_app.sub_app - ERROR - Debug message from sub_app
Logging from Multiple Threads
Logging from multiple threads is as simple as normal process
To differentiate the logs generated from each thread, threadName can be used
Below example shows the logging from the main thread and another one more thread
Code:
import logging
import threading
import time
def worker(arg):
while not arg['stop']:
logging.debug('Hi from thread 2')
time.sleep(0.5)
def main():
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s')
info = {'stop': False}
thread = threading.Thread(target=worker, args=(info,))
thread.start()
while True:
try:
logging.debug('Hello from main thread')
time.sleep(1.0)
except KeyboardInterrupt:
info['stop'] = True
break
thread.join()
if __name__ == '__main__':
main()
Output:
% python threadlogger.py
2021-11-28 22:38:54,207 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:54,207 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:54,712 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:55,212 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:55,214 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:55,716 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:56,217 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:56,220 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:56,725 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:57,221 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:57,229 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:57,734 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:58,223 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:58,236 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:58,738 - root - DEBUG - Thread-1 - Hi from thread 2
2021-11-28 22:38:59,226 - root - DEBUG - MainThread - Hello from main thread
2021-11-28 22:38:59,239 - root - DEBUG - Thread-1 - Hi from thread
Context Logging and Filter
LoggerAdapter
Sometimes you want logging output to contain contextual information in addition to the parameters passed to the logging call.
For example, in a networked application, it may be desirable to log client-specific information in the log (e.g. remote client’s username, or IP address).
An easy way in which you can pass contextual information to be output along with logging event information is to use the
LoggerAdapterclass.In the below example we are adding the host_ip, to all the log events.
Filter
Filter can be used for both at the logger level or by handler level.
Such as they can stop certain information to be passed to be logger
Also inject additional information into the LogRecord which will be logged
In our below example, we are using filter to allow only certain levels to be logged.
Though the log level is set to DEBUG, we are only allowing INFO and ERROR to be logged.
Note: These are cooked up examples, in real life the usecases varies.
Code:
import sys
import logging
host_ip = "localhost"
class StdoutFilter(logging.Filter):
def filter(self, record):
return record.levelno in (logging.INFO, logging.ERROR)
logger = logging.getLogger('sampleApp')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.addFilter(StdoutFilter())
formatter = logging.Formatter('%(host_ip)s - %(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger = logging.LoggerAdapter(logger, {'host_ip': host_ip})
print(logger)
logger.debug('This is a debug from main module')
logger.info('This is a info from main module')
logger.warning('This is a warning from main module')
logger.error('This is an error from main module')
Output:
<LoggerAdapter sampleApp (DEBUG)>
localhost - 2021-11-29 00:26:18,566 - sampleApp - INFO - This is a info from main module
localhost - 2021-11-29 00:26:18,567 - sampleApp - ERROR - This is an error from main module
Best Practices
Create loggers using getlogger function
Using appropriate log levels in different regions
Create module level loggers
Use rotating file handler
Include timestamp in logs, preferred to use standard format exists, and it’s called ISO-8601.
Eg:
import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.info('Example of logging with ISO-8601 timestamp')
Avoid opening same log file multiple times
Do not pass logger as parameter in class or methods
Conclusion
As seen the logging module is very flexible, customisable and ease to use. There are much more features such as,
context based logging
customisable log records
logging to multiple destination and servers
multiple processes logging to same file log
speaking logging messages
And many more, readymade code is available in the python cookbook URL: https://docs.python.org/3/howto/logging-cookbook.html.
It would be great to start using if you haven’t been using logging in your applications. You will see the difference and increase the code quality by adding this. Happy Logging!
References
https://realpython.com/python-logging/
https://docs.python.org/3/howto/logging.html
https://docs.python.org/3/howto/logging-cookbook.html
https://www.thepythoncode.com/media/articles/logging-in-python.PNG [credits for the header image]
Original Blog Posted in OSFY
https://www.opensourceforu.com/2022/04/an-introduction-to-low-code-and-no-code-test-automation-tools/
For further research and updates maintaining the blog here.


No comments:
Post a Comment