Skip to content

Instantly share code, notes, and snippets.

@vsajip
Created December 29, 2010 11:14
Show Gist options
  • Select an option

  • Save vsajip/758430 to your computer and use it in GitHub Desktop.

Select an option

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

vsajip commented Dec 30, 2010

Copy link
Copy Markdown
Author

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

@unux

unux commented Jun 23, 2011

Copy link
Copy Markdown

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

@vsajip

vsajip commented Jun 23, 2011

Copy link
Copy Markdown
Author

@unux: Good point. Gist updated. Thanks.

@Parlane

Parlane commented Apr 8, 2012

Copy link
Copy Markdown

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

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

vsajip commented Apr 15, 2012 via email

Copy link
Copy Markdown
Author

@vitorbrandao

Copy link
Copy Markdown

Done. Thanks.

@baby-gnu

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

Copy link
Copy Markdown

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

Copy link
Copy Markdown

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

heathhenley commented Nov 20, 2018

Copy link
Copy Markdown

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

zkghost commented Mar 12, 2020

Copy link
Copy Markdown

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