Using logging.config.dictConfig with QueueHandler and Timing Measurements Posting to Slack

Following on from the previous post, I have made a quick timing measurement posting 4 messages to Slack from three locations:

Location Time Taken
(seconds)
San Francisco, California 2.70512
Kuala Lumpur, Malaysia 5.56670
Suzhou, China 12.67123

As you can see, if you’re not using the asynchronous QueueListener then your program could be stuck for over 12 seconds waiting on the communication with Slack.

Here’s an example JSON dictionary config file to use QueueHandler in logging:

{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
        "simple": {
            "format": "%(asctime)s - %(levelname)s - %(message)s"
        }
    },

    "handlers": {
        "consoleHandler": {
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "formatter": "simple",
            "stream": "ext://sys.stdout"
        },        
        "slack-queue": {
            "class":"SlackQueue.sqHandler",
            "level":"DEBUG",
            "formatter": "simple",
            "queue": "ext://Global.slack_queue"
        }
    },

    "root": {
        "level": "DEBUG",
        "handlers": ["consoleHandler", "slack-queue"]
    }
}

Of note here is the ext:// tag that allows you to pass in references to an external object into the logging dictConfig. I have put the instantiation of the queue object in a file named Global.py.

import Queue as queue

#Instantiate a queue for the handler/listener
slack_queue = queue.Queue(-1)

This queue object is required by both the QueueHandler which is instantiated by dictConfig, and also the QueueListener which I have instantiated in my main program, test_program_timing.py.

import logging
import logging.config
import json
import atexit
import SlackQueue
import Global
import time

time_program_start = time.time()
time_program_end = 0.0

def print_timing():
    time_slack_queue_finish = time.time()
    print ("Time taken by main program: %.5f"
                %(time_program_end - time_program_start))
    print ("Extra time taken by sending Slack messages: %.5f"
                %(time_slack_queue_finish - time_program_end))

#Load the logging config json file.
with open('Logger.json','r') as f:
    logging.config.dictConfig(json.load(f))

#Instantiate the queue listener with the same queue above, and also set
#the settings for communicating with Slack.
#You will need to get your own Incoming Webhook URL from Slack's Custom
#Integration setup.
SlackListener = SlackQueue.sqListener(
        Global.slack_queue,
        logging_url="https://hooks.slack.com/services/<your_webhook_url>",
        channel="#your_channel",
        username="Penguin",
        icon_emoji = ":penguin:"
    )

#Start the listener
SlackListener.start()

#Print out the timing measurement right at the end.
atexit.register(print_timing)

#Register the listener stop method to run upon exit in order to allow the 
#queue to be emptied before exiting.
#If you miss this step, the queue might not be emptied before the program
#exits and you won't receive all the messages on Slack.
atexit.register(SlackListener.stop)

#Begin logging
logging.info("Test info message")

#You can temporarily change the Slack settings via the "extra" parameter.
#The settings in extra will only apply to this one log message.
logging.debug("Another test message",extra= {
                                                "channel":"@someone",
                                                "icon_emoji":":coffee:",
                                                "username":"Latte"
                                            })

#You can put the <!channel> tag in the message to send an announcement.
msg="Some sort of warning!"
logging.warning("<!channel>: %s"%msg)

logging.error("Gasp!")

time_program_end = time.time()

I registered the function print_timing( ) with atexit.register( ) before registering the SlackListener.stop function so that the timing print out will happen right at the end after the queue has been cleared. This is because atexit uses a “last in, first out” ordering when it comes to executing registered functions.

You can find all the code on GitHub

Leave a comment below if you have any comments or feedback on how to improve the implementation.

Asynchronous Posting to Slack from Python Logging QueueHandler

We had a problem at work where the regular blocking call to request post was getting in the way at physical locations that didn’t have ideal latency times connecting to the Slack server, like from within China. What takes a second in sunny California was suddenly 8 to 12 seconds from behind the Great Firewall.

The solution is to integrate Slack into Python logging and use the logging QueueHandler to make it asynchronous.

Here’s my implementation of an extension to the QueueHandler and QueueListener classes:

from logging import Handler
import requests
import json
#QueueHandler and QueueListener is part of standard Python logging module
#from Python 3.2 onwards; but if you're using Python 2.x, you can just 
#copy the QueueHandler and QueueListener code and save it into your own 
#queueHandler.py.
try:
    #check if they're part of standard Python logging module
    from logging.handlers import QueueHandler
    from logging.handlers import QueueListener
except ImportError:
    #if not, use your own copy
    from queueHandler import QueueHandler
    from queueHandler import QueueListener


class sqHandler(QueueHandler):
    def __init__(self, queue=None):
        QueueHandler.__init__(self,queue)

    def prepare(self, record):
        """
        Override the method to allow the formatter to work.
        """
        record.msg = self.format(record)
        record.args = None
        record.exc_info = None
        return record

class sqListener(QueueListener,Handler):
    def __init__(self, queue=None, logging_url="", channel="", username="",
                 icon_emoji = ""):
        QueueListener.__init__(self,queue)
        Handler.__init__(self)
        """
        logging_url, channel, username, icon_emoji can all be overridden
        by the extra dictionary parameter of a logging record
        For example: 
            logging.info('Test messate',extra={'channel':'@someone',
                                               'username':'testbot',
                                               'icon_emoji':':penguin:'})
        """
        self.logging_url = logging_url
        self.payload = {
            "channel": channel,
            "username": username,
            "icon_emoji": icon_emoji
            }

    def handle(self, record):
        """
        Override the QueueListener.handle method with the Handler.handle 
        method
        """
        Handler.handle(self, record)

    def emit(self, record):
        #make a copy of the default settings
        new_logging_url = self.logging_url
        new_payload = self.payload.copy()

        #override default settings if necessary
        if hasattr(record,'logging_url'):
            new_logging_url = record.logging_url

        for key in self.payload.keys():
            if hasattr(record,key):
                new_payload[key] = record.__dict__[key]

        #format the message and add to payload
        msg = self.format(record)
        new_payload["text"] = '%s' % msg

        #post the request
        requests.post(new_logging_url, data=json.dumps(new_payload))

And here’s a usage example:

import logging
import Queue as queue
import atexit
import sys
import SlackQueue

    
#Set up the root logger
logging.basicConfig(stream=sys.stdout,
                    level=logging.DEBUG, 
                    format='%(asctime)s - %(levelname)s - %(message)s')
root = logging.getLogger()

#Instantiate a queue for the handler/listener
slack_queue = queue.Queue(-1)

#Instantiate a queue handler using the queue above
SlackHandler = SlackQueue.sqHandler(slack_queue)
SlackHandler.setLevel("DEBUG")

#Set a formatter
simple = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
SlackHandler.setFormatter(simple)

#Add the queue handler to the root logger
root.addHandler(SlackHandler)

#Instantiate the queue listener with the same queue above, and also set
#the settings for communicating with Slack.
#You will need to get your own Incoming Webhook URL from Slack's Custom
#Integration setup.
SlackListener = SlackQueue.sqListener(
        slack_queue,
        logging_url="https://hooks.slack.com/services/<your_webhook_url>",
        channel="#your_channel",
        username="Penguin",
        icon_emoji = ":penguin:"
    )

#Start the listener
SlackListener.start()

#Register the listener stop method to run upon exit in order to allow the 
#queue to be emptied before exiting.
#If you miss this step, the queue might not be emptied before the program
#exits and you won't receive all the messages on Slack.
atexit.register(SlackListener.stop)


#Begin logging
logging.info("Test info message")

#You can temporarily change the Slack settings via the "extra" parameter.
#The settings in extra will only apply to this one log message.
logging.debug("Another test message",extra= {
                                                "channel":"@someone",
                                                "icon_emoji":":coffee:",
                                                "username":"Latte"
                                            })

#You can put the <!channel> tag in the message to send an announcement.
msg="Some sort of warning!"
logging.warning("<!channel>: %s"%msg)

logging.error("Gasp!")

You can use the extra parameter in logging to alter the Slack settings for a particular message. It can be useful if you decide that a particular logging message should ping a certain person directly, or post to a different channel from the default channel.

You can find all the code on GitHub.

If you’re using Python 2.x and need the source of queueHandler.py, check out Vinay Sajip’s blog post, specifically his whole script example.

Leave a comment below if you have any comments or feedback on how to improve the implementation.

References:
Logging for Slackers – Alan Gardner
Improved QueueHandler, QueueListener: dealing with handlers that block – Vinay Sajip