1# This file is dual licensed under the terms of the Apache License, Version 2# 2.0, and the MIT License. See the LICENSE file in the root of this 3# repository for complete details. 4 5""" 6Helpers that make development with ``structlog`` more pleasant. 7""" 8 9from __future__ import absolute_import, division, print_function 10 11from six import StringIO 12 13 14try: 15 import colorama 16except ImportError: 17 colorama = None 18 19 20__all__ = ["ConsoleRenderer"] 21 22 23_MISSING = "{who} requires the {package} package installed. " 24_EVENT_WIDTH = 30 # pad the event name to so many characters 25 26 27def _pad(s, l): 28 """ 29 Pads *s* to length *l*. 30 """ 31 missing = l - len(s) 32 return s + " " * (missing if missing > 0 else 0) 33 34 35if colorama is not None: 36 _has_colorama = True 37 38 RESET_ALL = colorama.Style.RESET_ALL 39 BRIGHT = colorama.Style.BRIGHT 40 DIM = colorama.Style.DIM 41 RED = colorama.Fore.RED 42 BLUE = colorama.Fore.BLUE 43 CYAN = colorama.Fore.CYAN 44 MAGENTA = colorama.Fore.MAGENTA 45 YELLOW = colorama.Fore.YELLOW 46 GREEN = colorama.Fore.GREEN 47 RED_BACK = colorama.Back.RED 48else: 49 _has_colorama = False 50 51 RESET_ALL = ( 52 BRIGHT 53 ) = DIM = RED = BLUE = CYAN = MAGENTA = YELLOW = GREEN = RED_BACK = "" 54 55 56class _ColorfulStyles(object): 57 reset = RESET_ALL 58 bright = BRIGHT 59 60 level_critical = RED 61 level_exception = RED 62 level_error = RED 63 level_warn = YELLOW 64 level_info = GREEN 65 level_debug = GREEN 66 level_notset = RED_BACK 67 68 timestamp = DIM 69 logger_name = BLUE 70 kv_key = CYAN 71 kv_value = MAGENTA 72 73 74class _PlainStyles(object): 75 reset = "" 76 bright = "" 77 78 level_critical = "" 79 level_exception = "" 80 level_error = "" 81 level_warn = "" 82 level_info = "" 83 level_debug = "" 84 level_notset = "" 85 86 timestamp = "" 87 logger_name = "" 88 kv_key = "" 89 kv_value = "" 90 91 92class ConsoleRenderer(object): 93 """ 94 Render `event_dict` nicely aligned, possibly in colors, and ordered. 95 96 :param int pad_event: Pad the event to this many characters. 97 :param bool colors: Use colors for a nicer output. 98 :param bool force_colors: Force colors even for non-tty destinations. 99 Use this option if your logs are stored in a file that is meant 100 to be streamed to the console. 101 :param bool repr_native_str: When ``True``, :func:`repr()` is also applied 102 to native strings (i.e. unicode on Python 3 and bytes on Python 2). 103 Setting this to ``False`` is useful if you want to have human-readable 104 non-ASCII output on Python 2. The `event` key is *never* 105 :func:`repr()` -ed. 106 :param dict level_styles: When present, use these styles for colors. This 107 must be a dict from level names (strings) to colorama styles. The 108 default can be obtained by calling 109 :meth:`ConsoleRenderer.get_default_level_styles` 110 111 Requires the colorama_ package if *colors* is ``True``. 112 113 .. _colorama: https://pypi.org/project/colorama/ 114 115 .. versionadded:: 16.0 116 .. versionadded:: 16.1 *colors* 117 .. versionadded:: 17.1 *repr_native_str* 118 .. versionadded:: 18.1 *force_colors* 119 .. versionadded:: 18.1 *level_styles* 120 """ 121 122 def __init__( 123 self, 124 pad_event=_EVENT_WIDTH, 125 colors=True, 126 force_colors=False, 127 repr_native_str=False, 128 level_styles=None, 129 ): 130 if colors is True: 131 if colorama is None: 132 raise SystemError( 133 _MISSING.format( 134 who=self.__class__.__name__ + " with `colors=True`", 135 package="colorama", 136 ) 137 ) 138 139 if force_colors: 140 colorama.deinit() 141 colorama.init(strip=False) 142 else: 143 colorama.init() 144 145 styles = _ColorfulStyles 146 else: 147 styles = _PlainStyles 148 149 self._styles = styles 150 self._pad_event = pad_event 151 152 if level_styles is None: 153 self._level_to_color = self.get_default_level_styles(colors) 154 else: 155 self._level_to_color = level_styles 156 157 for key in self._level_to_color.keys(): 158 self._level_to_color[key] += styles.bright 159 self._longest_level = len( 160 max(self._level_to_color.keys(), key=lambda e: len(e)) 161 ) 162 163 if repr_native_str is True: 164 self._repr = repr 165 else: 166 167 def _repr(inst): 168 if isinstance(inst, str): 169 return inst 170 else: 171 return repr(inst) 172 173 self._repr = _repr 174 175 def __call__(self, _, __, event_dict): 176 sio = StringIO() 177 178 ts = event_dict.pop("timestamp", None) 179 if ts is not None: 180 sio.write( 181 # can be a number if timestamp is UNIXy 182 self._styles.timestamp 183 + str(ts) 184 + self._styles.reset 185 + " " 186 ) 187 level = event_dict.pop("level", None) 188 if level is not None: 189 sio.write( 190 "[" 191 + self._level_to_color[level] 192 + _pad(level, self._longest_level) 193 + self._styles.reset 194 + "] " 195 ) 196 197 event = event_dict.pop("event") 198 if event_dict: 199 event = _pad(event, self._pad_event) + self._styles.reset + " " 200 else: 201 event += self._styles.reset 202 sio.write(self._styles.bright + event) 203 204 logger_name = event_dict.pop("logger", None) 205 if logger_name is not None: 206 sio.write( 207 "[" 208 + self._styles.logger_name 209 + self._styles.bright 210 + logger_name 211 + self._styles.reset 212 + "] " 213 ) 214 215 stack = event_dict.pop("stack", None) 216 exc = event_dict.pop("exception", None) 217 sio.write( 218 " ".join( 219 self._styles.kv_key 220 + key 221 + self._styles.reset 222 + "=" 223 + self._styles.kv_value 224 + self._repr(event_dict[key]) 225 + self._styles.reset 226 for key in sorted(event_dict.keys()) 227 ) 228 ) 229 230 if stack is not None: 231 sio.write("\n" + stack) 232 if exc is not None: 233 sio.write("\n\n" + "=" * 79 + "\n") 234 if exc is not None: 235 sio.write("\n" + exc) 236 237 return sio.getvalue() 238 239 @staticmethod 240 def get_default_level_styles(colors=True): 241 """ 242 Get the default styles for log levels 243 244 This is intended to be used with :class:`ConsoleRenderer`'s 245 ``level_styles`` parameter. For example, if you are adding 246 custom levels in your home-grown 247 :func:`~structlog.stdlib.add_log_level` you could do:: 248 249 my_styles = ConsoleRenderer.get_default_level_styles() 250 my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"] 251 renderer = ConsoleRenderer(level_styles=my_styles) 252 253 :param bool colors: Whether to use colorful styles. This must match the 254 `colors` parameter to :class:`ConsoleRenderer`. Default: True. 255 """ 256 if colors: 257 styles = _ColorfulStyles 258 else: 259 styles = _PlainStyles 260 return { 261 "critical": styles.level_critical, 262 "exception": styles.level_exception, 263 "error": styles.level_error, 264 "warn": styles.level_warn, 265 "warning": styles.level_warn, 266 "info": styles.level_info, 267 "debug": styles.level_debug, 268 "notset": styles.level_notset, 269 } 270