Instantly share code, notes, and snippets.

Embed
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

This comment has been minimized.

Owner

vsajip commented Dec 30, 2010

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

@unux

This comment has been minimized.

unux commented Jun 23, 2011

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

@vsajip

This comment has been minimized.

Owner

vsajip commented Jun 23, 2011

@unux: Good point. Gist updated. Thanks.

@Parlane

This comment has been minimized.

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

This comment has been minimized.

noisebleed commented Apr 14, 2012

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

This comment has been minimized.

Owner

vsajip commented Apr 15, 2012

@noisebleed

This comment has been minimized.

noisebleed commented Apr 15, 2012

Done. Thanks.

@baby-gnu

This comment has been minimized.

baby-gnu commented Oct 24, 2012

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

This comment has been minimized.

baby-gnu commented Oct 24, 2012

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

lc4t commented Oct 21, 2016

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())
@heath730

This comment has been minimized.

heath730 commented Nov 20, 2018

For Win10 color support, I found a flush was needed:

diff --git a/ansistrm.py b/ansistrm.py
index a8ef384..e681aeb 100644
--- a/ansistrm.py
+++ b/ansistrm.py
@@ -89,6 +89,7 @@ class ColorizingStreamHandler(logging.StreamHandler):
                 text = parts.pop(0)
                 if text:
                     write(text)
+                    self.stream.flush()
                 if parts:
                     params = parts.pop(0)
                     if h is not None:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment