Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include *.rst
include *.txt
include coloredlogs.pth
graft docs
32 changes: 29 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ defined by my verboselogs_ package: if you install both `coloredlogs` and

.. contents::
:local:
:depth: 1

Format of log messages
----------------------
Expand All @@ -46,6 +45,30 @@ You can customize the log format and styling using environment variables as
well as programmatically, please refer to the `online documentation`_ for
details.

Enabling millisecond precision
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you're switching from `logging.basicConfig()`_ to `coloredlogs.install()`_
you may notice that timestamps no longer include milliseconds. This is because
coloredlogs doesn't output milliseconds in timestamps unless you explicitly
tell it to. There are two ways to do that:

1. The easy way is to pass the `milliseconds` argument to
`coloredlogs.install()`_::

coloredlogs.install(milliseconds=True)

2. Alternatively you can change the log format `to include 'msecs'`_::

%(asctime)s,%(msecs)03d %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s

Here's what the call to `coloredlogs.install()`_ would then look like::

coloredlogs.install(fmt='%(asctime)s,%(msecs)03d %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s')

Customizing the log format also enables you to change the delimiter that
separates seconds from milliseconds (the comma above).

Usage
-----

Expand All @@ -56,7 +79,7 @@ Here's an example of how easy it is to get started:
import coloredlogs, logging

# Create a logger object.
logger = logging.getLogger('your-module')
logger = logging.getLogger(__name__)

# By default the install() function installs a handler on the root logger,
# this means that log messages from your code and log messages from the
Expand All @@ -71,7 +94,7 @@ Here's an example of how easy it is to get started:
# Some examples.
logger.debug("this is a debugging message")
logger.info("this is an informational message")
logger.warn("this is a warning message")
logger.warning("this is a warning message")
logger.error("this is an error message")
logger.critical("this is a critical message")

Expand Down Expand Up @@ -147,12 +170,15 @@ This software is licensed under the `MIT license`_.
.. _Colorama: https://pypi.python.org/pypi/colorama
.. _ColoredCronMailer: https://coloredlogs.readthedocs.io/en/latest/#coloredlogs.converter.ColoredCronMailer
.. _ColoredFormatter: https://coloredlogs.readthedocs.io/en/latest/#coloredlogs.ColoredFormatter
.. _coloredlogs.install(): https://coloredlogs.readthedocs.io/en/latest/#coloredlogs.install
.. _cron: https://en.wikipedia.org/wiki/Cron
.. _GitHub: https://github.com/xolox/python-coloredlogs
.. _logging.basicConfig(): https://docs.python.org/2/library/logging.html#logging.basicConfig
.. _logging.Formatter: https://docs.python.org/2/library/logging.html#logging.Formatter
.. _logging: https://docs.python.org/2/library/logging.html
.. _MIT license: https://en.wikipedia.org/wiki/MIT_License
.. _online documentation: https://coloredlogs.readthedocs.io/
.. [email protected]: [email protected]
.. _PyPI: https://pypi.python.org/pypi/coloredlogs
.. _to include 'msecs': https://stackoverflow.com/questions/6290739/python-logging-use-milliseconds-in-time-format
.. _verboselogs: https://pypi.python.org/pypi/verboselogs
37 changes: 33 additions & 4 deletions coloredlogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Colored terminal output for Python's logging module.
#
# Author: Peter Odding <[email protected]>
# Last Change: May 18, 2017
# Last Change: August 7, 2017
# URL: https://coloredlogs.readthedocs.io

"""
Expand Down Expand Up @@ -199,7 +199,7 @@
NEED_COLORAMA = WINDOWS

# Semi-standard module versioning.
__version__ = '7.0'
__version__ = '7.3'

DEFAULT_LOG_LEVEL = logging.INFO
"""The default log level for :mod:`coloredlogs` (:data:`logging.INFO`)."""
Expand Down Expand Up @@ -237,6 +237,7 @@
info=dict(),
notice=dict(color='magenta'),
warning=dict(color='yellow'),
success=dict(color='green', bold=CAN_USE_BOLD_FONT),
error=dict(color='red'),
critical=dict(color='red', bold=CAN_USE_BOLD_FONT))
"""Mapping of log level names to default font styles."""
Expand Down Expand Up @@ -273,6 +274,9 @@ def install(level=None, **kw):
:data:`DEFAULT_LOG_FORMAT`).
:param datefmt: Set the date/time format (a string, defaults to
:data:`DEFAULT_DATE_FORMAT`).
:param milliseconds: :data:`True` to show milliseconds like :mod:`logging`
does by default, :data:`False` to hide milliseconds
(the default is :data:`False`, see `#16`_).
:param level_styles: A dictionary with custom level styles (defaults to
:data:`DEFAULT_LEVEL_STYLES`).
:param field_styles: A dictionary with custom field styles (defaults to
Expand Down Expand Up @@ -327,6 +331,8 @@ def install(level=None, **kw):

6. The formatter is added to the handler and the handler is added to the
logger.

.. _issue 16: https://github.com/xolox/python-coloredlogs/issues/16
"""
logger = kw.get('logger') or logging.getLogger()
reconfigure = kw.get('reconfigure', True)
Expand Down Expand Up @@ -401,6 +407,20 @@ def install(level=None, **kw):
# back to the default).
if not formatter_options['datefmt']:
formatter_options['datefmt'] = os.environ.get('COLOREDLOGS_DATE_FORMAT') or DEFAULT_DATE_FORMAT
# Python's logging module shows milliseconds by default through special
# handling in the logging.Formatter.formatTime() method [1]. Because
# coloredlogs always defines a `datefmt' it bypasses this special
# handling, which is fine because ever since publishing coloredlogs
# I've never needed millisecond precision ;-). However there are users
# of coloredlogs that do want milliseconds to be shown [2] so we
# provide a shortcut to make it easy.
#
# [1] https://stackoverflow.com/questions/6290739/python-logging-use-milliseconds-in-time-format
# [2] https://github.com/xolox/python-coloredlogs/issues/16
if kw.get('milliseconds') and '%(msecs)' not in formatter_options['fmt']:
formatter_options['fmt'] = formatter_options['fmt'].replace(
'%(asctime)s', '%(asctime)s,%(msecs)03d',
)
# Do we need to make %(hostname) available to the formatter?
HostNameFilter.install(
handler=handler,
Expand Down Expand Up @@ -873,14 +893,23 @@ def format(self, record):
class.
"""
style = self.nn.get(self.level_styles, record.levelname)
if style:
# After the introduction of the `Empty' class it was reported in issue
# 33 that format() can be called when `Empty' has already been garbage
# collected. This explains the (otherwise rather out of place) `Empty
# is not None' check in the following `if' statement. The reasoning
# here is that it's much better to log a message without formatting
# then to raise an exception ;-).
#
# For more details refer to issue 33 on GitHub:
# https://github.com/xolox/python-coloredlogs/issues/33
if style and Empty is not None:
# Due to the way that Python's logging module is structured and
# documented the only (IMHO) clean way to customize its behavior is
# to change incoming LogRecord objects before they get to the base
# formatter. However we don't want to break other formatters and
# handlers, so we copy the log record.
#
# In the past this used copy.copy() but as reported in issue #29
# In the past this used copy.copy() but as reported in issue 29
# (which is reproducible) this can cause deadlocks. The following
# Python voodoo is intended to accomplish the same thing as
# copy.copy() without all of the generalization and overhead that
Expand Down
4 changes: 2 additions & 2 deletions coloredlogs/demo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Demonstration of the coloredlogs package.
#
# Author: Peter Odding <[email protected]>
# Last Change: May 17, 2017
# Last Change: August 7, 2017
# URL: https://coloredlogs.readthedocs.io

"""A simple demonstration of the `coloredlogs` package."""
Expand Down Expand Up @@ -32,7 +32,7 @@ def demonstrate_colored_logging():
# DEBUG logging level but enable the user the customize it.
coloredlogs.install(level=os.environ.get('COLOREDLOGS_LOG_LEVEL', 'SPAM'))
# Print some examples with different timestamps.
for level in ['spam', 'debug', 'verbose', 'info', 'notice', 'warn', 'error', 'critical']:
for level in ['spam', 'debug', 'verbose', 'info', 'success', 'notice', 'warning', 'error', 'critical']:
if hasattr(logger, level):
getattr(logger, level)("message with level %r", level)
time.sleep(DEMO_DELAY)
Expand Down
40 changes: 34 additions & 6 deletions coloredlogs/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Automated tests for the `coloredlogs' package.
#
# Author: Peter Odding <[email protected]>
# Last Change: May 17, 2017
# Last Change: August 7, 2017
# URL: https://coloredlogs.readthedocs.io

"""Automated tests for the `coloredlogs` package."""
Expand Down Expand Up @@ -66,6 +66,18 @@
\s (?P<message> .* )
''', re.VERBOSE)

# Compiled regular expression that matches a single line of output produced by
# the default log format with milliseconds=True.
PATTERN_INCLUDING_MILLISECONDS = re.compile(r'''
(?P<date> \d{4}-\d{2}-\d{2} )
\s (?P<time> \d{2}:\d{2}:\d{2},\d{3} )
\s (?P<hostname> \S+ )
\s (?P<logger_name> \w+ )
\[ (?P<process_id> \d+ ) \]
\s (?P<severity> [A-Z]+ )
\s (?P<message> .* )
''', re.VERBOSE)

# The pathname of the system log file on Ubuntu Linux (my laptops and Travis CI).
UNIX_SYSTEM_LOG = '/var/log/syslog'

Expand All @@ -85,7 +97,7 @@ def test_level_to_number(self):
# Make sure the default levels are translated as expected.
assert level_to_number('debug') == logging.DEBUG
assert level_to_number('info') == logging.INFO
assert level_to_number('warn') == logging.WARNING
assert level_to_number('warning') == logging.WARNING
assert level_to_number('error') == logging.ERROR
assert level_to_number('fatal') == logging.FATAL
# Make sure bogus level names don't blow up.
Expand Down Expand Up @@ -177,11 +189,11 @@ def test_syslog_shortcut_simple(self):
assert any(expected_message in line for line in handle)

def test_syslog_shortcut_enhanced(self):
"""Make sure that ``coloredlogs.install(syslog='warn')`` works."""
"""Make sure that ``coloredlogs.install(syslog='warning')`` works."""
with cleanup_handlers():
the_expected_message = random_string(50)
not_an_expected_message = random_string(50)
coloredlogs.install(syslog='warn')
coloredlogs.install(syslog='warning')
logging.info("%s", not_an_expected_message)
logging.warning("%s", the_expected_message)
if os.path.isfile(UNIX_SYSTEM_LOG):
Expand Down Expand Up @@ -250,7 +262,10 @@ def test_decrease_verbosity(self):
# NOTICE -> WARNING.
decrease_verbosity()
assert get_level() == logging.WARNING
# WARNING -> ERROR.
# WARNING -> SUCCESS.
decrease_verbosity()
assert get_level() == logging.SUCCESS
# SUCCESS -> ERROR.
decrease_verbosity()
assert get_level() == logging.ERROR
# ERROR -> CRITICAL.
Expand Down Expand Up @@ -308,6 +323,19 @@ def get_logger_tree(self):
grand_child = logging.getLogger(grand_child_name)
return root, parent, child, grand_child

def test_support_for_milliseconds(self):
"""Make sure milliseconds are hidden by default but can be easily enabled."""
# Check that the default log format doesn't include milliseconds.
stream = StringIO()
install(reconfigure=True, stream=stream)
logging.info("This should not include milliseconds.")
assert all(map(PLAIN_TEXT_PATTERN.match, stream.getvalue().splitlines()))
# Check that milliseconds can be enabled via a shortcut.
stream = StringIO()
install(milliseconds=True, reconfigure=True, stream=stream)
logging.info("This should include milliseconds.")
assert all(map(PATTERN_INCLUDING_MILLISECONDS.match, stream.getvalue().splitlines()))

def test_plain_text_output_format(self):
"""Inspect the plain text output of coloredlogs."""
logger = VerboseLogger(random_string(25))
Expand All @@ -322,7 +350,7 @@ def test_plain_text_output_format(self):
for method, severity in ((logger.debug, 'DEBUG'),
(logger.info, 'INFO'),
(logger.verbose, 'VERBOSE'),
(logger.warning, 'WARN'),
(logger.warning, 'WARNING'),
(logger.error, 'ERROR'),
(logger.critical, 'CRITICAL')):
# Prepare the text.
Expand Down
2 changes: 1 addition & 1 deletion requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ coverage >= 4.2
mock >= 1.0.1
pytest >= 3.0.3
pytest-cov >= 2.3.1
verboselogs >= 1.5
verboselogs >= 1.7