David Folkner
David Folkner

Reputation: 1199

Logging Formatting Right Justify Level Name

I am trying to format the output of the logging to have the levelname on the right side of the terminal always. I currently have a script that looks like:

import logging, os, time

fn = 'FN'
start = time.time()

def getTerminalSize():
    import os
    env = os.environ
    def ioctl_GWINSZ(fd):
        try:
            import fcntl, termios, struct, os
            cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
        '1234'))
        except:
            return
        return cr
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        cr = (env.get('LINES', 25), env.get('COLUMNS', 80))
    return int(cr[1]), int(cr[0])


(width, _) = getTerminalSize()
level_width = 8
message_width = width - level_width - 4
FORMAT = '%(message)-{len1:{width1}d}s [%(levelname){len2:{width2}d}s]'.format(
    len1 = message_width,_
    len2 = level_width,_
    width1 = len(str(message_width)),_
    width2 = len(str(level_width)))

logging.basicConfig(format=FORMAT, level="DEBUG")

logging.debug("Debug Message")
logging.info("Info Message")
logging.warning("Warning Message")
logging.error("Error Message")
logging.critical("Critical Message")
logging.info("Starting File: " + os.path.basename(fn) + "\n-----------------------------------------")
logging.info("\tTo read data: %s"%(time.time() - start))

The output looks like:

Debug Message                                                        [   DEBUG]
Info Message                                                         [    INFO]
Warning Message                                                      [ WARNING]
Error Message                                                        [   ERROR]
Critical Message                                                     [CRITICAL]
Starting File: Channel209.Raw32
----------------------------------------- [    INFO]
        To read data: 0.281999826431                                        [
 INFO]

I would like the output to look something like this instead and can't quite figure it out:

Debug Message                                                        [   DEBUG]
Info Message                                                         [    INFO]
Warning Message                                                      [ WARNING]
Error Message                                                        [   ERROR]
Critical Message                                                     [CRITICAL]
Starting File: Channel209.Raw32
-----------------------------------------                            [    INFO]
        To read data: 0.281999826431                                 [    INFO]

Upvotes: 3

Views: 2069

Answers (2)

David Folkner
David Folkner

Reputation: 1199

As @Carpetsmoker said, to do what I truly desired required creating a new formatter class overwriting the default.

The following class worked well for this process:

import logging
import textwrap
import itertools
'''
MyFormatter class
Adapted from: https://stackoverflow.com/questions/6847862/how-to-change-the-format-of-logged-messages-temporarily-in-python
              https://stackoverflow.com/questions/3096402/python-string-formatter-for-paragraphs
Authors: Vinay Sajip, unutbu
'''
class MyFormatter(logging.Formatter):

    #This function overwrites logging.Formatter.format
    #We conver the msg into the overall format we want to see
    def format(self,record):


        widths=[getTerminalSize()[0] - 12 ,10]
        form='{row[0]:<{width[0]}} {row[1]:<{width[1]}}'

        #Instead of formatting...rewrite message as desired here
        record.msg = self.Create_Columns(form,widths,[record.msg],["[%8s]"%record.levelname])

        #Return basic formatter
        return super(MyFormatter,self).format(record)

    def Create_Columns(self,format_str,widths,*columns):
        '''
        format_str describes the format of the report.
        {row[i]} is replaced by data from the ith element of columns.

        widths is expected to be a list of integers.
        {width[i]} is replaced by the ith element of the list widths.

        All the power of Python's string format spec is available for you to use
        in format_str. You can use it to define fill characters, alignment, width, type, etc.

        formatter takes an arbitrary number of arguments.
        Every argument after format_str and widths should be a list of strings.
        Each list contains the data for one column of the report.

        formatter returns the report as one big string.
        '''
        result=[]
        for row in zip(*columns):

            #Create a indents for each row...
            sub = []

            #Loop through
            for r in row:
                #Expand tabs to spaces to make our lives easier
                r = r.expandtabs()

                #Find the leading spaces and create indend character
                if r.find(" ") == 0:
                    i = 0
                    for letters in r:
                        if not letters == " ":
                            break
                        i += 1
                    sub.append(" "*i)
                else:
                    sub.append("")

            #Actually wrap and creat the string to return...stolen from internet
            lines=[textwrap.wrap(elt, width=num, subsequent_indent=ind) for elt,num,ind in zip(row,widths,sub)]
            for line in itertools.izip_longest(*lines,fillvalue=''):
                result.append(format_str.format(width=widths,row=line))
        return '\n'.join(result)

It relies on getting the terminal size in some function called getTerminalSize. I used Harco Kuppens' Method that I will not repost here.

An example driver program is as follows, where MyFormatter and getTerminalSize are located in Colorer:

import logging
import Colorer

logger = logging.getLogger()
logger_handler = logging.StreamHandler()
logger.addHandler(logger_handler)
logger_handler.setFormatter(Colorer.MyFormatter("%(message)s"))
logger.setLevel("DEBUG")

logging.debug("\t\tTHIS IS A REALY long DEBUG Message that works and wraps around great........")
logging.info("   THIS IS A REALY long INFO Message that works and wraps around great........")
logging.warning("THIS IS A REALY long WARNING Message that works and wraps around great........")
logging.error("\tTHIS IS A REALY long ERROR Message that works and wraps around great........")
logging.critical("THIS IS A REALY long CRITICAL Message that works and wraps around great........")

Where the output is (commented for readability):

#                 THIS IS A REALY long DEBUG Message that works and    [   DEBUG]
#                 wraps around great........
#    THIS IS A REALY long INFO Message that works and wraps around     [    INFO]
#    great........
# THIS IS A REALY long WARNING Message that works and wraps around     [ WARNING]
# great........
#         THIS IS A REALY long ERROR Message that works and wraps      [   ERROR]
#         around great........
# THIS IS A REALY long CRITICAL Message that works and wraps around    [CRITICAL]
# great........

Upvotes: 2

Martin Tournoij
Martin Tournoij

Reputation: 27822

I modified the last lines to look like:

logging.info("Starting File: %s" % os.path.basename(fn))
logging.info("%s" % ('-' * 15))
logging.info("        To read data: %s" % (time.time() - start))

Your error was using a newline (\n) and tab (\t) character.

Or, if you must keep the newline (which seems rather odd to me), you could manually add the spaces, like so:

logging.info("Starting File: %s\n%s%s" % (
    os.path.basename(fn),
    ('-' * 15),
    ' ' * (width - 15 - 12)))

Other notes

You should create a Minimal, Complete, Tested and Readable. Your code wasn't working, I needed to modify a few things just to get the example running. See your messages edit history for what I had to edit.

Since Python 3.3, there's os.get_terminal_size. If that isn't available, then doing subprocess.call(['tput cols'], shell=True) seems a whole lot simpler to me...

Upvotes: 1

Related Questions