Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Python logging: colourising terminal output
#
# Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. Licensed under the new BSD license.
#
import ctypes
import logging
import os
class ColorizingStreamHandler(logging.StreamHandler):
# color names to indices
color_map = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'white': 7,
}
#levels to (background, foreground, bold/intense)
if os.name == 'nt':
level_map = {
logging.DEBUG: (None, 'blue', True),
logging.INFO: (None, 'white', False),
logging.WARNING: (None, 'yellow', True),
logging.ERROR: (None, 'red', True),
logging.CRITICAL: ('red', 'white', True),
}
else:
level_map = {
logging.DEBUG: (None, 'blue', False),
logging.INFO: (None, 'black', False),
logging.WARNING: (None, 'yellow', False),
logging.ERROR: (None, 'red', False),
logging.CRITICAL: ('red', 'white', True),
}
csi = '\x1b['
reset = '\x1b[0m'
@property
def is_tty(self):
isatty = getattr(self.stream, 'isatty', None)
return isatty and isatty()
def emit(self, record):
try:
message = self.format(record)
stream = self.stream
if not self.is_tty:
stream.write(message)
else:
self.output_colorized(message)
stream.write(getattr(self, 'terminator', '\n'))
self.flush()
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
if os.name != 'nt':
def output_colorized(self, message):
self.stream.write(message)
else:
import re
ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m')
nt_color_map = {
0: 0x00, # black
1: 0x04, # red
2: 0x02, # green
3: 0x06, # yellow
4: 0x01, # blue
5: 0x05, # magenta
6: 0x03, # cyan
7: 0x07, # white
}
def output_colorized(self, message):
parts = self.ansi_esc.split(message)
write = self.stream.write
h = None
fd = getattr(self.stream, 'fileno', None)
if fd is not None:
fd = fd()
if fd in (1, 2): # stdout or stderr
h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
while parts:
text = parts.pop(0)
if text:
write(text)
if parts:
params = parts.pop(0)
if h is not None:
params = [int(p) for p in params.split(';')]
color = 0
for p in params:
if 40 <= p <= 47:
color |= self.nt_color_map[p - 40] << 4
elif 30 <= p <= 37:
color |= self.nt_color_map[p - 30]
elif p == 1:
color |= 0x08 # foreground intensity on
elif p == 0: # reset to default color
color = 0x07
else:
pass # error condition ignored
ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
def colorize(self, message, record):
if record.levelno in self.level_map:
bg, fg, bold = self.level_map[record.levelno]
params = []
if bg in self.color_map:
params.append(str(self.color_map[bg] + 40))
if fg in self.color_map:
params.append(str(self.color_map[fg] + 30))
if bold:
params.append('1')
if params:
message = ''.join((self.csi, ';'.join(params),
'm', message, self.reset))
return message
def format(self, record):
message = logging.StreamHandler.format(self, record)
if self.is_tty:
# Don't colorize any traceback
parts = message.split('\n', 1)
parts[0] = self.colorize(parts[0], record)
message = '\n'.join(parts)
return message
def main():
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(ColorizingStreamHandler())
logging.debug('DEBUG')
logging.info('INFO')
logging.warning('WARNING')
logging.error('ERROR')
logging.critical('CRITICAL')
if __name__ == '__main__':
main()
@vsajip
Owner
vsajip commented Dec 30, 2010

Gist updated to use non-capturing groups. Thanks to Marius Gedminas for the suggestion.

@unux
unux commented Jun 23, 2011

this also seems to only require ctypes fi you're using NT

@vsajip
Owner
vsajip commented Jun 23, 2011

@unux: Good point. Gist updated. Thanks.

@Parlane
Parlane commented Apr 8, 2012

With python 2.7, this didn't work for me on windows 64bit without "import ctypes" at the top :. Either way, thanks :D

@noisebleed

I was looking for this! Thanks for sharing. I'm using your code in a github project I'm developing. Is it enough to keep the copyright notice at the top of the file? Module is here.

@vsajip
Owner
vsajip commented Apr 15, 2012
@noisebleed

Done. Thanks.

@baby-gnu

Providing an __init__() will facilitate the customization when using dictConfig()

    def __init__(self, level_map=None, *args, **kwargs):
        if level_map is not None:
            self.level_map = level_map
        logging.StreamHandler.__init__(self, *args, **kwargs)
@baby-gnu

According to the code, the logging.INFO level seems to be the terminal default, if you comment it or set all params to None you do not have to make a special case for nt:

--- ansistrm.py.orig    2012-10-24 15:52:45.051056546 +0200
+++ ansistrm.py 2012-10-24 15:57:06.599067746 +0200
@@ -19,25 +19,21 @@
     }

     #levels to (background, foreground, bold/intense)
-    if os.name == 'nt':
-        level_map = {
-            logging.DEBUG: (None, 'blue', True),
-            logging.INFO: (None, 'white', False),
-            logging.WARNING: (None, 'yellow', True),
-            logging.ERROR: (None, 'red', True),
-            logging.CRITICAL: ('red', 'white', True),
-        }
-    else:
-        level_map = {
-            logging.DEBUG: (None, 'blue', False),
-            logging.INFO: (None, 'black', False),
-            logging.WARNING: (None, 'yellow', False),
-            logging.ERROR: (None, 'red', False),
-            logging.CRITICAL: ('red', 'white', True),
-        }
+    level_map = {
+        logging.DEBUG: (None, 'blue', False),
+        logging.INFO: (None, None, False),
+        logging.WARNING: (None, 'yellow', False),
+        logging.ERROR: (None, 'red', False),
+        logging.CRITICAL: ('red', 'white', True),
+    }
     csi = '\x1b['
     reset = '\x1b[0m'

+    def __init__(self, level_map=None, *args, **kwargs):
+        if level_map is not None:
+            self.level_map = level_map
+        logging.StreamHandler.__init__(self, *args, **kwargs)
+
     @property
     def is_tty(self):
         isatty = getattr(self.stream, 'isatty', None)
@5nizza
5nizza commented Apr 10, 2014

Thanks for the gist, well done!
On my terminal (gnome 3.4.1.1) white you specified in the color_map is 'normal white' but not the one the terminal actually uses ('bright white'), so I had to change in the color_map to 'white':9 instead of original 7. Value 9 should mean the default text color.

@thecere
thecere commented Apr 15, 2014

After switching from win7 64bit to win8 64bit the following ctypes declaration was needed:

import ctypes
ctypes.windll.kernel32.SetConsoleTextAttribute.argtypes = [ctypes.c_ulong, ctypes.c_ushort]
@lc4t
lc4t commented Oct 21, 2016 edited

for python3 support:

import sys
if os.name != 'nt':
    def output_colorized(self, message):
        if sys.version[0] == '2':
            self.stream.write(message)
        else:
            self.stream.write(message.decode())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment