Skip to content

Instantly share code, notes, and snippets.

@vsajip
Created December 29, 2010 11:14
Show Gist options
  • Star 60 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save vsajip/758430 to your computer and use it in GitHub Desktop.
Save vsajip/758430 to your computer and use it in GitHub Desktop.
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
Copy link
Author

vsajip commented Dec 30, 2010

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

@unux
Copy link

unux commented Jun 23, 2011

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

@vsajip
Copy link
Author

vsajip commented Jun 23, 2011

@unux: Good point. Gist updated. Thanks.

@Parlane
Copy link

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

@vitorbrandao
Copy link

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
Copy link
Author

vsajip commented Apr 15, 2012 via email

@vitorbrandao
Copy link

Done. Thanks.

@baby-gnu
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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

@heathhenley
Copy link

heathhenley 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:

@zkghost
Copy link

zkghost commented Mar 12, 2020

Seems like info doesnt print on python3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment