Skip to content

Commit

Permalink
Merge pull request #68 from jonathanj/67-graceful-rendering-exceptions
Browse files Browse the repository at this point in the history
Gracefully handle exceptions during node and value formatting.
  • Loading branch information
jonathanj committed Jul 22, 2018
2 parents 983999c + db7527c commit 86b5043
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 53 deletions.
2 changes: 2 additions & 0 deletions eliottree/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ def display_tasks(tasks, color, ignored_fields, field_limit, human_readable):
the task trees to stdout.
"""
write = text_writer(sys.stdout).write
write_err = text_writer(sys.stderr).write
if color == 'auto':
colorize = sys.stdout.isatty()
else:
colorize = color == 'always'
render_tasks(
write=write,
write_err=write_err,
tasks=tasks,
ignored_fields=set(ignored_fields) or None,
field_limit=field_limit,
Expand Down
69 changes: 59 additions & 10 deletions eliottree/_render.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import sys
import traceback
from functools import partial

from eliot._action import WrittenAction
from eliot._message import WrittenMessage
from eliot._parse import Task
from six import text_type
from termcolor import colored
from toolz import compose, identity
from toolz import compose, excepts, identity
from tree_format import format_tree

from eliottree import format
from eliottree._util import eliot_ns, format_namespace, is_namespace


RIGHT_DOUBLE_ARROW = u'\u21d2'
Expand All @@ -33,6 +37,7 @@ class COLORS(object):
success = Color('green')
failure = Color('red')
prop = Color('blue')
error = Color('red', ['bold'])

def __init__(self, colored):
self.colored = colored
Expand All @@ -52,7 +57,7 @@ def _default_value_formatter(human_readable, field_limit, encoding='utf-8'):
fields = {}
if human_readable:
fields = {
u'timestamp': format.timestamp(),
eliot_ns(u'timestamp'): format.timestamp(),
}
return compose(
# We want tree-format to handle newlines.
Expand Down Expand Up @@ -126,6 +131,8 @@ def format_node(format_value, colors, node):
value = u''
else:
value = format_value(value, key)
if is_namespace(key):
key = format_namespace(key)
return u'{}: {}'.format(
colors.prop(format.escape_control_characters(key)),
value)
Expand All @@ -138,13 +145,17 @@ def message_fields(message, ignored_fields):
"""
def _items():
try:
yield u'timestamp', message.timestamp
yield eliot_ns('timestamp'), message.timestamp
except KeyError:
pass
for key, value in message.contents.items():
if key not in ignored_fields:
yield key, value
return sorted(_items()) if message else []

def _sortkey(x):
k = x[0]
return format_namespace(k) if is_namespace(k) else k
return sorted(_items(), key=_sortkey) if message else []


def get_children(ignored_fields, node):
Expand Down Expand Up @@ -176,8 +187,20 @@ def get_children(ignored_fields, node):
return []


def track_exceptions(f, caught, default=None):
"""
Decorate ``f`` with a function that traps exceptions and appends them to
``caught``, returning ``default`` in their place.
"""
def _catch(_):
caught.append(sys.exc_info())
return default
return excepts(Exception, f, _catch)


def render_tasks(write, tasks, field_limit=0, ignored_fields=None,
human_readable=False, colorize=False):
human_readable=False, colorize=False, write_err=None,
format_node=format_node, format_value=None):
"""
Render Eliot tasks as an ASCII tree.
Expand All @@ -193,18 +216,44 @@ def render_tasks(write, tasks, field_limit=0, ignored_fields=None,
most Eliot metadata.
:param bool human_readable: Render field values as human-readable?
:param bool colorize: Colorized the output?
:type write_err: Callable[[`text_type`], None]
:param write_err: Callable used to write errors.
:param format_node: See `format_node`.
:type format_value: Callable[[Any], `text_type`]
:param format_value: Callable to format a value.
"""
if ignored_fields is None:
ignored_fields = DEFAULT_IGNORED_KEYS
_format_node = partial(
format_node,
_default_value_formatter(human_readable=human_readable,
field_limit=field_limit),
COLORS(colored if colorize else _no_color))
colors = COLORS(colored if colorize else _no_color)
caught_exceptions = []
if format_value is None:
format_value = _default_value_formatter(
human_readable=human_readable,
field_limit=field_limit)
_format_value = track_exceptions(
format_value,
caught_exceptions,
u'<value formatting exception>')
_format_node = track_exceptions(
partial(format_node, _format_value, colors),
caught_exceptions,
u'<node formatting exception>')
_get_children = partial(get_children, ignored_fields)
for task in tasks:
write(format_tree(task, _format_node, _get_children))
write(u'\n')

if write_err and caught_exceptions:
write_err(
colors.error(
u'Exceptions ({}) occurred during processing:\n'.format(
len(caught_exceptions))))
for exc in caught_exceptions:
for line in traceback.format_exception(*exc):
if not isinstance(line, text_type):
line = line.decode('utf-8')
write_err(line)
write_err(u'\n')


__all__ = ['render_tasks']
36 changes: 36 additions & 0 deletions eliottree/_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from collections import namedtuple


namespace = namedtuple('namespace', ['prefix', 'name'])


def namespaced(prefix):
"""
Create a function that creates new names in the ``prefix`` namespace.
:rtype: Callable[[unicode], `namespace`]
"""
return lambda name: namespace(prefix, name)


def format_namespace(ns):
"""
Format a `namespace`.
:rtype: unicode
"""
if not is_namespace(ns):
raise TypeError('Expected namespace', ns)
return u'{}/{}'.format(ns.prefix, ns.name)


def is_namespace(x):
"""
Is this a `namespace` instance?
:rtype: bool
"""
return isinstance(x, namespace)


eliot_ns = namespaced(u'eliot')
13 changes: 11 additions & 2 deletions eliottree/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from eliottree._render import (
COLORS, DEFAULT_IGNORED_KEYS, _default_value_formatter, _no_color)
from eliottree._util import is_namespace, eliot_ns
from eliottree.format import escape_control_characters


Expand Down Expand Up @@ -81,7 +82,10 @@ def get_name(task):
if isinstance(task, text_type):
return escape_control_characters(task)
elif isinstance(task, tuple):
name = escape_control_characters(task[0])
key = task[0]
if is_namespace(key):
key = key.name
name = escape_control_characters(key)
if isinstance(task[1], dict):
return name
elif isinstance(task[1], text_type):
Expand Down Expand Up @@ -129,6 +133,8 @@ def get_children_factory(ignored_task_keys, format_value):
def items_children(items):
for key, value in sorted(items):
if key not in ignored_task_keys:
if key == u'timestamp':
key = eliot_ns(key)
if isinstance(value, dict):
yield key, value
else:
Expand All @@ -150,7 +156,10 @@ def get_children(task):
return
else:
for child in items_children(task.task.items()):
yield child
if child[0] == u'timestamp':
yield eliot_ns(child[0]), child[1]
else:
yield child
for child in task.children():
yield child
return get_children
Expand Down
4 changes: 2 additions & 2 deletions eliottree/test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
rendered_message_task = (
u'cdeb220d-7605-4d5f-8341-1a170222e308\n'
u'\u2514\u2500\u2500 twisted:log/1\n'
u' \u251c\u2500\u2500 eliot/timestamp: 2015-03-03 04:25:00\n'
u' \u251c\u2500\u2500 error: False\n'
u' \u251c\u2500\u2500 message: Main loop terminated.\n'
u' \u2514\u2500\u2500 timestamp: 2015-03-03 04:25:00\n\n'
u' \u2514\u2500\u2500 message: Main loop terminated.\n\n'
).encode('utf-8')


Expand Down
Loading

0 comments on commit 86b5043

Please sign in to comment.