Skip to content

Commit 282758f

Browse files
author
Jens Timmerman
authored
Merge pull request #225 from riccardomurri/coloredlogs
Add support for colorization of log messages sent to the screen.
2 parents 4e7a895 + eaa8adf commit 282758f

File tree

3 files changed

+260
-8
lines changed

3 files changed

+260
-8
lines changed

lib/vsc/utils/fancylogger.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
@author: Kenneth Hoste (Ghent University)
7676
"""
7777

78+
from collections import namedtuple
7879
import inspect
7980
import logging
8081
import logging.handlers
@@ -85,6 +86,79 @@
8586
import weakref
8687
from distutils.version import LooseVersion
8788

89+
90+
def _env_to_boolean(varname, default=False):
91+
"""
92+
Compute a boolean based on the truth value of environment variable `varname`.
93+
If no variable by that name is present in `os.environ`, then return `default`.
94+
95+
For the purpose of this function, the string values ``'1'``,
96+
``'y'``, ``'yes'``, and ``'true'`` (case-insensitive) are all
97+
mapped to the truth value ``True``::
98+
99+
>>> os.environ['NO_FOOBAR'] = '1'
100+
>>> _env_to_boolean('NO_FOOBAR')
101+
True
102+
>>> os.environ['NO_FOOBAR'] = 'Y'
103+
>>> _env_to_boolean('NO_FOOBAR')
104+
True
105+
>>> os.environ['NO_FOOBAR'] = 'Yes'
106+
>>> _env_to_boolean('NO_FOOBAR')
107+
True
108+
>>> os.environ['NO_FOOBAR'] = 'yes'
109+
>>> _env_to_boolean('NO_FOOBAR')
110+
True
111+
>>> os.environ['NO_FOOBAR'] = 'True'
112+
>>> _env_to_boolean('NO_FOOBAR')
113+
True
114+
>>> os.environ['NO_FOOBAR'] = 'TRUE'
115+
>>> _env_to_boolean('NO_FOOBAR')
116+
True
117+
>>> os.environ['NO_FOOBAR'] = 'true'
118+
>>> _env_to_boolean('NO_FOOBAR')
119+
True
120+
121+
Any other value is mapped to Python ``False``::
122+
123+
>>> os.environ['NO_FOOBAR'] = '0'
124+
>>> _env_to_boolean('NO_FOOBAR')
125+
False
126+
>>> os.environ['NO_FOOBAR'] = 'no'
127+
>>> _env_to_boolean('NO_FOOBAR')
128+
False
129+
>>> os.environ['NO_FOOBAR'] = 'if you please'
130+
>>> _env_to_boolean('NO_FOOBAR')
131+
False
132+
133+
If no variable named `varname` is present in `os.environ`, then
134+
return `default`::
135+
136+
>>> del os.environ['NO_FOOBAR']
137+
>>> _env_to_boolean('NO_FOOBAR', 42)
138+
42
139+
140+
By default, calling `_env_to_boolean` on an undefined
141+
variable returns Python ``False``::
142+
143+
>>> if 'NO_FOOBAR' in os.environ: del os.environ['NO_FOOBAR']
144+
>>> _env_to_boolean('NO_FOOBAR')
145+
False
146+
"""
147+
if varname not in os.environ:
148+
return default
149+
else:
150+
return os.environ.get(varname).lower() in ('1', 'yes', 'true', 'y')
151+
152+
153+
HAVE_COLOREDLOGS_MODULE = False
154+
if not _env_to_boolean('FANCYLOGGER_NO_COLOREDLOGS'):
155+
try:
156+
import coloredlogs
157+
import humanfriendly
158+
HAVE_COLOREDLOGS_MODULE = True
159+
except ImportError:
160+
pass
161+
88162
# constants
89163
TEST_LOGGING_FORMAT = '%(levelname)-10s %(name)-15s %(threadName)-10s %(message)s'
90164
DEFAULT_LOGGING_FORMAT = '%(asctime)-15s ' + TEST_LOGGING_FORMAT
@@ -101,6 +175,9 @@
101175

102176
DEFAULT_UDP_PORT = 5005
103177

178+
# poor man's enum
179+
Colorize = namedtuple('Colorize', 'AUTO ALWAYS NEVER')('auto', 'always', 'never')
180+
104181
# register new loglevelname
105182
logging.addLevelName(logging.CRITICAL * 2 + 1, 'APOCALYPTIC')
106183
# register QUIET, EXCEPTION and FATAL alias
@@ -111,7 +188,7 @@
111188

112189
# mpi rank support
113190
_MPIRANK = MPIRANK_NO_MPI
114-
if os.environ.get('FANCYLOGGER_IGNORE_MPI4PY', '0').lower() not in ('1', 'yes', 'true', 'y'):
191+
if not _env_to_boolean('FANCYLOGGER_IGNORE_MPI4PY'):
115192
try:
116193
from mpi4py import MPI
117194
if MPI.Is_initialized():
@@ -383,7 +460,7 @@ def getLogger(name=None, fname=False, clsname=False, fancyrecord=None):
383460

384461
l = logging.getLogger(fullname)
385462
l.fancyrecord = fancyrecord
386-
if os.environ.get('FANCYLOGGER_GETLOGGER_DEBUG', '0').lower() in ('1', 'yes', 'true', 'y'):
463+
if _env_to_boolean('FANCYLOGGER_GETLOGGER_DEBUG'):
387464
print 'FANCYLOGGER_GETLOGGER_DEBUG',
388465
print 'name', name, 'fname', fname, 'fullname', fullname,
389466
print "getRootLoggerName: ", getRootLoggerName()
@@ -435,7 +512,7 @@ def getRootLoggerName():
435512
return "not available in optimized mode"
436513

437514

438-
def logToScreen(enable=True, handler=None, name=None, stdout=False):
515+
def logToScreen(enable=True, handler=None, name=None, stdout=False, colorize=Colorize.NEVER):
439516
"""
440517
enable (or disable) logging to screen
441518
returns the screenhandler (this can be used to later disable logging to screen)
@@ -447,15 +524,22 @@ def logToScreen(enable=True, handler=None, name=None, stdout=False):
447524
448525
by default, logToScreen will log to stderr; logging to stdout instead can be done
449526
by setting the 'stdout' parameter to True
527+
528+
The `colorize` parameter enables or disables log colorization using
529+
ANSI terminal escape sequences, according to the values allowed
530+
in the `colorize` parameter to function `_screenLogFormatterFactory`
531+
(which see).
450532
"""
451533
handleropts = {'stdout': stdout}
534+
formatter = _screenLogFormatterFactory(colorize=colorize, stream=(sys.stdout if stdout else sys.stderr))
452535

453536
return _logToSomething(FancyStreamHandler,
454537
handleropts,
455538
loggeroption='logtoscreen_stdout_%s' % str(stdout),
456539
name=name,
457540
enable=enable,
458541
handler=handler,
542+
formatterclass=formatter,
459543
)
460544

461545

@@ -516,17 +600,22 @@ def logToUDP(hostname, port=5005, enable=True, datagramhandler=None, name=None):
516600
)
517601

518602

519-
def _logToSomething(handlerclass, handleropts, loggeroption, enable=True, name=None, handler=None):
603+
def _logToSomething(handlerclass, handleropts, loggeroption,
604+
enable=True, name=None, handler=None, formatterclass=None):
520605
"""
521606
internal function to enable (or disable) logging to handler named handlername
522-
handleropts is options dictionary passed to create the handler instance
607+
handleropts is options dictionary passed to create the handler instance;
608+
`formatterclass` is the class to use to instantiate a log formatter object.
523609
524610
returns the handler (this can be used to later disable logging to file)
525611
526612
if you want to disable logging to the handler, pass the earlier obtained handler
527613
"""
528614
logger = getLogger(name, fname=False, clsname=False)
529615

616+
if formatterclass is None:
617+
formatterclass = logging.Formatter
618+
530619
if not hasattr(logger, loggeroption):
531620
# not set.
532621
setattr(logger, loggeroption, False) # set default to False
@@ -538,7 +627,7 @@ def _logToSomething(handlerclass, handleropts, loggeroption, enable=True, name=N
538627
f_format = DEFAULT_LOGGING_FORMAT
539628
else:
540629
f_format = FANCYLOG_LOGGING_FORMAT
541-
formatter = logging.Formatter(f_format)
630+
formatter = formatterclass(f_format)
542631
handler = handlerclass(**handleropts)
543632
handler.setFormatter(formatter)
544633
logger.addHandler(handler)
@@ -566,6 +655,36 @@ def _logToSomething(handlerclass, handleropts, loggeroption, enable=True, name=N
566655
return handler
567656

568657

658+
def _screenLogFormatterFactory(colorize=Colorize.NEVER, stream=sys.stdout):
659+
"""
660+
Return a log formatter class, with optional colorization features.
661+
662+
Second argument `colorize` controls whether the formatter
663+
can use ANSI terminal escape sequences:
664+
665+
* ``Colorize.NEVER`` (default) forces use the plain `logging.Formatter` class;
666+
* ``Colorize.ALWAYS`` forces use of the colorizing formatter;
667+
* ``Colorize.AUTO`` selects the colorizing formatter depending on
668+
whether `stream` is connected to a terminal.
669+
670+
Second argument `stream` is the stream to check in case `colorize`
671+
is ``Colorize.AUTO``.
672+
"""
673+
formatter = logging.Formatter # default
674+
if HAVE_COLOREDLOGS_MODULE:
675+
if colorize == Colorize.AUTO:
676+
# auto-detect
677+
if humanfriendly.terminal.terminal_supports_colors(stream):
678+
formatter = coloredlogs.ColoredFormatter
679+
elif colorize == Colorize.ALWAYS:
680+
formatter = coloredlogs.ColoredFormatter
681+
elif colorize == Colorize.NEVER:
682+
pass
683+
else:
684+
raise ValueError("Argument `colorize` must be one of 'auto', 'always', or 'never'.")
685+
return formatter
686+
687+
569688
def _getSysLogFacility(name=None):
570689
"""Look for proper syslog facility
571690
typically the syslog/rsyslog config has an entry
@@ -605,7 +724,7 @@ def setLogLevel(level):
605724
level = getLevelInt(level)
606725
logger = getLogger(fname=False, clsname=False)
607726
logger.setLevel(level)
608-
if os.environ.get('FANCYLOGGER_LOGLEVEL_DEBUG', '0').lower() in ('1', 'yes', 'true', 'y'):
727+
if _env_to_boolean('FANCYLOGGER_LOGLEVEL_DEBUG'):
609728
print "FANCYLOGGER_LOGLEVEL_DEBUG", level, logging.getLevelName(level)
610729
print "\n".join(logger.get_parent_info("FANCYLOGGER_LOGLEVEL_DEBUG"))
611730
sys.stdout.flush()

setup.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,23 @@
3838

3939
VSC_INSTALL_REQ_VERSION = '0.10.1'
4040

41+
_coloredlogs_pkgs = [
42+
'coloredlogs', # automatic log colorizer
43+
'humanfriendly', # detect if terminal has colors
44+
]
45+
4146
PACKAGE = {
42-
'version': '2.5.2',
47+
'version': '2.5.3',
4348
'author': [sdw, jt, ag, kh],
4449
'maintainer': [sdw, jt, ag, kh],
4550
# as long as 1.0.0 is not out, vsc-base should still provide vsc.fancylogger
4651
# setuptools must become a requirement for shared namespaces if vsc-install is removed as requirement
4752
'install_requires': ['vsc-install >= %s' % VSC_INSTALL_REQ_VERSION],
53+
'extras_require': {
54+
'coloredlogs': _coloredlogs_pkgs,
55+
},
4856
'setup_requires': ['vsc-install >= %s' % VSC_INSTALL_REQ_VERSION],
57+
'tests_require': ['prospector'] + _coloredlogs_pkgs,
4958
}
5059

5160
if __name__ == '__main__':

0 commit comments

Comments
 (0)