User Guide¶
umsg is a convenience wrapper around Python’s logging
library,
primarily intended to help facilitate easier library logging, while it works
for general logging as well.
Basic Usage¶
There are two methods by which umsg may be utilized. The first is directly via module member functions, or as a mixin to add logging class member methods.
Module Level¶
In its simplest form, umsg can be invoked directly without pre-initialization in the same manner that the standard logging facilities can.
import umsg
umsg.log('Logging has been initialized')
Class Mixin¶
For a library class, however, you may instead use the LoggingMixin
mixin to add logging directly as a class method.
from umsg.mixins import LoggingMixin
class MyClass(LoggingMixin):
def __init__(self):
super().__init__()
self.log('Logging has been initialized')
Logging Prefixes¶
One of the core features umsg provides is message prefixes. While something similar can be accomplished in vanilla logging via specially crafted Format strings, an important distinction between umsg’s implementation, and vanilla logging is umsg prefixes are always optional. When using the format attributes from vanilla logging, you must always provide the values or the message will not be logged at all:
If you choose to use these attributes in logged messages, you need to exercise some care. In the above example, for instance, the Formatter has been set up with a format string which expects ‘clientip’ and ‘user’ in the attribute dictionary of the LogRecord. If these are missing, the message will not be logged because a string formatting exception will occur. So in this case, you always need to pass the extra dictionary with these keys.
While this might be annoying, this feature is intended for use in specialized circumstances, such as multi-threaded servers where the same code executes in many contexts, and interesting conditions which arise are dependent on this context (such as remote client IP address and authenticated user name, in the above example). In such circumstances, it is likely that specialized Formatters would be used with particular Handlers.
—https://docs.python.org/3/library/logging.html, emphasis added
Prefixes permit blocks of program execution to be called out within logs when necessary, and programmatically, without requiring the prefix in all instances. This is independent of the logging message format string, and the two may be used simultaneously. This convenience feature isn’t meant to replace the message formatting that vanilla logging provides, rather it compliments it for a specific, though common, use-case.
Configured Prefixes¶
The logging prefix can be added to the module profile at any point using the
umsg.set_attr
function. For instance, the given example using the
default prefix format:
umsg.set_attr('msg_prefix', 'main')
umsg.log('How now brown cow')
would result in the following message being created, and sent to the registered logging handlers:
"[main] How now brown cow"
This occurs before the logging formatter is applied, thus, we don’t have issues with needing to always supply a format parameter. In addition, we can change the prefix format as required, either on-the-fly, or more likely, at the start of our program.
umsg.set_attr('msg_prefix_format', '<{prefix}> ')
umsg.set_attr('msg_prefix', 'main')
umsg.log('How now brown cow')
Changing the previous example as above now gives us:
"<main> How now brown cow"
Inline Prefixes¶
Prefixes are evaluated per message, prior to sending the message on to the logging formatter. Thus, every log message may have a custom prefix, however unlikely this scenario may be. Inline prefixes override the configured prefix at the time and only for the duration of the specific call.
umsg.set_attr('msg_prefix', 'main')
umsg.log('How now brown cow', prefix='alt')
umsg.log('How now brown cow')
The above scenario results in two distinct log messages:
"[alt] How now brown cow"
"[main] How now brown cow"
This enables specific functions, classes, decorators, or other code segments to identify themselves as necessary, without worrying about logging state, or even logging formatting parameters. More importantly, this enables you to selectively expose only the information you wish.
Take the following example, which exposes the function name of every logging caller:
import logging
logging.basicConfig(format='%(levelname)s - %(funcName)s - %(message)s', level=logging.DEBUG)
class Rocket:
def __security_check(self):
logging.debug('checking security')
def engine_start(self):
logging.info('starting engines')
In this example, all logging messages will have their calling function/method’s name exposed in the logs. This might be helpful for debugging, chances are you don’t want or need such information in your day-to-day application logs. You may still want some contextual information though.
import umsg
umsg.init(msg_format='%(levelname)s - %(message)s',
msg_prefix_format='{prefix} - ',
level=logging.INFO
)
class Rocket(LoggingMixin):
def __security_check(self):
self.log('checking security', level='debug', prefix=__security_check.__name__)
def engine_start(self):
self.log('starting engines')
The format of the log message in this case is identical to the previous example, with the exception that we only print the method name for the debug message. A better approach would be to create a utility to pull the function name from the stack, but the idea remains the same. The use of prefixes doesn’t obviate or seek to replace useful logging formatters, rather, it provides a complimentary function.
The basicConfig Conundrum¶
Pythhon provides a zero config logging option in the standard logging
library by directly invoking the logging methods. The defaults for using this
option are not particularly useful for libraries or module logging purposes,
being that StreamHandler
is the chosen default handler, and
the handler logs directly to the root logger.
The approach taken by umsg is slightly different. It too supports a zero config
option inspired by logging
, and will initialize a logging handler with
a default configuration by simply calling the logging method. Where umsg
diverges is in what it instantiates, and where. Instead of initializing a StreamHandler
,
umsg defaults to the more library appropriate NullHandler
,
and initializes it on the module itself, not the root logger. Doing so is
important for several reasons. This is in point of fact a recommended behavior
for library logging, which honors the idea that “the configuration of handlers
is the prerogative of the application developer who uses your library” 1.
Using the NullHandler
ensures we’re not emitting logs the developer
may not want, need, or even be aware of. It also honors the Zen of Python
aphorism that explicit is better than implicit, by requiring the developer to
be explicit about their logging.
By using the module level logger, we further isolate the library, or module,
logging from unintentionally mucking up the consuming application’s logs.
Logging should be intentional by design. Blindly logging to the root logger
negates this. Lastly, umsg defaults to the logging.INFO
level. This
is for two reasons. First, libraries don’t usually emit a lot of general messages,
and by isolating to the module, the developer is already required to deliberately
enabling library logs, so they should override with their own desired level.
Second, umsg does support generic application logging, which more often
requires general information messages by default. If the application so requires,
logging.DEBUG
can be easily enabled, though we shouldn’t assume this
is required by default for all applications.
Enabling the StreamHandler
, if desired, is trivial:
import logging
import umsg
umsg.init()
umsg.add_handler(logging.StreamHandler())
umsg.log('Hello World')
Unlike the basicConfig()
defaults, which are set at the logging.WARNING
level, this log message will immediately be displayed at the umsg default level
of logging.INFO
.