structlog-18.1.0/0000755000076500000240000000000013233032005014057 5ustar hynekstaff00000000000000structlog-18.1.0/PKG-INFO0000644000076500000240000002445413233032005015165 0ustar hynekstaff00000000000000Metadata-Version: 1.1 Name: structlog Version: 18.1.0 Summary: Structured Logging for Python Home-page: http://www.structlog.org/ Author: Hynek Schlawack Author-email: hs@ox.cx License: MIT or Apache License, Version 2.0 Description-Content-Type: UNKNOWN Description: .. image:: http://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: http://www.structlog.org/ ``structlog``: Structured Logging for Python ============================================ .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://structlog.readthedocs.io/en/stable/?badge=stable :alt: Documentation Status .. image:: https://travis-ci.org/hynek/structlog.svg?branch=master :target: https://travis-ci.org/hynek/structlog .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://www.irccloud.com/invite-svg?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 :target: https://www.irccloud.com/invite?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 .. -begin-short- ``structlog`` makes logging in Python less painful and more powerful by adding structure to your log entries. It's up to you whether you want ``structlog`` to take care about the **output** of your log entries or whether you prefer to **forward** them to an existing logging system like the standard library's ``logging`` module. .. -end-short- .. -begin-spiel- Easier Logging -------------- You can stop writing prose and start thinking in terms of an event that happens in the context of key/value pairs: .. code-block:: pycon >>> from structlog import get_logger >>> log = get_logger() >>> log.info("key_value_logging", out_of_the_box=True, effort=0) 2016-04-20 16:20.13 key_value_logging effort=0 out_of_the_box=True Each log entry is a meaningful dictionary instead of an opaque string now! Data Binding ------------ Since log entries are dictionaries, you can start binding and re-binding key/value pairs to your loggers to ensure they are present in every following logging call: .. code-block:: pycon >>> log = log.bind(user="anonymous", some_key=23) >>> log = log.bind(user="hynek", another_key=42) >>> log.info("user.logged_in", happy=True) 2016-04-20 16:20.13 user.logged_in another_key=42 happy=True some_key=23 user='hynek' Powerful Pipelines ------------------ Each log entry goes through a `processor pipeline `_ that is just a chain of functions that receive a dictionary and return a new dictionary that gets fed into the next function. That allows for simple but powerful data manipulation: .. code-block:: python def timestamper(logger, log_method, event_dict): """Add a timestamp to each log entry.""" event_dict["timestamp"] = time.time() return event_dict There are `plenty of processors `_ for most common tasks coming with ``structlog``: - Collectors of `call stack information `_ ("How did this log entry happen?"), - …and `exceptions `_ ("What happened‽"). - Unicode encoders/decoders. - Flexible `timestamping `_. Formatting ---------- ``structlog`` is completely flexible about *how* the resulting log entry is emitted. Since each log entry is a dictionary, it can be formatted to **any** format: - A colorful key/value format for `local development `_, - `JSON `_ for easy parsing, - or some standard format you have parsers for like nginx or Apache httpd. Internally, formatters are processors whose return value (usually a string) is passed into loggers that are responsible for the output of your message. ``structlog`` comes with multiple useful formatters out of-the-box. Output ------ ``structlog`` is also very flexible with the final output of your log entries: - A **built-in** lightweight printer like in the examples above. Easy to use and fast. - Use the **standard library**'s or **Twisted**'s logging modules for compatibility. In this case ``structlog`` works like a wrapper that formats a string and passes them off into existing systems that won't ever know that ``structlog`` even exists. Or the other way round: ``structlog`` comes with a ``logging`` formatter that allows for processing third party log records. - Don't format it to a string at all! ``structlog`` passes you a dictionary and you can do with it whatever you want. Reported uses cases are sending them out via network or saving them in a database. .. -end-spiel- Project Information ------------------- .. -begin-meta- ``structlog`` is dual-licensed under `Apache License, version 2 `_ and `MIT `_, available from `PyPI `_, the source code can be found on `GitHub `_, the documentation at http://www.structlog.org/. ``structlog`` targets Python 2.7, 3.4 and newer, and PyPy. If you need any help, visit us on ``#structlog`` on `Freenode `_! Release Information =================== 18.1.0 (2018-01-27) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - The meaning of the ``structlog[dev]`` installation target will change from "colorful output" to "dependencies to develop ``structlog``" in 19.1.0. The main reason behind this decision is that it's impossible to have a ``structlog`` in your normal dependencies and additionally a ``structlog[dev]`` for developement (``pip`` will report an error). Changes: ^^^^^^^^ - Empty strings are valid events now. `#110 `_ - Do not encapsulate Twisted failures twice with newer versions of Twisted. `#144 `_ - ``structlog.dev.ConsoleRenderer`` now accepts a *force_colors* argument to output colored logs even if the destination is not a tty. Use this option if your logs are stored in files that are intended to be streamed to the console. - ``structlog.dev.ConsoleRenderer`` now accepts a *level_styles* argument for overriding the colors for individual levels, as well as to add new levels. See the docs for ``ConsoleRenderer.get_default_level_styles()`` for usage. `#139 `_ - ``structlog.stdlib.BoundLogger.exception()`` now uses the ``exc_info`` argument if it has been passed instead of setting it unconditionally to ``True``. `#149 `_ - Default configuration now uses plain ``dict``\ s on Python 3.6+ and PyPy since they are ordered by default. - Added ``structlog.is_configured()`` to check whether or not ``structlog`` has been configured. - Added ``structlog.get_config()`` to introspect current configuration. `Full changelog `_. Authors ======= ``structlog`` is written and maintained by `Hynek Schlawack `_. It’s inspired by previous work done by `Jean-Paul Calderone `_ and `David Reid `_. The development is kindly supported by `Variomedia AG `_. A full list of contributors can be found on GitHub’s `overview `_. Some of them disapprove of the addition of thread local context data. :) The ``structlog`` logo has been contributed by `Russell Keith-Magee `_. Keywords: logging,structured,structure,log Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules structlog-18.1.0/LICENSE.apache20000644000076500000240000002367612431374731016422 0ustar hynekstaff00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS structlog-18.1.0/LICENSE.mit0000644000076500000240000000207212635253703015674 0ustar hynekstaff00000000000000The MIT License (MIT) Copyright (c) 2013 Hynek Schlawack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. structlog-18.1.0/conftest.py0000644000076500000240000000130113233027243016261 0ustar hynekstaff00000000000000# -*- coding: utf-8 -*- # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import pytest from six.moves import cStringIO as StringIO @pytest.fixture def sio(): """ A StringIO instance. """ return StringIO() @pytest.fixture def event_dict(): """ An example event dictionary with multiple value types w/o the event itself. """ class A(object): def __repr__(self): return "" return {"a": A(), "b": [3, 4], "x": 7, "y": "test", "z": (1, 2)} structlog-18.1.0/LICENSE0000644000076500000240000000030312432430627015074 0ustar hynekstaff00000000000000This software is made available under the terms of *either* of the licenses found in LICENSE.apache or LICENSE.mit. Contributions to structlog are made under the terms of *both* these licenses. structlog-18.1.0/tests/0000755000076500000240000000000013233032005015221 5ustar hynekstaff00000000000000structlog-18.1.0/tests/test_processors.py0000644000076500000240000003531713233027337021061 0ustar hynekstaff00000000000000# -*- coding: utf-8 -*- # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import datetime import json import sys import pytest import six from freezegun import freeze_time import structlog from structlog.processors import ( ExceptionPrettyPrinter, JSONRenderer, KeyValueRenderer, StackInfoRenderer, TimeStamper, UnicodeDecoder, UnicodeEncoder, _figure_out_exc_info, _json_fallback_handler, format_exc_info ) from structlog.threadlocal import wrap_dict from .utils import py3_only try: import simplejson except ImportError: simplejson = None try: import rapidjson except ImportError: rapidjson = None class TestKeyValueRenderer(object): def test_sort_keys(self, event_dict): """ Keys are sorted if sort_keys is set. """ assert ( r"a= b=[3, 4] x=7 y='test' z=(1, 2)" == KeyValueRenderer(sort_keys=True)(None, None, event_dict) ) def test_order_complete(self, event_dict): """ Orders keys according to key_order. """ assert ( r"y='test' b=[3, 4] a= z=(1, 2) x=7" == KeyValueRenderer(key_order=['y', 'b', 'a', 'z', 'x']) (None, None, event_dict) ) def test_order_missing(self, event_dict): """ Missing keys get rendered as None. """ assert ( r"c=None y='test' b=[3, 4] a= z=(1, 2) x=7" == KeyValueRenderer(key_order=['c', 'y', 'b', 'a', 'z', 'x']) (None, None, event_dict) ) def test_order_missing_dropped(self, event_dict): """ Missing keys get dropped """ assert ( r"y='test' b=[3, 4] a= z=(1, 2) x=7" == KeyValueRenderer(key_order=['c', 'y', 'b', 'a', 'z', 'x'], drop_missing=True) (None, None, event_dict) ) def test_order_extra(self, event_dict): """ Extra keys get sorted if sort_keys=True. """ event_dict['B'] = 'B' event_dict['A'] = 'A' assert ( r"c=None y='test' b=[3, 4] a= z=(1, 2) x=7 A='A' B='B'" == KeyValueRenderer(key_order=['c', 'y', 'b', 'a', 'z', 'x'], sort_keys=True) (None, None, event_dict) ) def test_order_sorted_missing_dropped(self, event_dict): """ Keys get sorted if sort_keys=True and extras get dropped. """ event_dict['B'] = 'B' event_dict['A'] = 'A' assert ( r"y='test' b=[3, 4] a= z=(1, 2) x=7 A='A' B='B'" == KeyValueRenderer(key_order=['c', 'y', 'b', 'a', 'z', 'x'], sort_keys=True, drop_missing=True) (None, None, event_dict) ) def test_random_order(self, event_dict): """ No special ordering doesn't blow up. """ rv = KeyValueRenderer()(None, None, event_dict) assert isinstance(rv, str) @pytest.mark.parametrize("rns", [True, False]) def test_repr_native_str(self, rns): """ repr_native_str=False doesn't repr on native strings. """ rv = KeyValueRenderer(repr_native_str=rns)(None, None, { "event": "哈", "key": 42, "key2": "哈", }) cnt = rv.count("哈") if rns and six.PY2: assert 0 == cnt else: assert 2 == cnt class TestJSONRenderer(object): def test_renders_json(self, event_dict): """ Renders a predictable JSON string. """ assert ( r'{"a": "", "b": [3, 4], "x": 7, "y": "test", "z": ' r'[1, 2]}' == JSONRenderer(sort_keys=True)(None, None, event_dict) ) def test_FallbackEncoder_handles_ThreadLocalDictWrapped_dicts(self): """ Our fallback handling handles properly ThreadLocalDictWrapper values. """ s = json.dumps(wrap_dict(dict)({'a': 42}), default=_json_fallback_handler) assert '{"a": 42}' == s def test_FallbackEncoder_falls_back(self): """ The fallback handler uses repr if it doesn't know the type. """ s = json.dumps({'date': datetime.date(1980, 3, 25)}, default=_json_fallback_handler) assert '{"date": "datetime.date(1980, 3, 25)"}' == s def test_serializer(self): """ A custom serializer is used if specified. """ jr = JSONRenderer(serializer=lambda obj, **kw: {"a": 42}) obj = object() assert {"a": 42} == jr(None, None, obj) @pytest.mark.skipif(simplejson is None, reason="simplejson is missing.") def test_simplejson(self, event_dict): """ Integration test with simplejson. """ jr = JSONRenderer(serializer=simplejson.dumps) assert { 'a': '', 'b': [3, 4], 'x': 7, 'y': 'test', 'z': [1, 2] } == json.loads(jr(None, None, event_dict)) @pytest.mark.skipif(rapidjson is None, reason="python-rapidjson is missing.") def test_rapidjson(self, event_dict): """ Integration test with python-rapidjson. """ jr = JSONRenderer(serializer=rapidjson.dumps) assert { 'a': '', 'b': [3, 4], 'x': 7, 'y': 'test', 'z': [1, 2] } == json.loads(jr(None, None, event_dict)) class TestTimeStamper(object): def test_disallows_non_utc_unix_timestamps(self): """ A asking for a UNIX timestamp with a timezone that's not UTC raises a ValueError. """ with pytest.raises(ValueError) as e: TimeStamper(utc=False) assert "UNIX timestamps are always UTC." == e.value.args[0] def test_inserts_utc_unix_timestamp_by_default(self): """ Per default a float UNIX timestamp is used. """ ts = TimeStamper() d = ts(None, None, {}) # freezegun doesn't work with time.time. :( assert isinstance(d["timestamp"], float) @freeze_time('1980-03-25 16:00:00') def test_local(self): """ Timestamp in local timezone work. We can't add a timezone to the string without additional libraries. """ ts = TimeStamper(fmt="iso", utc=False) d = ts(None, None, {}) assert "1980-03-25T16:00:00" == d["timestamp"] @freeze_time('1980-03-25 16:00:00') def test_formats(self): """ The fmt string is respected. """ ts = TimeStamper(fmt="%Y") d = ts(None, None, {}) assert "1980" == d["timestamp"] @freeze_time('1980-03-25 16:00:00') def test_adds_Z_to_iso(self): """ stdlib's isoformat is buggy, so we fix it. """ ts = TimeStamper(fmt="iso", utc=True) d = ts(None, None, {}) assert "1980-03-25T16:00:00Z" == d["timestamp"] @freeze_time('1980-03-25 16:00:00') def test_key_can_be_specified(self): """ Timestamp is stored with the specified key. """ ts = TimeStamper(fmt="%m", key="month") d = ts(None, None, {}) assert "03" == d["month"] class TestFormatExcInfo(object): def test_formats_tuple(self, monkeypatch): """ If exc_info is a tuple, it is used. """ monkeypatch.setattr(structlog.processors, "_format_exception", lambda exc_info: exc_info) d = format_exc_info(None, None, {"exc_info": (None, None, 42)}) assert {"exception": (None, None, 42)} == d def test_gets_exc_info_on_bool(self): """ If exc_info is True, it is obtained using sys.exc_info(). """ # monkeypatching sys.exc_info makes currently py.test return 1 on # success. try: raise ValueError('test') except ValueError: d = format_exc_info(None, None, {"exc_info": True}) assert "exc_info" not in d assert "raise ValueError('test')\nValueError: test" in d["exception"] @py3_only def test_exception_on_py3(self, monkeypatch): """ Passing excetions as exc_info is valid on Python 3. """ monkeypatch.setattr(structlog.processors, "_format_exception", lambda exc_info: exc_info) try: raise ValueError("test") except ValueError as e: d = format_exc_info(None, None, {"exc_info": e}) assert {"exception": (ValueError, e, e.__traceback__)} == d else: pytest.fail("Exception not raised.") @py3_only def test_exception_without_traceback(self): """ If an Exception is missing a traceback, render it anyway. """ rv = format_exc_info(None, None, { "exc_info": Exception("no traceback!") }) assert {"exception": "Exception: no traceback!"} == rv class TestUnicodeEncoder(object): def test_encodes(self): """ Unicode strings get encoded (as UTF-8 by default). """ ue = UnicodeEncoder() assert {"foo": b"b\xc3\xa4r"} == ue(None, None, {"foo": u"b\xe4r"}) def test_passes_arguments(self): """ Encoding options are passed into the encoding call. """ ue = UnicodeEncoder("latin1", "xmlcharrefreplace") assert {"foo": b"–"} == ue(None, None, {"foo": u"\u2013"}) def test_bytes_nop(self): """ If the string is already bytes, don't do anything. """ ue = UnicodeEncoder() assert {"foo": b"b\xc3\xa4r"} == ue(None, None, {"foo": b"b\xc3\xa4r"}) class TestUnicodeDecoder(object): def test_decodes(self): """ Byte strings get decoded (as UTF-8 by default). """ ud = UnicodeDecoder() assert {"foo": u"b\xe4r"} == ud(None, None, {"foo": b"b\xc3\xa4r"}) def test_passes_arguments(self): """ Encoding options are passed into the encoding call. """ ud = UnicodeDecoder("utf-8", "ignore") assert {"foo": u""} == ud(None, None, {"foo": b"\xa1\xa4"}) def test_bytes_nop(self): """ If the value is already unicode, don't do anything. """ ud = UnicodeDecoder() assert {"foo": u"b\u2013r"} == ud(None, None, {"foo": u"b\u2013r"}) class TestExceptionPrettyPrinter(object): def test_stdout_by_default(self): """ If no file is supplied, use stdout. """ epp = ExceptionPrettyPrinter() assert sys.stdout is epp._file def test_prints_exception(self, sio): """ If there's an `exception` key in the event_dict, just print it out. This happens if `format_exc_info` was run before us in the chain. """ epp = ExceptionPrettyPrinter(file=sio) try: raise ValueError except ValueError: ed = format_exc_info(None, None, {'exc_info': True}) epp(None, None, ed) out = sio.getvalue() assert 'test_prints_exception' in out assert 'raise ValueError' in out def test_removes_exception_after_printing(self, sio): """ After pretty printing `exception` is removed from the event_dict. """ epp = ExceptionPrettyPrinter(sio) try: raise ValueError except ValueError: ed = format_exc_info(None, None, {'exc_info': True}) assert 'exception' in ed new_ed = epp(None, None, ed) assert 'exception' not in new_ed def test_handles_exc_info(self, sio): """ If `exc_info` is passed in, it behaves like `format_exc_info`. """ epp = ExceptionPrettyPrinter(sio) try: raise ValueError except ValueError: epp(None, None, {'exc_info': True}) out = sio.getvalue() assert 'test_handles_exc_info' in out assert 'raise ValueError' in out def test_removes_exc_info_after_printing(self, sio): """ After pretty printing `exception` is removed from the event_dict. """ epp = ExceptionPrettyPrinter(sio) try: raise ValueError except ValueError: ed = epp(None, None, {'exc_info': True}) assert 'exc_info' not in ed def test_nop_if_no_exception(self, sio): """ If there is no exception, don't print anything. """ epp = ExceptionPrettyPrinter(sio) epp(None, None, {}) assert '' == sio.getvalue() def test_own_exc_info(self, sio): """ If exc_info is a tuple, use it. """ epp = ExceptionPrettyPrinter(sio) try: raise ValueError("XXX") except ValueError: ei = sys.exc_info() epp(None, None, {"exc_info": ei}) assert "XXX" in sio.getvalue() @py3_only def test_exception_on_py3(self, sio): """ On Python 3, it's also legal to pass an Exception. """ epp = ExceptionPrettyPrinter(sio) try: raise ValueError("XXX") except ValueError as e: epp(None, None, {"exc_info": e}) assert "XXX" in sio.getvalue() @pytest.fixture def sir(): return StackInfoRenderer() class TestStackInfoRenderer(object): def test_removes_stack_info(self, sir): """ The `stack_info` key is removed from `event_dict`. """ ed = sir(None, None, {'stack_info': True}) assert 'stack_info' not in ed def test_adds_stack_if_asked(self, sir): """ If `stack_info` is true, `stack` is added. """ ed = sir(None, None, {'stack_info': True}) assert 'stack' in ed def test_renders_correct_stack(self, sir): ed = sir(None, None, {'stack_info': True}) assert "ed = sir(None, None, {'stack_info': True})" in ed['stack'] class TestFigureOutExcInfo(object): @pytest.mark.parametrize('true_value', [ True, 1, 1.1 ]) def test_obtains_exc_info_on_True(self, true_value): """ If the passed argument evaluates to True obtain exc_info ourselves. """ try: 0/0 except Exception: assert sys.exc_info() == _figure_out_exc_info(true_value) else: pytest.fail("Exception not raised.") @py3_only def test_py3_exception_no_traceback(self): """ Exceptions without tracebacks are simply returned with None for traceback. """ e = ValueError() assert (e.__class__, e, None) == _figure_out_exc_info(e) structlog-18.1.0/tests/test_utils.py0000644000076500000240000000203513233027243020002 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import errno import pytest from pretend import raiser from structlog._utils import until_not_interrupted class TestUntilNotInterrupted(object): def test_passes_arguments_and_returns_return_value(self): def returner(*args, **kw): return args, kw assert ((42,), {'x': 23}) == until_not_interrupted(returner, 42, x=23) def test_leaves_unrelated_exceptions_through(self): exc = IOError with pytest.raises(exc): until_not_interrupted(raiser(exc('not EINTR'))) def test_retries_on_EINTR(self): calls = [0] def raise_on_first_three(): if calls[0] < 3: calls[0] += 1 raise IOError(errno.EINTR) until_not_interrupted(raise_on_first_three) assert 3 == calls[0] structlog-18.1.0/tests/test_frames.py0000644000076500000240000001143213233027243020120 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import sys import pytest from pretend import stub import structlog._frames from structlog._frames import ( _find_first_app_frame_and_name, _format_exception, _format_stack ) class TestFindFirstAppFrameAndName(object): def test_ignores_structlog_by_default(self, monkeypatch): """ No matter what you pass in, structlog frames get always ignored. """ f1 = stub(f_globals={'__name__': 'test'}, f_back=None) f2 = stub(f_globals={'__name__': 'structlog.blubb'}, f_back=f1) monkeypatch.setattr(structlog._frames.sys, '_getframe', lambda: f2) f, n = _find_first_app_frame_and_name() assert ((f1, 'test') == (f, n)) def test_ignoring_of_additional_frame_names_works(self, monkeypatch): """ Additional names are properly ignored too. """ f1 = stub(f_globals={'__name__': 'test'}, f_back=None) f2 = stub(f_globals={'__name__': 'ignored.bar'}, f_back=f1) f3 = stub(f_globals={'__name__': 'structlog.blubb'}, f_back=f2) monkeypatch.setattr(structlog._frames.sys, '_getframe', lambda: f3) f, n = _find_first_app_frame_and_name(additional_ignores=['ignored']) assert ((f1, 'test') == (f, n)) def test_tolerates_missing_name(self, monkeypatch): """ Use ``?`` if `f_globals` lacks a `__name__` key """ f1 = stub(f_globals={}, f_back=None) monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1) f, n = _find_first_app_frame_and_name() assert ((f1, "?") == (f, n)) def test_tolerates_name_explicitly_None_oneframe(self, monkeypatch): """ Use ``?`` if `f_globals` has a `None` valued `__name__` key """ f1 = stub(f_globals={'__name__': None}, f_back=None) monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1) f, n = _find_first_app_frame_and_name() assert ((f1, "?") == (f, n)) def test_tolerates_name_explicitly_None_manyframe(self, monkeypatch): """ Use ``?`` if `f_globals` has a `None` valued `__name__` key, multiple frames up. """ f1 = stub(f_globals={'__name__': None}, f_back=None) f2 = stub(f_globals={'__name__': 'structlog.blubb'}, f_back=f1) monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f2) f, n = _find_first_app_frame_and_name() assert ((f1, "?") == (f, n)) def test_tolerates_f_back_is_None(self, monkeypatch): """ Use ``?`` if all frames are in ignored frames. """ f1 = stub(f_globals={'__name__': 'structlog'}, f_back=None) monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1) f, n = _find_first_app_frame_and_name() assert ((f1, "?") == (f, n)) @pytest.fixture def exc_info(): """ Fake a valid exc_info. """ try: raise ValueError except ValueError: return sys.exc_info() class TestFormatException(object): def test_returns_str(self, exc_info): """ Always returns a native string. """ assert isinstance(_format_exception(exc_info), str) def test_formats(self, exc_info): """ The passed exc_info is formatted. """ assert _format_exception(exc_info).startswith( "Traceback (most recent call last):\n" ) def test_no_trailing_nl(self, exc_info, monkeypatch): """ Trailing newlines are snipped off but if the string does not contain one nothing is removed. """ from structlog._frames import traceback monkeypatch.setattr( traceback, "print_exception", lambda *a: a[-1].write("foo") ) assert "foo" == _format_exception(exc_info) class TestFormatStack(object): def test_returns_str(self): """ Always returns a native string. """ assert isinstance(_format_stack(sys._getframe()), str) def test_formats(self): """ The passed stack is formatted. """ assert _format_stack(sys._getframe()).startswith( "Stack (most recent call last):\n" ) def test_no_trailing_nl(self, monkeypatch): """ Trailing newlines are snipped off but if the string does not contain one nothing is removed. """ from structlog._frames import traceback monkeypatch.setattr( traceback, "print_stack", lambda frame, file: file.write("foo") ) assert _format_stack(sys._getframe()).endswith("foo") structlog-18.1.0/tests/test_stdlib.py0000644000076500000240000004715513233027243020137 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import collections import logging import logging.config import os import pytest from pretend import call_recorder from structlog import ReturnLogger, configure, get_logger, reset_defaults from structlog.dev import ConsoleRenderer from structlog.exceptions import DropEvent from structlog.processors import JSONRenderer from structlog.stdlib import ( CRITICAL, WARN, BoundLogger, LoggerFactory, PositionalArgumentsFormatter, ProcessorFormatter, _FixedFindCallerLogger, add_log_level, add_logger_name, filter_by_level, render_to_log_kwargs ) from .additional_frame import additional_frame from .utils import py3_only def build_bl(logger=None, processors=None, context=None): """ Convenience function to build BoundLogger with sane defaults. """ return BoundLogger( logger or ReturnLogger(), processors, {} ) def return_method_name(_, method_name, __): """ A final renderer that returns the name of the logging method. """ return method_name class TestLoggerFactory(object): def setup_method(self, method): """ The stdlib logger factory modifies global state to fix caller identification. """ self.original_logger = logging.getLoggerClass() def teardown_method(self, method): logging.setLoggerClass(self.original_logger) def test_deduces_correct_name(self): """ The factory isn't called directly but from structlog._config so deducing has to be slightly smarter. """ assert 'tests.additional_frame' == ( additional_frame(LoggerFactory()).name ) assert 'tests.test_stdlib' == LoggerFactory()().name def test_ignores_frames(self): """ The name guesser walks up the frames until it reaches a frame whose name is not from structlog or one of the configurable other names. """ assert '__main__' == additional_frame(LoggerFactory( ignore_frame_names=["tests.", "_pytest.", "pluggy"]) ).name def test_deduces_correct_caller(self): logger = _FixedFindCallerLogger('test') file_name, line_number, func_name = logger.findCaller()[:3] assert file_name == os.path.realpath(__file__) assert func_name == 'test_deduces_correct_caller' @py3_only def test_stack_info(self): logger = _FixedFindCallerLogger('test') testing, is_, fun, stack_info = logger.findCaller(stack_info=True) assert 'testing, is_, fun' in stack_info @py3_only def test_no_stack_info_by_default(self): logger = _FixedFindCallerLogger('test') testing, is_, fun, stack_info = logger.findCaller() assert None is stack_info def test_find_caller(self, monkeypatch): logger = LoggerFactory()() log_handle = call_recorder(lambda x: None) monkeypatch.setattr(logger, 'handle', log_handle) logger.error('Test') log_record = log_handle.calls[0].args[0] assert log_record.funcName == 'test_find_caller' assert log_record.name == __name__ assert log_record.filename == os.path.basename(__file__) def test_sets_correct_logger(self): assert logging.getLoggerClass() is logging.Logger LoggerFactory() assert logging.getLoggerClass() is _FixedFindCallerLogger def test_positional_argument_avoids_guessing(self): """ If a positional argument is passed to the factory, it's used as the name instead of guessing. """ lf = LoggerFactory()("foo") assert "foo" == lf.name class TestFilterByLevel(object): def test_filters_lower_levels(self): logger = logging.Logger(__name__) logger.setLevel(CRITICAL) with pytest.raises(DropEvent): filter_by_level(logger, 'warn', {}) def test_passes_higher_levels(self): logger = logging.Logger(__name__) logger.setLevel(WARN) event_dict = {'event': 'test'} assert event_dict is filter_by_level(logger, 'warn', event_dict) assert event_dict is filter_by_level(logger, 'error', event_dict) assert event_dict is filter_by_level(logger, 'exception', event_dict) class TestBoundLogger(object): @pytest.mark.parametrize(('method_name'), [ 'debug', 'info', 'warning', 'error', 'critical', ]) def test_proxies_to_correct_method(self, method_name): """ The basic proxied methods are proxied to the correct counterparts. """ bl = BoundLogger(ReturnLogger(), [return_method_name], {}) assert method_name == getattr(bl, method_name)('event') def test_proxies_exception(self): """ BoundLogger.exception is proxied to Logger.error. """ bl = BoundLogger(ReturnLogger(), [return_method_name], {}) assert "error" == bl.exception("event") def test_proxies_log(self): """ BoundLogger.exception.log() is proxied to the apropriate method. """ bl = BoundLogger(ReturnLogger(), [return_method_name], {}) assert "critical" == bl.log(50, "event") assert "debug" == bl.log(10, "event") def test_positional_args_proxied(self): """ Positional arguments supplied are proxied as kwarg. """ bl = BoundLogger(ReturnLogger(), [], {}) args, kwargs = bl.debug('event', 'foo', bar='baz') assert 'baz' == kwargs.get('bar') assert ('foo',) == kwargs.get('positional_args') @pytest.mark.parametrize('method_name,method_args', [ ('addHandler', [None]), ('removeHandler', [None]), ('hasHandlers', None), ('callHandlers', [None]), ('handle', [None]), ('setLevel', [None]), ('getEffectiveLevel', None), ('isEnabledFor', [None]), ('findCaller', None), ('makeRecord', ['name', 'debug', 'test_func', '1', 'test msg', ['foo'], False]), ('getChild', [None]), ]) def test_stdlib_passthrough_methods(self, method_name, method_args): """ stdlib logger methods are also available in stdlib BoundLogger. """ called_stdlib_method = [False] def validate(*args, **kw): called_stdlib_method[0] = True stdlib_logger = logging.getLogger('Test') stdlib_logger_method = getattr(stdlib_logger, method_name, None) if stdlib_logger_method: setattr(stdlib_logger, method_name, validate) bl = BoundLogger(stdlib_logger, [], {}) bound_logger_method = getattr(bl, method_name) assert bound_logger_method is not None if method_args: bound_logger_method(*method_args) else: bound_logger_method() assert called_stdlib_method[0] is True def test_exception_exc_info(self): """ BoundLogger.exception sets exc_info=True. """ bl = BoundLogger(ReturnLogger(), [], {}) assert ( (), {"exc_info": True, "event": "event"} ) == bl.exception("event") def test_exception_exc_info_override(self): """ If *exc_info* is password to exception, it's used. """ bl = BoundLogger(ReturnLogger(), [], {}) assert ( (), {"exc_info": 42, "event": "event"} ) == bl.exception("event", exc_info=42) class TestPositionalArgumentsFormatter(object): def test_formats_tuple(self): """ Positional arguments as simple types are rendered. """ formatter = PositionalArgumentsFormatter() event_dict = formatter(None, None, {'event': '%d %d %s', 'positional_args': (1, 2, 'test')}) assert '1 2 test' == event_dict['event'] assert 'positional_args' not in event_dict def test_formats_dict(self): """ Positional arguments as dict are rendered. """ formatter = PositionalArgumentsFormatter() event_dict = formatter(None, None, {'event': '%(foo)s bar', 'positional_args': ( {'foo': 'bar'},)}) assert 'bar bar' == event_dict['event'] assert 'positional_args' not in event_dict def test_positional_args_retained(self): """ Positional arguments are retained if remove_positional_args argument is set to False. """ formatter = PositionalArgumentsFormatter(remove_positional_args=False) positional_args = (1, 2, 'test') event_dict = formatter( None, None, {'event': '%d %d %s', 'positional_args': positional_args}) assert 'positional_args' in event_dict assert positional_args == event_dict['positional_args'] def test_nop_no_args(self): """ If no positional args are passed, nothing happens. """ formatter = PositionalArgumentsFormatter() assert {} == formatter(None, None, {}) def test_args_removed_if_empty(self): """ If remove_positional_args is True and positional_args is (), still remove them. Regression test for https://github.com/hynek/structlog/issues/82. """ formatter = PositionalArgumentsFormatter() assert {} == formatter(None, None, {"positional_args": ()}) class TestAddLogLevel(object): def test_log_level_added(self): """ The log level is added to the event dict. """ event_dict = add_log_level(None, 'error', {}) assert 'error' == event_dict['level'] def test_log_level_alias_normalized(self): """ The normalized name of the log level is added to the event dict. """ event_dict = add_log_level(None, 'warn', {}) assert 'warning' == event_dict['level'] @pytest.fixture def log_record(): """ A LogRecord factory. """ def create_log_record(**kwargs): defaults = { "name": "sample-name", "level": logging.INFO, "pathname": None, "lineno": None, "msg": "sample-message", "args": [], "exc_info": None, } defaults.update(kwargs) return logging.LogRecord(**defaults) return create_log_record class TestAddLoggerName(object): def test_logger_name_added(self): """ The logger name is added to the event dict. """ name = "sample-name" logger = logging.getLogger(name) event_dict = add_logger_name(logger, None, {}) assert name == event_dict["logger"] def test_logger_name_added_with_record(self, log_record): """ The logger name is deduced from the LogRecord if provided. """ name = "sample-name" record = log_record(name=name) event_dict = add_logger_name(None, None, {"_record": record}) assert name == event_dict["logger"] class TestRenderToLogKW(object): def test_default(self): """ Translates `event` to `msg` and handles otherwise empty `event_dict`s. """ d = render_to_log_kwargs(None, None, {"event": "message"}) assert {"msg": "message", "extra": {}} == d def test_add_extra_event_dict(self, event_dict): """ Adds all remaining data from `event_dict` into `extra`. """ event_dict["event"] = "message" d = render_to_log_kwargs(None, None, event_dict) assert {"msg": "message", "extra": event_dict} == d @pytest.fixture def configure_for_pf(): """ Configure structlog to use ProcessorFormatter. Reset both structlog and logging setting after the test. """ configure( processors=[ add_log_level, ProcessorFormatter.wrap_for_formatter, ], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) yield logging.basicConfig() reset_defaults() def configure_logging(pre_chain): """ Configure logging to use ProcessorFormatter. """ return logging.config.dictConfig({ "version": 1, "disable_existing_loggers": False, "formatters": { "plain": { "()": ProcessorFormatter, "processor": ConsoleRenderer(colors=False), "foreign_pre_chain": pre_chain, "format": "%(message)s [in %(funcName)s]" } }, "handlers": { "default": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "plain", }, }, "loggers": { "": { "handlers": ["default"], "level": "DEBUG", "propagate": True, }, } }) class TestProcessorFormatter(object): """ These are all integration tests because they're all about integration. """ def test_foreign_delegate(self, configure_for_pf, capsys): """ If foreign_pre_chain is None, non-structlog log entries are delegated to logging. """ configure_logging(None) configure( processors=[ ProcessorFormatter.wrap_for_formatter, ], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) logging.getLogger().warning("foo") assert ( "", "foo [in test_foreign_delegate]\n", ) == capsys.readouterr() def test_clears_args(self, capsys, configure_for_pf): """ We render our log records before sending it back to logging. Therefore we must clear `LogRecord.args` otherwise the user gets an `TypeError: not all arguments converted during string formatting.` if they use positional formatting in stdlib logging. """ configure_logging(None) logging.getLogger().warning("hello %s.", "world") assert ( "", "hello world. [in test_clears_args]\n", ) == capsys.readouterr() def test_foreign_pre_chain(self, configure_for_pf, capsys): """ If foreign_pre_chain is an iterable, it's used to pre-process non-structlog log entries. """ configure_logging((add_log_level,)) configure( processors=[ ProcessorFormatter.wrap_for_formatter, ], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) logging.getLogger().warning("foo") assert ( "", "[warning ] foo [in test_foreign_pre_chain]\n", ) == capsys.readouterr() def test_foreign_pre_chain_add_logger_name(self, configure_for_pf, capsys): """ foreign_pre_chain works with add_logger_name processor. """ configure_logging((add_logger_name,)) configure( processors=[ ProcessorFormatter.wrap_for_formatter, ], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) logging.getLogger("sample-name").warning("foo") assert ( "", "foo [sample-name] [in test_foreign_pr" "e_chain_add_logger_name]\n", ) == capsys.readouterr() def test_foreign_pre_chain_gets_exc_info(self, configure_for_pf, capsys): """ If non-structlog record contains exc_info, foreign_pre_chain functions have access to it. """ test_processor = call_recorder(lambda l, m, event_dict: event_dict) configure_logging((test_processor,)) configure( processors=[ ProcessorFormatter.wrap_for_formatter, ], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) try: raise RuntimeError("oh noo") except Exception: logging.getLogger().exception("okay") event_dict = test_processor.calls[0].args[2] assert "exc_info" in event_dict assert isinstance(event_dict["exc_info"], tuple) def test_other_handlers_get_original_record(self, configure_for_pf, capsys): """ Logging handlers that come after the handler with ProcessorFormatter should receive original, unmodified record. """ configure_logging(None) handler1 = logging.StreamHandler() handler1.setFormatter(ProcessorFormatter(JSONRenderer())) handler2 = type("", (), {})() handler2.handle = call_recorder(lambda record: None) handler2.level = logging.INFO logger = logging.getLogger() logger.addHandler(handler1) logger.addHandler(handler2) logger.info("meh") assert 1 == len(handler2.handle.calls) handler2_record = handler2.handle.calls[0].args[0] assert "meh" == handler2_record.msg @pytest.mark.parametrize("keep", [True, False]) def test_formatter_unsets_exc_info(self, configure_for_pf, capsys, keep): """ Stack traces doesn't get printed outside of the json document when keep_exc_info are set to False but preserved if set to True. """ configure_logging(None) logger = logging.getLogger() def format_exc_info_fake(logger, name, event_dict): event_dict = collections.OrderedDict(event_dict) del event_dict["exc_info"] event_dict["exception"] = "Exception!" return event_dict formatter = ProcessorFormatter( processor=JSONRenderer(), keep_stack_info=keep, keep_exc_info=keep, foreign_pre_chain=[format_exc_info_fake], ) logger.handlers[0].setFormatter(formatter) try: raise RuntimeError("oh noo") except Exception: logging.getLogger().exception("seen worse") out, err = capsys.readouterr() assert "" == out if keep is False: assert ( '{"event": "seen worse", "exception": "Exception!"}\n' ) == err else: assert "Traceback (most recent call last):" in err @pytest.mark.parametrize("keep", [True, False]) @py3_only def test_formatter_unsets_stack_info(self, configure_for_pf, capsys, keep): """ Stack traces doesn't get printed outside of the json document when keep_stack_info are set to False but preserved if set to True. """ configure_logging(None) logger = logging.getLogger() formatter = ProcessorFormatter( processor=JSONRenderer(), keep_stack_info=keep, keep_exc_info=keep, foreign_pre_chain=[], ) logger.handlers[0].setFormatter(formatter) logging.getLogger().warning("have a stack trace", stack_info=True) out, err = capsys.readouterr() assert "" == out if keep is False: assert 1 == err.count("Stack (most recent call last):") else: assert 2 == err.count("Stack (most recent call last):") def test_native(self, configure_for_pf, capsys): """ If the log entry comes from structlog, it's unpackaged and processed. """ configure_logging(None) get_logger().warning("foo") assert ( "", "[warning ] foo [in test_native]\n", ) == capsys.readouterr() structlog-18.1.0/tests/__init__.py0000644000076500000240000000026513233027243017345 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. structlog-18.1.0/tests/test_loggers.py0000644000076500000240000000705513233027243020313 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import sys import pytest from six.moves import cStringIO as StringIO from structlog._loggers import ( WRITE_LOCKS, PrintLogger, PrintLoggerFactory, ReturnLogger, ReturnLoggerFactory ) from structlog.stdlib import _NAME_TO_LEVEL def test_return_logger(): obj = ['hello'] assert obj is ReturnLogger().msg(obj) STDLIB_MSG_METHODS = [m for m in _NAME_TO_LEVEL if m != 'notset'] class TestPrintLogger(object): def test_prints_to_stdout_by_default(self, capsys): """ Instantiating without arguments gives conveniently a logger to standard out. """ PrintLogger().msg("hello") out, err = capsys.readouterr() assert 'hello\n' == out assert '' == err def test_prints_to_correct_file(self, tmpdir, capsys): """ Supplied files are respected. """ f = tmpdir.join("test.log") fo = f.open("w") PrintLogger(fo).msg("hello") out, err = capsys.readouterr() assert "" == out == err fo.close() assert "hello\n" == f.read() def test_repr(self): """ __repr__ makes sense. """ assert repr(PrintLogger()).startswith( "") @pytest.mark.skipif(greenlet is None, reason="Needs greenlet.") def test_is_greenlet_local(self, D): """ Context is shared between greenlets. """ d = wrap_dict(dict)() d['switch'] = 42 def run(): assert 'x' not in d._dict d['switch'] = 23 greenlet.greenlet(run).switch() assert 42 == d._dict["switch"] def test_delattr(self, D): """ ___delattr__ is proxied to the wrapped class. """ d = D() d['delattr'] = 42 assert 42 == d._dict["delattr"] del d.__class__._tl.dict_ def test_delattr_missing(self, D): """ __delattr__ on an inexisting attribute raises AttributeError. """ d = D() with pytest.raises(AttributeError) as e: d._tl.__delattr__("does_not_exist") assert "does_not_exist" == e.value.args[0] def test_del(self, D): """ ___del__ is proxied to the wrapped class. """ d = D() d["del"] = 13 assert 13 == d._dict["del"] del d["del"] assert "del" not in d._dict def test_new_class(self, D): """ The context of a new wrapped class is empty. """ assert 0 == len(D()) structlog-18.1.0/tests/test_generic.py0000644000076500000240000000243513233027243020262 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function from structlog._config import _CONFIG from structlog._generic import BoundLogger from structlog._loggers import ReturnLogger class TestLogger(object): def log(self, msg): return 'log', msg def gol(self, msg): return 'gol', msg class TestGenericBoundLogger(object): def test_caches(self): """ __getattr__() gets called only once per logger method. """ b = BoundLogger( ReturnLogger(), _CONFIG.default_processors, _CONFIG.default_context_class(), ) assert 'msg' not in b.__dict__ b.msg('foo') assert 'msg' in b.__dict__ def test_proxies_anything(self): """ Anything that isn't part of BoundLoggerBase gets proxied to the correct wrapped logger methods. """ b = BoundLogger( ReturnLogger(), _CONFIG.default_processors, _CONFIG.default_context_class(), ) assert 'log', 'foo' == b.log('foo') assert 'gol', 'bar' == b.gol('bar') structlog-18.1.0/tests/test_dev.py0000644000076500000240000002037113233027243017423 0ustar hynekstaff00000000000000# -*- coding: utf-8 -*- # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import pytest import six from structlog import dev class TestPad(object): def test_normal(self): """ If chars are missing, adequate number of " " are added. """ assert 100 == len(dev._pad("test", 100)) def test_negative(self): """ If string is already too long, don't do anything. """ assert len("test") == len(dev._pad("test", 2)) @pytest.fixture def cr(): return dev.ConsoleRenderer(colors=dev._has_colorama) @pytest.fixture def styles(cr): return cr._styles @pytest.fixture def padded(styles): return ( styles.bright + dev._pad("test", dev._EVENT_WIDTH) + styles.reset + " " ) @pytest.fixture def unpadded(styles): return styles.bright + "test" + styles.reset class TestConsoleRenderer(object): @pytest.mark.skipif(dev._has_colorama, reason="Colorama must be missing.") def test_missing_colorama(self): """ ConsoleRenderer(colors=True) raises SystemError on initialization if colorama is missing. """ with pytest.raises(SystemError) as e: dev.ConsoleRenderer() assert ( "ConsoleRenderer with `colors=True` requires the colorama package " "installed." ) in e.value.args[0] def test_plain(self, cr, styles, unpadded): """ Works with a plain event_dict with only the event. """ rv = cr(None, None, {"event": "test"}) assert unpadded == rv def test_timestamp(self, cr, styles, unpadded): """ Timestamps get prepended. """ rv = cr(None, None, {"event": "test", "timestamp": 42}) assert ( styles.timestamp + "42" + styles.reset + " " + unpadded ) == rv def test_level(self, cr, styles, padded): """ Levels are rendered aligned, in square brackets, and color coded. """ rv = cr(None, None, { "event": "test", "level": "critical", "foo": "bar" }) assert ( "[" + dev.RED + styles.bright + dev._pad("critical", cr._longest_level) + styles.reset + "] " + padded + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset ) == rv def test_init_accepts_overriding_levels(self, styles, padded): """ Stdlib levels are rendered aligned, in brackets, and color coded. """ my_styles = dev.ConsoleRenderer.get_default_level_styles( colors=dev._has_colorama ) my_styles["MY_OH_MY"] = my_styles["critical"] cr = dev.ConsoleRenderer( colors=dev._has_colorama, level_styles=my_styles ) # this would blow up if the level_styles override failed rv = cr(None, None, { "event": "test", "level": "MY_OH_MY", "foo": "bar" }) assert ( "[" + dev.RED + styles.bright + dev._pad("MY_OH_MY", cr._longest_level) + styles.reset + "] " + padded + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset ) == rv def test_logger_name(self, cr, styles, padded): """ Logger names are appended after the event. """ rv = cr(None, None, {"event": "test", "logger": "some_module"}) assert ( padded + "[" + dev.BLUE + styles.bright + "some_module" + styles.reset + "] " ) == rv def test_key_values(self, cr, styles, padded): """ Key-value pairs go sorted alphabetically to the end. """ rv = cr(None, None, { "event": "test", "key": "value", "foo": "bar", }) assert ( padded + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset + " " + styles.kv_key + "key" + styles.reset + "=" + styles.kv_value + "value" + styles.reset ) == rv def test_exception(self, cr, padded): """ Exceptions are rendered after a new line. """ exc = "Traceback:\nFake traceback...\nFakeError: yolo" rv = cr(None, None, { "event": "test", "exception": exc }) assert ( padded + "\n" + exc ) == rv def test_stack_info(self, cr, padded): """ Stack traces are rendered after a new line. """ stack = "fake stack" rv = cr(None, None, { "event": "test", "stack": stack }) assert ( padded + "\n" + stack ) == rv def test_pad_event_param(self, styles): """ `pad_event` parameter works. """ rv = dev.ConsoleRenderer(42, dev._has_colorama)(None, None, { "event": "test", "foo": "bar" }) assert ( styles.bright + dev._pad("test", 42) + styles.reset + " " + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset ) == rv def test_everything(self, cr, styles, padded): """ Put all cases together. """ exc = "Traceback:\nFake traceback...\nFakeError: yolo" stack = "fake stack trace" rv = cr(None, None, { "event": "test", "exception": exc, "key": "value", "foo": "bar", "timestamp": "13:13", "logger": "some_module", "level": "error", "stack": stack, }) assert ( styles.timestamp + "13:13" + styles.reset + " [" + styles.level_error + styles.bright + dev._pad("error", cr._longest_level) + styles.reset + "] " + padded + "[" + dev.BLUE + styles.bright + "some_module" + styles.reset + "] " + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset + " " + styles.kv_key + "key" + styles.reset + "=" + styles.kv_value + "value" + styles.reset + "\n" + stack + "\n\n" + "=" * 79 + "\n" + "\n" + exc ) == rv def test_colorama_colors_false(self): """ If colors is False, don't use colors or styles ever. """ plain_cr = dev.ConsoleRenderer(colors=False) rv = plain_cr(None, None, { "event": "event", "level": "info", "foo": "bar" }) assert dev._PlainStyles is plain_cr._styles assert "[info ] event foo=bar" == rv def test_colorama_force_colors(self, styles, padded): """ If force_colors is True, use colors even if the destination is non-tty. """ cr = dev.ConsoleRenderer( colors=dev._has_colorama, force_colors=dev._has_colorama) rv = cr(None, None, { "event": "test", "level": "critical", "foo": "bar" }) assert ( "[" + dev.RED + styles.bright + dev._pad("critical", cr._longest_level) + styles.reset + "] " + padded + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset ) == rv assert not dev._has_colorama or dev._ColorfulStyles is cr._styles @pytest.mark.parametrize("rns", [True, False]) def test_repr_native_str(self, rns): """ repr_native_str=False doesn't repr on native strings. "event" is never repr'ed. """ rv = dev.ConsoleRenderer( colors=False, repr_native_str=rns)(None, None, { "event": "哈", "key": 42, "key2": "哈", } ) cnt = rv.count("哈") if rns and six.PY2: assert 1 == cnt else: assert 2 == cnt structlog-18.1.0/tests/test_twisted.py0000644000076500000240000002456213233027243020336 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import json from collections import OrderedDict import pytest from pretend import call_recorder from six import PY3 from six.moves import cStringIO as StringIO from structlog import ReturnLogger from structlog._config import _CONFIG from structlog.processors import KeyValueRenderer from structlog.twisted import ( BoundLogger, EventAdapter, JSONLogObserverWrapper, JSONRenderer, LoggerFactory, PlainFileLogObserver, ReprWrapper, _extractStuffAndWhy, plainJSONStdOutLogger ) from twisted.python.failure import Failure, NoCurrentExceptionError from twisted.python.log import ILogObserver def test_LoggerFactory(): from twisted.python import log assert log is LoggerFactory()() def _render_repr(_, __, event_dict): return repr(event_dict) def build_bl(logger=None, processors=None, context=None): """ Convenience function to build BoundLoggerses with sane defaults. """ return BoundLogger( logger or ReturnLogger(), processors or [KeyValueRenderer()], context if context is not None else _CONFIG.default_context_class(), ) class TestBoundLogger(object): def test_msg(self): """ log.msg renders correctly. """ bl = build_bl() assert "foo=42 event='event'" == bl.msg("event", foo=42) def test_errVanilla(self): """ log.err renders correctly if no failure is attached. """ bl = build_bl() assert "foo=42 event='event'" == bl.err("event", foo=42) def test_errWithFailure(self): """ Failures are correctly injected into the log entries. """ bl = build_bl(processors=[ EventAdapter(dictRenderer=KeyValueRenderer()) ]) try: raise ValueError except ValueError: # Use str() for comparison to avoid tricky # deep-compares of Failures. assert str(((), { '_stuff': Failure(ValueError()), '_why': "foo=42 event='event'", })) == str(bl.err('event', foo=42)) class TestExtractStuffAndWhy(object): def test_extractFailsOnTwoFailures(self): """ Raise ValueError if both _stuff and event contain exceptions. """ with pytest.raises(ValueError) as e: _extractStuffAndWhy({'_stuff': Failure(ValueError()), 'event': Failure(TypeError())}) assert ( "Both _stuff and event contain an Exception/Failure." == e.value.args[0] ) def test_failsOnConflictingEventAnd_why(self): """ Raise ValueError if both _why and event are in the event_dict. """ with pytest.raises(ValueError) as e: _extractStuffAndWhy({'_why': 'foo', 'event': 'bar'}) assert ( "Both `_why` and `event` supplied." == e.value.args[0] ) def test_handlesFailures(self): """ Extracts failures and events. """ f = Failure(ValueError()) assert ( ({'value': f}, "foo", {}) == _extractStuffAndWhy({"_why": "foo", "_stuff": {'value': f}}) ) assert ( ({'value': f}, None, {}) == _extractStuffAndWhy({"_stuff": {'value': f}}) ) def test_handlesMissingFailure(self): """ Missing failures extract a None. """ assert ( (None, "foo", {}) == _extractStuffAndWhy({"event": "foo"}) ) @pytest.mark.xfail(PY3, reason="Py3 does not allow for cleaning exc_info") def test_recognizesErrorsAndCleansThem(self): """ If no error is supplied, the environment is checked for one. If one is found, it's used and cleared afterwards so log.err doesn't add it as well. """ try: raise ValueError except ValueError: f = Failure() _stuff, _why, ed = _extractStuffAndWhy({'event': 'foo'}) assert _stuff.value is f.value with pytest.raises(NoCurrentExceptionError): Failure() class TestEventAdapter(object): """ Some tests here are redundant because they predate _extractStuffAndWhy. """ def test_EventAdapterFormatsLog(self): la = EventAdapter(_render_repr) assert "{'foo': 'bar'}" == la(None, 'msg', {'foo': 'bar'}) def test_transforms_whyIntoEvent(self): """ log.err(_stuff=exc, _why='foo') makes the output 'event="foo"' """ la = EventAdapter(_render_repr) error = ValueError('test') rv = la(None, 'err', { '_stuff': error, '_why': 'foo', 'event': None, }) assert () == rv[0] assert isinstance(rv[1]['_stuff'], Failure) assert error == rv[1]['_stuff'].value assert "{'event': 'foo'}" == rv[1]['_why'] def test_worksUsualCase(self): """ log.err(exc, _why='foo') makes the output 'event="foo"' """ la = EventAdapter(_render_repr) error = ValueError('test') rv = la(None, 'err', {'event': error, '_why': 'foo'}) assert () == rv[0] assert isinstance(rv[1]['_stuff'], Failure) assert error == rv[1]['_stuff'].value assert "{'event': 'foo'}" == rv[1]['_why'] def test_allKeywords(self): """ log.err(_stuff=exc, _why='event') """ la = EventAdapter(_render_repr) error = ValueError('test') rv = la(None, 'err', {'_stuff': error, '_why': 'foo'}) assert () == rv[0] assert isinstance(rv[1]['_stuff'], Failure) assert error == rv[1]['_stuff'].value assert "{'event': 'foo'}" == rv[1]['_why'] def test_noFailure(self): """ log.err('event') """ la = EventAdapter(_render_repr) assert ((), { '_stuff': None, '_why': "{'event': 'someEvent'}", }) == la(None, 'err', { 'event': 'someEvent' }) def test_noFailureWithKeyword(self): """ log.err(_why='event') """ la = EventAdapter(_render_repr) assert ((), { '_stuff': None, '_why': "{'event': 'someEvent'}", }) == la(None, 'err', { '_why': 'someEvent' }) def test_catchesConflictingEventAnd_why(self): la = EventAdapter(_render_repr) with pytest.raises(ValueError) as e: la(None, 'err', { 'event': 'someEvent', '_why': 'someReason', }) assert 'Both `_why` and `event` supplied.' == e.value.args[0] @pytest.fixture def jr(): """ A plain Twisted JSONRenderer. """ return JSONRenderer() class TestJSONRenderer(object): def test_dumpsKWsAreHandedThrough(self, jr): """ JSONRenderer allows for setting arguments that are passed to json.dumps(). Make sure they are passed. """ d = OrderedDict(x='foo') d.update(a='bar') jr_sorted = JSONRenderer(sort_keys=True) assert jr_sorted(None, 'err', d) != jr(None, 'err', d) def test_handlesMissingFailure(self, jr): """ Calling err without an actual failure works and returns the event as a string wrapped in ReprWrapper. """ assert ReprWrapper( '{"event": "foo"}' ) == jr(None, "err", {"event": "foo"})[0][0] assert ReprWrapper( '{"event": "foo"}' ) == jr(None, "err", {"_why": "foo"})[0][0] def test_msgWorksToo(self, jr): """ msg renders the event as a string and wraps it using ReprWrapper. """ assert ReprWrapper( '{"event": "foo"}' ) == jr(None, 'msg', {'_why': 'foo'})[0][0] def test_handlesFailure(self, jr): rv = jr(None, 'err', {'event': Failure(ValueError())})[0][0].string assert 'Failure: {0}.ValueError'.format("builtins" if PY3 else "exceptions") in rv assert '"event": "error"' in rv def test_setsStructLogField(self, jr): """ Formatted entries are marked so they can be identified without guessing for example in JSONLogObserverWrapper. """ assert {'_structlog': True} == jr(None, 'msg', {'_why': 'foo'})[1] class TestReprWrapper(object): def test_repr(self): """ The repr of the wrapped string is the vanilla string without quotes. """ assert "foo" == repr(ReprWrapper("foo")) class TestPlainFileLogObserver(object): def test_isLogObserver(self): assert ILogObserver.providedBy(PlainFileLogObserver(StringIO())) def test_writesOnlyMessageWithLF(self): sio = StringIO() PlainFileLogObserver(sio)({'system': 'some system', 'message': ('hello',)}) assert 'hello\n' == sio.getvalue() class TestJSONObserverWrapper(object): def test_IsAnObserver(self): assert ILogObserver.implementedBy(JSONLogObserverWrapper) def test_callsWrappedObserver(self): """ The wrapper always runs the wrapped observer in the end. """ o = call_recorder(lambda *a, **kw: None) JSONLogObserverWrapper(o)({'message': ('hello',)}) assert 1 == len(o.calls) def test_jsonifiesPlainLogEntries(self): """ Entries that aren't formatted by JSONRenderer are rendered as JSON now. """ o = call_recorder(lambda *a, **kw: None) JSONLogObserverWrapper(o)({'message': ('hello',), 'system': '-'}) msg = json.loads(o.calls[0].args[0]['message'][0]) assert msg == {'event': 'hello', 'system': '-'} def test_leavesStructLogAlone(self): """ Entries that are formatted by JSONRenderer are left alone. """ d = {'message': ('hello',), '_structlog': True} def verify(eventDict): assert d == eventDict JSONLogObserverWrapper(verify)(d) class TestPlainJSONStdOutLogger(object): def test_isLogObserver(self): assert ILogObserver.providedBy(plainJSONStdOutLogger()) structlog-18.1.0/tests/utils.py0000644000076500000240000000067113233027243016747 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Shared test utilities. """ from __future__ import absolute_import, division, print_function import pytest import six py3_only = pytest.mark.skipif(not six.PY3, reason="Python 3-only") py2_only = pytest.mark.skipif(not six.PY2, reason="Python 2-only") structlog-18.1.0/tests/test_config.py0000644000076500000240000002452313233027243020115 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import platform import sys import warnings from collections import OrderedDict import pytest from pretend import call, call_recorder, stub from six import PY3 import structlog from structlog._base import BoundLoggerBase from structlog._config import ( _BUILTIN_DEFAULT_CONTEXT_CLASS, _BUILTIN_DEFAULT_LOGGER_FACTORY, _BUILTIN_DEFAULT_PROCESSORS, _BUILTIN_DEFAULT_WRAPPER_CLASS, _CONFIG, BoundLoggerLazyProxy, configure, configure_once, get_logger, wrap_logger ) @pytest.fixture def proxy(): """ Returns a BoundLoggerLazyProxy constructed w/o paramaters & None as logger. """ return BoundLoggerLazyProxy(None) class Wrapper(BoundLoggerBase): """ Custom wrapper class for testing. """ def test_default_context_class(): """ Default context class is dict on Python 3.6+ and PyPy, OrderedDict otherwise. """ if ( platform.python_implementation() == "PyPy" or sys.version_info[:2] >= (3, 6) ): cls = dict else: cls = OrderedDict assert cls is _BUILTIN_DEFAULT_CONTEXT_CLASS class TestConfigure(object): def teardown_method(self, method): structlog.reset_defaults() def test_get_config_is_configured(self): """ Return value of structlog.get_config() works as input for structlog.configure(). is_configured() reflects the state of configuration. """ assert False is structlog.is_configured() structlog.configure(**structlog.get_config()) assert True is structlog.is_configured() structlog.reset_defaults() assert False is structlog.is_configured() def test_configure_all(self, proxy): x = stub() configure(processors=[x], context_class=dict) b = proxy.bind() assert [x] == b._processors assert dict is b._context.__class__ def test_reset(self, proxy): x = stub() configure(processors=[x], context_class=dict, wrapper_class=Wrapper) structlog.reset_defaults() b = proxy.bind() assert [x] != b._processors assert _BUILTIN_DEFAULT_PROCESSORS == b._processors assert isinstance(b, _BUILTIN_DEFAULT_WRAPPER_CLASS) assert _BUILTIN_DEFAULT_CONTEXT_CLASS == b._context.__class__ assert _BUILTIN_DEFAULT_LOGGER_FACTORY is _CONFIG.logger_factory def test_just_processors(self, proxy): x = stub() configure(processors=[x]) b = proxy.bind() assert [x] == b._processors assert _BUILTIN_DEFAULT_PROCESSORS != b._processors assert _BUILTIN_DEFAULT_CONTEXT_CLASS == b._context.__class__ def test_just_context_class(self, proxy): configure(context_class=dict) b = proxy.bind() assert dict is b._context.__class__ assert _BUILTIN_DEFAULT_PROCESSORS == b._processors def test_configure_sets_is_configured(self): assert False is _CONFIG.is_configured configure() assert True is _CONFIG.is_configured def test_configures_logger_factory(self): def f(): pass configure(logger_factory=f) assert f is _CONFIG.logger_factory class TestBoundLoggerLazyProxy(object): def teardown_method(self, method): structlog.reset_defaults() def test_repr(self): p = BoundLoggerLazyProxy( None, processors=[1, 2, 3], context_class=dict, initial_values={'foo': 42}, logger_factory_args=(4, 5), ) assert ( ", " "initial_values={'foo': 42}, " "logger_factory_args=(4, 5))>" % ('class' if PY3 else 'type',) ) == repr(p) def test_returns_bound_logger_on_bind(self, proxy): assert isinstance(proxy.bind(), BoundLoggerBase) def test_returns_bound_logger_on_new(self, proxy): assert isinstance(proxy.new(), BoundLoggerBase) def test_prefers_args_over_config(self): p = BoundLoggerLazyProxy(None, processors=[1, 2, 3], context_class=dict) b = p.bind() assert isinstance(b._context, dict) assert [1, 2, 3] == b._processors class Class(object): def __init__(self, *args, **kw): pass def update(self, *args, **kw): pass configure(processors=[4, 5, 6], context_class=Class) b = p.bind() assert not isinstance(b._context, Class) assert [1, 2, 3] == b._processors def test_falls_back_to_config(self, proxy): b = proxy.bind() assert isinstance(b._context, _CONFIG.default_context_class) assert _CONFIG.default_processors == b._processors def test_bind_honors_initial_values(self): p = BoundLoggerLazyProxy(None, initial_values={'a': 1, 'b': 2}) b = p.bind() assert {'a': 1, 'b': 2} == b._context b = p.bind(c=3) assert {'a': 1, 'b': 2, 'c': 3} == b._context def test_bind_binds_new_values(self, proxy): b = proxy.bind(c=3) assert {'c': 3} == b._context def test_unbind_unbinds_from_initial_values(self): p = BoundLoggerLazyProxy(None, initial_values={'a': 1, 'b': 2}) b = p.unbind('a') assert {'b': 2} == b._context def test_honors_wrapper_class(self): p = BoundLoggerLazyProxy(None, wrapper_class=Wrapper) b = p.bind() assert isinstance(b, Wrapper) def test_honors_wrapper_from_config(self, proxy): configure(wrapper_class=Wrapper) b = proxy.bind() assert isinstance(b, Wrapper) def test_new_binds_only_initial_values_impolicit_ctx_class(self, proxy): proxy = BoundLoggerLazyProxy(None, initial_values={'a': 1, 'b': 2}) b = proxy.new(foo=42) assert {'a': 1, 'b': 2, 'foo': 42} == b._context def test_new_binds_only_initial_values_explicit_ctx_class(self, proxy): proxy = BoundLoggerLazyProxy(None, initial_values={'a': 1, 'b': 2}, context_class=dict) b = proxy.new(foo=42) assert {'a': 1, 'b': 2, 'foo': 42} == b._context def test_rebinds_bind_method(self, proxy): """ To save time, be rebind the bind method once the logger has been cached. """ configure(cache_logger_on_first_use=True) bind = proxy.bind proxy.bind() assert bind != proxy.bind def test_does_not_cache_by_default(self, proxy): """ Proxy's bind method doesn't change by default. """ bind = proxy.bind proxy.bind() assert bind == proxy.bind def test_argument_takes_precedence_over_configuration(self): configure(cache_logger_on_first_use=True) proxy = BoundLoggerLazyProxy(None, cache_logger_on_first_use=False) bind = proxy.bind proxy.bind() assert bind == proxy.bind def test_argument_takes_precedence_over_configuration2(self): configure(cache_logger_on_first_use=False) proxy = BoundLoggerLazyProxy(None, cache_logger_on_first_use=True) bind = proxy.bind proxy.bind() assert bind != proxy.bind def test_bind_doesnt_cache_logger(self): """ Calling configure() changes BoundLoggerLazyProxys immediately. Previous uses of the BoundLoggerLazyProxy don't interfere. """ class F(object): "New logger factory with a new attribute" def a(self, *args): return 5 proxy = BoundLoggerLazyProxy(None) proxy.bind() configure(logger_factory=F) new_b = proxy.bind() assert new_b.a("test") == 5 def test_emphemeral(self): """ Calling an unknown method proxy creates a new wrapped bound logger first. """ class Foo(BoundLoggerBase): def foo(self): return 42 proxy = BoundLoggerLazyProxy( None, wrapper_class=Foo, cache_logger_on_first_use=False, ) assert 42 == proxy.foo() class TestFunctions(object): def teardown_method(self, method): structlog.reset_defaults() def test_wrap_passes_args(self): logger = object() p = wrap_logger(logger, processors=[1, 2, 3], context_class=dict) assert logger is p._logger assert [1, 2, 3] == p._processors assert dict is p._context_class def test_empty_processors(self): """ An empty list is a valid value for processors so it must be preserved. """ # We need to do a bind such that we get an actual logger and not just # a lazy proxy. logger = wrap_logger(object(), processors=[]).new() assert [] == logger._processors def test_wrap_returns_proxy(self): assert isinstance(wrap_logger(None), BoundLoggerLazyProxy) def test_configure_once_issues_warning_on_repeated_call(self): with warnings.catch_warnings(record=True) as warns: configure_once() assert 0 == len(warns) with warnings.catch_warnings(record=True) as warns: configure_once() assert 1 == len(warns) assert RuntimeWarning == warns[0].category assert 'Repeated configuration attempted.' == warns[0].message.args[0] def test_get_logger_configures_according_to_config(self): b = get_logger().bind() assert isinstance(b._logger, _BUILTIN_DEFAULT_LOGGER_FACTORY().__class__) assert _BUILTIN_DEFAULT_PROCESSORS == b._processors assert isinstance(b, _BUILTIN_DEFAULT_WRAPPER_CLASS) assert _BUILTIN_DEFAULT_CONTEXT_CLASS == b._context.__class__ def test_get_logger_passes_positional_arguments_to_logger_factory(self): """ Ensure `get_logger` passes optional positional arguments through to the logger factory. """ factory = call_recorder(lambda *args: object()) configure(logger_factory=factory) get_logger('test').bind(x=42) assert [call('test')] == factory.calls structlog-18.1.0/tests/test_base.py0000644000076500000240000001250013233027243017552 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import pytest from pretend import raiser, stub from structlog._base import BoundLoggerBase from structlog._config import _CONFIG from structlog._loggers import ReturnLogger from structlog.exceptions import DropEvent from structlog.processors import KeyValueRenderer def build_bl(logger=None, processors=None, context=None): """ Convenience function to build BoundLoggerBases with sane defaults. """ return BoundLoggerBase( logger if logger is not None else ReturnLogger(), processors if processors is not None else _CONFIG.default_processors, context if context is not None else _CONFIG.default_context_class(), ) class TestBinding(object): def test_repr(self): bl = build_bl(processors=[1, 2, 3], context={}) assert ( "" ) == repr(bl) def test_binds_independently(self): """ Ensure BoundLogger is immutable by default. """ b = build_bl(processors=[KeyValueRenderer(sort_keys=True)]) b = b.bind(x=42, y=23) b1 = b.bind(foo='bar') b2 = b.bind(foo='qux') assert b._context != b1._context != b2._context def test_new_clears_state(self): b = build_bl() b = b.bind(x=42) assert 42 == b._context['x'] b = b.bind() assert 42 == b._context['x'] b = b.new() assert 'x' not in b._context def test_comparison(self): b = build_bl() assert b == b.bind() assert b is not b.bind() assert b != b.bind(x=5) assert b != 'test' def test_bind_keeps_class(self): class Wrapper(BoundLoggerBase): pass b = Wrapper(None, [], {}) assert isinstance(b.bind(), Wrapper) def test_new_keeps_class(self): class Wrapper(BoundLoggerBase): pass b = Wrapper(None, [], {}) assert isinstance(b.new(), Wrapper) def test_unbind(self): b = build_bl().bind(x=42, y=23).unbind('x', 'y') assert {} == b._context class TestProcessing(object): def test_event_empty_string(self): """ Empty strings are a valid event. """ b = build_bl(processors=[], context={}) args, kw = b._process_event("meth", "", {"foo": "bar"}) assert () == args assert {"event": "", "foo": "bar"} == kw def test_copies_context_before_processing(self): """ BoundLoggerBase._process_event() gets called before relaying events to wrapped loggers. """ def chk(_, __, event_dict): assert b._context is not event_dict return '' b = build_bl(processors=[chk]) assert (('',), {}) == b._process_event('', 'event', {}) assert 'event' not in b._context def test_chain_does_not_swallow_all_exceptions(self): b = build_bl(processors=[raiser(ValueError)]) with pytest.raises(ValueError): b._process_event('', 'boom', {}) def test_last_processor_returns_string(self): """ If the final processor returns a string, ``(the_string,), {}`` is returned. """ logger = stub(msg=lambda *args, **kw: (args, kw)) b = build_bl(logger, processors=[lambda *_: 'foo']) assert ( (('foo',), {}) == b._process_event('', 'foo', {}) ) def test_last_processor_returns_tuple(self): """ If the final processor returns a tuple, it is just passed through. """ logger = stub(msg=lambda *args, **kw: (args, kw)) b = build_bl(logger, processors=[lambda *_: (('foo',), {'key': 'value'})]) assert ( (('foo',), {'key': 'value'}) == b._process_event('', 'foo', {}) ) def test_last_processor_returns_dict(self): """ If the final processor returns a dict, ``(), the_dict`` is returned. """ logger = stub(msg=lambda *args, **kw: (args, kw)) b = build_bl(logger, processors=[lambda *_: {'event': 'foo'}]) assert ( ((), {'event': 'foo'}) == b._process_event('', 'foo', {}) ) def test_last_processor_returns_unknown_value(self): """ If the final processor returns something unexpected, raise ValueError with a helpful error message. """ logger = stub(msg=lambda *args, **kw: (args, kw)) b = build_bl(logger, processors=[lambda *_: object()]) with pytest.raises(ValueError) as exc: b._process_event('', 'foo', {}) assert ( exc.value.args[0].startswith("Last processor didn't return") ) class TestProxying(object): def test_processor_raising_DropEvent_silently_aborts_chain(self, capsys): """ If a processor raises DropEvent, the chain is aborted and nothing is proxied to the logger. """ b = build_bl(processors=[raiser(DropEvent), raiser(ValueError)]) b._proxy_to_logger("", None, x=5) assert (("", "") == capsys.readouterr()) structlog-18.1.0/tests/additional_frame.py0000644000076500000240000000077513233027243021076 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Helper function for testing the deduction of stdlib logger names. Since the logger factories are called from within structlog._config, they have to skip a frame. Calling them here emulates that. """ from __future__ import absolute_import, division, print_function def additional_frame(callable): return callable() structlog-18.1.0/MANIFEST.in0000644000076500000240000000044513230611604015625 0ustar hynekstaff00000000000000include LICENSE LICENSE.apache2 LICENSE.mit .coveragerc conftest.py .readthedocs.yml include *.rst recursive-include .github *.rst include tox.ini graft tests recursive-exclude tests *.pyc graft docs prune docs/_build # Don't package GitHub-specific files. exclude .github/*.md .travis.yml structlog-18.1.0/.coveragerc0000644000076500000240000000027412610175427016221 0ustar hynekstaff00000000000000[run] branch = True source = structlog [paths] source = src/structlog .tox/*/lib/python*/site-packages/structlog .tox/pypy*/site-packages/structlog [report] show_missing = True structlog-18.1.0/docs/0000755000076500000240000000000013233032005015007 5ustar hynekstaff00000000000000structlog-18.1.0/docs/index.rst0000644000076500000240000000316513231335155016667 0ustar hynekstaff00000000000000============================= Structured Logging for Python ============================= Release v\ |version| (:doc:`What's new? `). .. include:: ../README.rst :start-after: -begin-short- :end-before: -end-short- First steps: - If you're not sure whether ``structlog`` is for you, have a look at :doc:`why`. - If you can't wait to log your first entry, start at :doc:`getting-started` and then work yourself through the basic docs. - Once you have basic grasp of how ``structlog`` works, acquaint yourself with the `integrations <#integration-with-existing-systems>`_ structlog is shipping with. User's Guide ============ Basics ------ .. toctree:: :maxdepth: 1 why getting-started loggers configuration thread-local processors examples development Integration with Existing Systems --------------------------------- ``structlog`` can be used immediately with *any* existing logger. However it comes with special wrappers for the Python standard library and Twisted that are optimized for their respective underlying loggers and contain less magic. .. toctree:: :maxdepth: 1 standard-library twisted logging-best-practices Advanced Topics --------------- .. toctree:: :maxdepth: 1 custom-wrappers performance API Reference ============= .. toctree:: :maxdepth: 4 api Project Information =================== .. include:: ../README.rst :start-after: -begin-meta- .. toctree:: :maxdepth: 1 backward-compatibility contributing license changelog Indices and tables ================== - :ref:`genindex` - :ref:`modindex` - :ref:`search` structlog-18.1.0/docs/contributing.rst0000644000076500000240000000007413230606412020257 0ustar hynekstaff00000000000000.. _contributing: .. include:: ../.github/CONTRIBUTING.rst structlog-18.1.0/docs/twisted.rst0000644000076500000240000001064313231351267017244 0ustar hynekstaff00000000000000Twisted ======= .. warning:: Since :func:`sys.exc_clear` has been dropped in Python 3, there is currently no way to avoid multiple tracebacks in your log files if using ``structlog`` together with Twisted on Python 3. .. note:: ``structlog`` currently only supports the legacy -- but still perfectly working -- Twisted logging system found in ``twisted.python.log``. Concrete Bound Logger --------------------- To make ``structlog``'s behavior less magicy, it ships with a Twisted-specific wrapper class that has an explicit API instead of improvising: :class:`structlog.twisted.BoundLogger`. It behaves exactly like the generic :class:`structlog.BoundLogger` except: - it's slightly faster due to less overhead, - has an explicit API (:func:`~structlog.twisted.BoundLogger.msg` and :func:`~structlog.twisted.BoundLogger.err`), - hence causing less cryptic error messages if you get method names wrong. In order to avoid that ``structlog`` disturbs your CamelCase harmony, it comes with an alias for :func:`structlog.get_logger` called :func:`structlog.getLogger`. Processors ---------- ``structlog`` comes with two Twisted-specific processors: :class:`~structlog.twisted.EventAdapter` This is useful if you have an existing Twisted application and just want to wrap your loggers for now. It takes care of transforming your event dictionary into something `twisted.python.log.err `_ can digest. For example:: def onError(fail): failure = fail.trap(MoonExploded) log.err(failure, _why="event-that-happend") will still work as expected. Needs to be put at the end of the processing chain. It formats the event using a renderer that needs to be passed into the constructor:: configure(processors=[EventAdapter(KeyValueRenderer()]) The drawback of this approach is that Twisted will format your exceptions as multi-line log entries which is painful to parse. Therefore ``structlog`` comes with: :class:`~structlog.twisted.JSONRenderer` Goes a step further and circumvents Twisted logger's Exception/Failure handling and renders it itself as JSON strings. That gives you regular and simple-to-parse single-line JSON log entries no matter what happens. Bending Foreign Logging To Your Will ------------------------------------ ``structlog`` comes with a wrapper for Twisted's log observers to ensure the rest of your logs are in JSON too: :func:`~structlog.twisted.JSONLogObserverWrapper`. What it does is determining whether a log entry has been formatted by :class:`~structlog.twisted.JSONRenderer` and if not, converts the log entry to JSON with `event` being the log message and putting Twisted's `system` into a second key. So for example:: 2013-09-15 22:02:18+0200 [-] Log opened. becomes:: 2013-09-15 22:02:18+0200 [-] {"event": "Log opened.", "system": "-"} There is obviously some redundancy here. Also, I'm presuming that if you write out JSON logs, you're going to let something else parse them which makes the human-readable date entries more trouble than they're worth. To get a clean log without timestamps and additional system fields (``[-]``), ``structlog`` comes with :class:`~structlog.twisted.PlainFileLogObserver` that writes only the plain message to a file and :func:`~structlog.twisted.plainJSONStdOutLogger` that composes it with the aforementioned :func:`~structlog.twisted.JSONLogObserverWrapper` and gives you a pure JSON log without any timestamps or other noise straight to `standard out`_:: $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web {"event": "Log opened.", "system": "-"} {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"} {"event": "reactor class: twisted...EPollReactor.", "system": "-"} {"event": "Site starting on 8080", "system": "-"} {"event": "Starting factory ", ...} ... Suggested Configuration ----------------------- :: import structlog structlog.configure( processors=[ structlog.processors.StackInfoRenderer(), structlog.twisted.JSONRenderer() ], context_class=dict, logger_factory=structlog.twisted.LoggerFactory(), wrapper_class=structlog.twisted.BoundLogger, cache_logger_on_first_use=True, ) See also :doc:`logging-best-practices`. .. _`standard out`: https://en.wikipedia.org/wiki/Standard_out#Standard_output_.28stdout.29 structlog-18.1.0/docs/Makefile0000644000076500000240000001271012204211413016447 0ustar hynekstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/structlog.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/structlog.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/structlog" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/structlog" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." structlog-18.1.0/docs/why.rst0000644000076500000240000000150313231333717016363 0ustar hynekstaff00000000000000==== Why… ==== …Structured Logging? ==================== I believe the widespread use of format strings in logging is based on two presumptions: - The first level consumer of a log message is a human. - The programmer knows what information is needed to debug an issue. I believe these presumptions are **no longer correct** in server side software. ---`Paul Querna`_ Structured logging means that you don't write hard-to-parse and hard-to-keep-consistent prose in your logs but that you log *events* that happen in a *context* instead. …structlog? =========== .. include:: ../README.rst :start-after: -begin-spiel- :end-before: -end-spiel- .. _`Paul Querna`: https://web.archive.org/web/20170801134840/https://journal.paul.querna.org/articles/2011/12/26/log-for-machines-in-json/ structlog-18.1.0/docs/configuration.rst0000644000076500000240000001712213231347170020425 0ustar hynekstaff00000000000000.. _configuration: Configuration ============= Global Defaults --------------- To make logging as unintrusive and straight-forward to use as possible, ``structlog`` comes with a plethora of configuration options and convenience functions. Let me start at the end and introduce you to the ultimate convenience function that relies purely on configuration: :func:`structlog.get_logger` (and its camelCase-friendly alias :func:`structlog.getLogger` for y'all Twisted and Zope aficionados). The goal is to reduce your per-file logging boilerplate to:: from structlog import get_logger logger = get_logger() while still giving you the full power via configuration. To achieve that you'll have to call :func:`structlog.configure` on app initialization. The :ref:`example ` from the previous chapter could thus have been written as following: .. testcleanup:: * import structlog structlog.reset_defaults() .. testsetup:: * import structlog structlog.reset_defaults() .. testsetup:: config_wrap_logger, config_get_logger from structlog import PrintLogger, configure, reset_defaults, wrap_logger, get_logger from structlog.threadlocal import wrap_dict def proc(logger, method_name, event_dict): print("I got called with", event_dict) return repr(event_dict) .. doctest:: config_wrap_logger >>> configure(processors=[proc], context_class=dict) >>> log = wrap_logger(PrintLogger()) >>> log.msg("hello world") I got called with {'event': 'hello world'} {'event': 'hello world'} In fact, it could even be written like .. doctest:: config_get_logger >>> configure(processors=[proc], context_class=dict) >>> log = get_logger() >>> log.msg("hello world") I got called with {'event': 'hello world'} {'event': 'hello world'} because :class:`~structlog.processors.PrintLogger` is the default ``LoggerFactory`` used (see :ref:`logger-factories`). You can call :func:`structlog.configure` repeatedly and only set one or more settings -- the rest will not be affected. ``structlog`` tries to behave in the least surprising way when it comes to handling defaults and configuration: #. Arguments passed to :func:`structlog.wrap_logger` *always* take the highest precedence over configuration. That means that you can overwrite whatever you've configured for each logger respectively. #. If you leave them on ``None``, ``structlog`` will check whether you've configured default values using :func:`structlog.configure` and uses them if so. #. If you haven't configured or passed anything at all, the default fallback values try to be convenient and development-friendly. If necessary, you can always reset your global configuration back to default values using :func:`structlog.reset_defaults`. That can be handy in tests. At any time, you can check whether and how ``structlog`` is configured: .. doctest:: >>> structlog.is_configured() False >>> class MyDict(dict): pass >>> structlog.configure(context_class=MyDict) >>> structlog.is_configured() True >>> cfg = structlog.get_config() >>> cfg["context_class"] .. note:: Since you will call :func:`structlog.get_logger` most likely in module scope, they run at import time before you had a chance to configure ``structlog``. Hence they return a **lazy proxy** that returns a correct wrapped logger on first ``bind()``/``new()``. Therefore, you must never call ``new()`` or ``bind()`` in module or class scope becauce otherwise you will receive a logger configured with ``structlog``'s default values. Use :func:`~structlog.get_logger`\ 's ``initial_values`` to achieve pre-populated contexts. To enable you to log with the module-global logger, it will create a temporary BoundLogger and relay the log calls to it on *each call*. Therefore if you have nothing to bind but intend to do lots of log calls in a function, it makes sense performance-wise to create a local logger by calling ``bind()`` or ``new()`` without any parameters. See also :doc:`performance`. .. _logger-factories: Logger Factories ---------------- To make :func:`structlog.get_logger` work, one needs one more option that hasn't been discussed yet: ``logger_factory``. It is a callable that returns the logger that gets wrapped and returned. In the simplest case, it's a function that returns a logger -- or just a class. But you can also pass in an instance of a class with a ``__call__`` method for more complicated setups. .. versionadded:: 0.4.0 :func:`structlog.get_logger` can optionally take positional parameters. These will be passed to the logger factories. For example, if you use run ``structlog.get_logger("a name")`` and configure ``structlog`` to use the standard library :class:`~structlog.stdlib.LoggerFactory` which has support for positional parameters, the returned logger will have the name ``"a name"``. When writing custom logger factories, they should always accept positional parameters even if they don't use them. That makes sure that loggers are interchangeable. For the common cases of standard library logging and Twisted logging, ``structlog`` comes with two factories built right in: - :class:`structlog.stdlib.LoggerFactory` - :class:`structlog.twisted.LoggerFactory` So all it takes to use ``structlog`` with standard library logging is this:: >>> from structlog import get_logger, configure >>> from structlog.stdlib import LoggerFactory >>> configure(logger_factory=LoggerFactory()) >>> log = get_logger() >>> log.critical("this is too easy!") event='this is too easy!' By using ``structlog``'s :class:`structlog.stdlib.LoggerFactory`, it is also ensured that variables like function names and line numbers are expanded correctly in your log format. The :ref:`Twisted example ` shows how easy it is for Twisted. .. note:: `LoggerFactory()`-style factories always need to get passed as *instances* like in the examples above. While neither allows for customization using parameters yet, they may do so in the future. Calling :func:`structlog.get_logger` without configuration gives you a perfectly useful :class:`structlog.PrintLogger`. We don't believe silent loggers are a sensible default. Where to Configure ------------------ The best place to perform your configuration varies with applications and frameworks. Ideally as late as possible but *before* non-framework (i.e. your) code is executed. If you use standard library's logging, it makes sense to configure them next to each other. **Django** Django has to date unfortunately no concept of an application assembler or "app is done" hooks. Therefore the bottom of your ``settings.py`` will have to do. **Flask** See `Logging Application Errors `_. **Pyramid** `Application constructor `_. **Twisted** The `plugin definition `_ is the best place. If your app is not a plugin, put it into your `tac file `_ (and then `learn `_ about plugins). If you have no choice but *have* to configure on import time in module-global scope, or can't rule out for other reasons that that your :func:`structlog.configure` gets called more than once, ``structlog`` offers :func:`structlog.configure_once` that raises a warning if ``structlog`` has been configured before (no matter whether using :func:`structlog.configure` or :func:`~structlog.configure_once`) but doesn't change anything. structlog-18.1.0/docs/conf.py0000644000076500000240000002406213231315564016326 0ustar hynekstaff00000000000000# -*- coding: utf-8 -*- # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. # This file is execfile()d with the current directory set to its containing dir # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import codecs import os import re here = os.path.abspath(os.path.dirname(__file__)) # We want an image in the README and include the README in the docs. suppress_warnings = ['image.nonlocal_uri'] def read(*parts): return codecs.open(os.path.join(here, *parts), 'r').read() def find_version(*file_paths): version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'structlog' author = u"Hynek Schlawack" copyright = u'2013, {author}'.format(author=author) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = find_version("..", "src", "structlog", "__init__.py") # The full version, including alpha/beta/rc tags. release = "" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ '_build', ] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "alabaster" html_theme_options = { "font_family": '"Avenir Next", Calibri, "PT Sans", sans-serif', "head_font_family": '"Avenir Next", Calibri, "PT Sans", sans-serif', "font_size": "18px", "page_width": "980px", } html_logo = "_static/structlog_logo_small.png" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'structlogdoc' # -- Options for LaTeX output ------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'structlog.tex', u'structlog Documentation', u'Author', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'structlog', u'structlog Documentation', [u'Author'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'structlog', u'structlog Documentation', u'Author', 'structlog', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Epub output -------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True linkcheck_ignore = [ ] # Twisted's trac tends to be slow linkcheck_timeout = 300 intersphinx_mapping = { 'https://docs.python.org/3': None, } structlog-18.1.0/docs/custom-wrappers.rst0000644000076500000240000000502513231353575020736 0ustar hynekstaff00000000000000Custom Wrappers =============== .. testsetup:: * import structlog structlog.configure( processors=[structlog.processors.KeyValueRenderer()], ) .. testcleanup:: * import structlog structlog.reset_defaults() ``structlog`` comes with a generic bound logger called :class:`structlog.BoundLogger` that can be used to wrap any logger class you fancy. It does so by intercepting unknown method names and proxying them to the wrapped logger. This works fine, except that it has a performance penalty and the API of :class:`~structlog.BoundLogger` isn't clear from reading the documentation because large parts depend on the wrapped logger. An additional reason is that you may want to have semantically meaningful log method names that add meta data to log entries as it is fit (see example below). To solve that, ``structlog`` offers you to use an own wrapper class which you can configure using :func:`structlog.configure`. And to make it easier for you, it comes with the class :class:`structlog.BoundLoggerBase` which takes care of all data binding duties so you just add your log methods if you choose to sub-class it. .. _wrapper_class-example: Example ------- It's much easier to demonstrate with an example: .. doctest:: >>> from structlog import BoundLoggerBase, PrintLogger, wrap_logger >>> class SemanticLogger(BoundLoggerBase): ... def msg(self, event, **kw): ... if not "status" in kw: ... return self._proxy_to_logger("msg", event, status="ok", **kw) ... else: ... return self._proxy_to_logger("msg", event, **kw) ... ... def user_error(self, event, **kw): ... self.msg(event, status="user_error", **kw) >>> log = wrap_logger(PrintLogger(), wrapper_class=SemanticLogger) >>> log = log.bind(user="fprefect") >>> log.user_error("user.forgot_towel") user='fprefect' status='user_error' event='user.forgot_towel' You can observe the following: - The wrapped logger can be found in the instance variable :attr:`structlog.BoundLoggerBase._logger`. - The helper method :func:`structlog.BoundLoggerBase._proxy_to_logger` that is a DRY_ convenience function that runs the processor chain, handles possible :exc:`~structlog.DropEvent`\ s and calls a named function on :attr:`~structlog.BoundLoggerBase._logger`. - You can run the chain by hand though using :func:`structlog.BoundLoggerBase._process_event` . These two methods and one attribute is all you need to write own wrapper classes. .. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself structlog-18.1.0/docs/processors.rst0000644000076500000240000001075313231350403017754 0ustar hynekstaff00000000000000.. _processors: Processors ========== The true power of ``structlog`` lies in its *combinable log processors*. A log processor is a regular callable, i.e. a function or an instance of a class with a ``__call__()`` method. .. _chains: Chains ------ The *processor chain* is a list of processors. Each processors receives three positional arguments: **logger** Your wrapped logger object. For example :class:`logging.Logger`. **method_name** The name of the wrapped method. If you called ``log.warning("foo")``, it will be ``"warning"``. **event_dict** Current context together with the current event. If the context was ``{"a": 42}`` and the event is ``"foo"``, the initial ``event_dict`` will be ``{"a":42, "event": "foo"}``. The return value of each processor is passed on to the next one as ``event_dict`` until finally the return value of the last processor gets passed into the wrapped logging method. Examples ^^^^^^^^ If you set up your logger like: .. code:: python from structlog import PrintLogger, wrap_logger wrapped_logger = PrintLogger() logger = wrap_logger(wrapped_logger, processors=[f1, f2, f3, f4]) log = logger.new(x=42) and call ``log.msg("some_event", y=23)``, it results in the following call chain: .. code:: python wrapped_logger.msg( f4(wrapped_logger, "msg", f3(wrapped_logger, "msg", f2(wrapped_logger, "msg", f1(wrapped_logger, "msg", {"event": "some_event", "x": 42, "y": 23}) ) ) ) ) In this case, ``f4`` has to make sure it returns something ``wrapped_logger.msg`` can handle (see :ref:`adapting`). The simplest modification a processor can make is adding new values to the ``event_dict``. Parsing human-readable timestamps is tedious, not so `UNIX timestamps `_ -- let's add one to each log entry! .. literalinclude:: code_examples/processors/timestamper.py :language: python Please note, that ``structlog`` comes with such an processor built in: :class:`~structlog.processors.TimeStamper`. Filtering --------- If a processor raises :exc:`structlog.DropEvent`, the event is silently dropped. Therefore, the following processor drops every entry: .. literalinclude:: code_examples/processors/dropper.py :language: python But we can do better than that! .. _cond_drop: How about dropping only log entries that are marked as coming from a certain peer (e.g. monitoring)? .. literalinclude:: code_examples/processors/conditional_dropper.py :language: python .. _adapting: Adapting and Rendering ---------------------- An important role is played by the *last* processor because its duty is to adapt the ``event_dict`` into something the underlying logging method understands. With that, it's also the *only* processor that needs to know anything about the underlying system. It can return one of three types: - A string that is passed as the first (and only) positional argument to the underlying logger. - A tuple of ``(args, kwargs)`` that are passed as ``log_method(*args, **kwargs)``. - A dictionary which is passed as ``log_method(**kwargs)``. Therefore ``return "hello world"`` is a shortcut for ``return (("hello world",), {})`` (the example in :ref:`chains` assumes this shortcut has been taken). This should give you enough power to use ``structlog`` with any logging system while writing agnostic processors that operate on dictionaries. .. versionchanged:: 14.0.0 Allow final processor to return a `dict`. Examples ^^^^^^^^ The probably most useful formatter for string based loggers is :class:`~structlog.processors.JSONRenderer`. Advanced log aggregation and analysis tools like `logstash `_ offer features like telling them “this is JSON, deal with it” instead of fiddling with regular expressions. More examples can be found in the :ref:`examples ` chapter. For a list of shipped processors, check out the :ref:`API documentation `. Third Party Packages -------------------- Since processors are self-contained callables, it's easy to write your own and to share them in separate packages. The following processor packages are known to be currently available on PyPI: - `structlog-pretty `_: Processors for prettier output -- a code syntax highlighter, JSON and XML prettifiers, a multiline string printer, and a numeric value rounder. Please feel free to submit a pull request to extend this list with *your* package! structlog-18.1.0/docs/examples.rst0000644000076500000240000001010213231350632017360 0ustar hynekstaff00000000000000.. _examples: Examples ======== This chapter is intended to give you a taste of realistic usage of ``structlog``. .. _flask-example: Flask and Thread Local Data --------------------------- In the simplest case, you bind a unique request ID to every incoming request so you can easily see which log entries belong to which request. .. literalinclude:: code_examples/flask_/webapp.py :language: python ``some_module.py`` .. literalinclude:: code_examples/flask_/some_module.py :language: python While wrapped loggers are *immutable* by default, this example demonstrates how to circumvent that using a thread local dict implementation for context data for convenience (hence the requirement for using `new()` for re-initializing the logger). Please note that :class:`structlog.stdlib.LoggerFactory` is a totally magic-free class that just deduces the name of the caller's module and does a :func:`logging.getLogger` with it. It's used by :func:`structlog.get_logger` to rid you of logging boilerplate in application code. If you prefer to name your standard library loggers explicitly, a positional argument to :func:`~structlog.get_logger` gets passed to the factory and used as the name. .. _twisted-example: Twisted, and Logging Out Objects -------------------------------- If you prefer to log less but with more context in each entry, you can bind everything important to your logger and log it out with each log entry. .. literalinclude:: code_examples/twisted_echo.py :language: python gives you something like: .. code:: text ... peer='127.0.0.1' connection_id='1c6c0cb5-...' count=1 data='123\n' event='echoed data!' ... peer='127.0.0.1' connection_id='1c6c0cb5-...' count=2 data='456\n' event='echoed data!' ... peer='127.0.0.1' connection_id='1c6c0cb5-...' count=3 data='foo\n' event='echoed data!' ... peer='10.10.0.1' connection_id='85234511-...' count=1 data='cba\n' event='echoed data!' ... peer='127.0.0.1' connection_id='1c6c0cb5-...' count=4 data='bar\n' event='echoed data!' Since Twisted's logging system is a bit peculiar, ``structlog`` ships with an :class:`adapter ` so it keeps behaving like you'd expect it to behave. I'd also like to point out the Counter class that doesn't do anything spectacular but gets bound *once* per connection to the logger and since its repr is the number itself, it's logged out correctly for each event. This shows off the strength of keeping a dict of objects for context instead of passing around serialized strings. .. _processors-examples: Processors ---------- :ref:`Processors` are a both simple and powerful feature of ``structlog``. So you want timestamps as part of the structure of the log entry, censor passwords, filter out log entries below your log level before they even get rendered, and get your output as JSON for convenient parsing? Here you go: .. doctest:: >>> import datetime, logging, sys >>> from structlog import wrap_logger >>> from structlog.processors import JSONRenderer >>> from structlog.stdlib import filter_by_level >>> logging.basicConfig(stream=sys.stdout, format="%(message)s") >>> def add_timestamp(_, __, event_dict): ... event_dict["timestamp"] = datetime.datetime.utcnow() ... return event_dict >>> def censor_password(_, __, event_dict): ... pw = event_dict.get("password") ... if pw: ... event_dict["password"] = "*CENSORED*" ... return event_dict >>> log = wrap_logger( ... logging.getLogger(__name__), ... processors=[ ... filter_by_level, ... add_timestamp, ... censor_password, ... JSONRenderer(indent=1, sort_keys=True) ... ] ... ) >>> log.info("something.filtered") >>> log.warning("something.not_filtered", password="secret") # doctest: +ELLIPSIS { "event": "something.not_filtered", "password": "*CENSORED*", "timestamp": "datetime.datetime(..., ..., ..., ..., ...)" } ``structlog`` comes with many handy processors build right in -- for a list of shipped processors, check out the :ref:`API documentation `. structlog-18.1.0/docs/logging-best-practices.rst0000644000076500000240000000553213231316051022106 0ustar hynekstaff00000000000000Logging Best Practices ====================== Logging is not a new concept and in no way special to Python. Logfiles have existed for decades and there's little reason to reinvent the wheel in our little world. Therefore let's rely on proven tools as much as possible and do only the absolutely necessary inside of Python\ [*]_. A simple but powerful approach is to log to unbuffered `standard out`_ and let other tools take care of the rest. That can be your terminal window while developing, it can be systemd_ redirecting your log entries it to syslogd_, or your `cluster manager`_. It doesn't matter where or how your application is running, it just works. This is why the popular `twelve-factor app methodology`_ suggests just that. .. [*] This is obviously a privileged UNIX-centric view but even Windows has tools and means for log management although we won't be able to discuss them here. Centralized Logging ------------------- Nowadays you usually don't want your logfiles in compressed archives distributed over dozens -- if not thousands -- servers or cluster nodes. You want them in a single location; parsed, indexed and easy to search. ELK ^^^ The ELK stack (Elasticsearch_, Logstash_, Kibana_) from Elastic is a great way to store, parse, and search your logs. The way it works is that you have local log shippers like Filebeat_ that parse your log files and forward the log entries to your Logstash_ server. Logstash parses the log entries and stores them in in Elasticsearch_. Finally, you can view and search them in Kibana_. If your log entries consist of a JSON dictionary, this is fairly easy and efficient. All you have to do is to tell Logstash_ the name of you timestamp field. Graylog ^^^^^^^ Graylog_ goes one step further. It not only supports everything those above do (and then some); you can also log directly JSON entries towards it -- optionally even through an AMQP server (like RabbitMQ_) for better reliability. Additionally, `Graylog's Extended Log Format`_ (GELF) allows for structured data which makes it an obvious choice to use together with ``structlog``. .. _Graylog: https://www.graylog.org/ .. _Elastic: https://www.elastic.co/ .. _Logstash: https://www.elastic.co/products/logstash .. _Kibana: https://www.elastic.co/products/kibana .. _Elasticsearch: https://www.elastic.co/products/elasticsearch .. _`Graylog's Extended Log Format`: http://docs.graylog.org/en/latest/pages/gelf.html .. _`standard out`: https://en.wikipedia.org/wiki/Standard_out#Standard_output_.28stdout.29 .. _syslogd: https://en.wikipedia.org/wiki/Syslogd .. _`twelve-factor app methodology`: https://12factor.net/logs .. _systemd: https://en.wikipedia.org/wiki/Systemd .. _`cluster manager`: https://kubernetes.io/docs/concepts/cluster-administration/logging/ .. _Filebeat: https://github.com/elastic/beats/tree/master/filebeat .. _RabbitMQ: https://www.rabbitmq.com/ structlog-18.1.0/docs/backward-compatibility.rst0000644000076500000240000000143412635324726022213 0ustar hynekstaff00000000000000Backward Compatibility ====================== ``structlog`` has a very strong backward compatibility policy that is inspired by the one of the `Twisted framework `_. Put simply, you shouldn't ever be afraid to upgrade ``structlog`` if you're using its public APIs. If there will ever be need to break compatibility, it will be announced in the :doc:`changelog` and raise deprecation warning for a year before it's finally really broken. .. _exemption: .. warning:: You cannot however rely on the default settings and the :mod:`structlog.dev` module. They may be adjusted in the future to provide a better experience when starting to use ``structlog``. So please make sure to **always** properly configure your applications. structlog-18.1.0/docs/_static/0000755000076500000240000000000013233032005016435 5ustar hynekstaff00000000000000structlog-18.1.0/docs/_static/console_renderer.png0000644000076500000240000023413612635326216022523 0ustar hynekstaff00000000000000PNG  IHDR g^-sRGB@IDATx]\T{Qi@l{"Ě苦GhgIkPAQ"ET鰔ݻV(?s9sL ?.pppppppppppnu}nj hbwí6ϭBPvЬGW~um$""k[x{s4TwwP_69mۯnO<(UFѫ/jyڜLU!}/(cEgܮQ)LY5~-+ eVa. Rҵi|_ܶ,Q֟LD@ #eđ$,|FM@j|{/(v fs1]A.5eUt==8 Nf)k'a~?;/WlvU~c#_F~`F_Wp(JцY?Ov0V8, {(Bۗ`ދ"Ǥ_=?4V:/ ֶVWie6Ju寯eOYMa3W:];=kA\`cQ\{<Ժz7\'O @!JE芪 W$ik׀!hLlU+C4,v?Rȁh5N*(fBRLbPFRdZ@/,ʍTCRUfsU30Ga|DѹbR%9tX]9I+>q ݒp,e4 quJOx,/CeA&3Լ6Mi&~4IkE~ngXTQFۑwFSC1.%*SP7܅լn`L8胩S/DZ|dž"W c}#6)*hL>XwtqF8EO^ Ņ1T#P81MO;  5L 1b ^ X_o(Pno`!z g\̜Xl|u` ~5,l'0t'"Tr! ' w@HuUkieU J # 0ҠJ#ɫҁT?9f=L+'i$w i|Q\"Fs D@DZw# zf֬j3z7p`oFgؽ3K~')J`C#/qSV jx 5>{sPo<.DbvU*;A3aNFºs*UJKwRL>YU5,N7af(lFѸUk>*{0}T˵<;4L!:^=b%VH++k,&J' ^&>9kߍ򚋥Lңx ܲYd&W'߫I.bD"cgNEC?caTXV}^.W?8 \4pd,v>*GQl$g9G2$W"//%*QK'}rQ]&@PSY^LDߟGiceކ][OOcE_I\.G<_"͛{|ecwFK ֣=9R3OSJ6B:H+r.ۿ`qX7!yjCgJ 3Y~'^V~%ӂ3X6@_댟\> &7n?3ؾkk6b ti'Hlw0O"[~)\%mrojMRFL(OEjSe$9D|HuJ꘨T?@mLYv$+%y_;>ҥV..q'q >h kd(+vL镴,E8y2MF˱)ƫ u|[!?Mj]{e !ЯUBhjTI/ %%sQ_MVn?qn^0>ZWK~zM)S0}tLh`Jmビ1[aE1F;P-9l,f 6+]˴}=޾[cgNGHgC+̬h8Kn[vOQ865n":op 67‡&ፈ|=cҍ1nQ [󭧑ݲ;;іo +ْ9+[#XXYxc=F%cBf;~ְt1Zz}\?sǏ X*(07xw 붜B#1zKZAkT"OrE貕p@*Og>c8IJ8k<E_Ⱦ78EN ̤_".EYً+^mV | /; dǯ{0uWt}3c<(k(KШ7{y^ GmP%.Dn@0 o#=&lYnSi041Ay_&F i /HI؇̋x*7ըbX(G׭;*mz!fsf<Y';C"l?bYd>@QHl`Ld.)"V*'4GA HĭXUd&4hW+~4@Z>{Zսn#QmJn,*4CLJ\{Bf/%Y}1L϶d "fA^"VcPXx 7_O+IO`Ҽ \maP[^V(vl%4: 6f|߻doàqp˿?($>ɭ_PJK7Oנ5uK`Wa0jWAG %nkGdn!!tuenGvNCпQ"}5.5pKº ,</cM)98h'glC3p- ǀov z[<ʾ ҍp ~_LOT g t> /$;=4M&:i\~a_!cM:G7[ T|O_MP{F/on rwpI#sp\,eq<|`h;Y:u1e˾Xеc 6&h/$-,=Ya OLr7Z-4е_d ˪#ػit. H7Pצ#>kE~)F}ϿVӯuB3} R.:$U"RwZ*RF?q%|Kb3R'h#_G}N(w@OwF)̱H̤NRĚ;Ѹ(%Ғa6=/WFE%p: oah"d_ ĶMr{(hҫP@L}2ZT KL]sx|(~ G Ō7Ւ8tZ4?e+`1 8>4տD4Ca5+x@!*O h\J"hGYQmI0;[&#XPS'3!'^.%-P*ZV@$*Fkӡ VhOtGkkw `b1U*㪊E =` Yݾ34GQ *'ͩ.bjpriz0Km`(&2 oqnM49-mB#!%_@Jv ХK}e&A? W) ѥ\HҤ^X m=FT9O]q0̓h{ b0$0!C(|6wxvcDt ue,y߂|4տ3w rA3 B>(M@f̿;(Q޲3~R*~8㗣܎t[ n*E48#xZ|>~})4 ;zN>)QG~U2 f.}m 9}ϸ̭[iZ[ M2rbAH^(к"(y܄ *(>U孡;qX|vF5:K~r,#+R_pD=y/y8%YGݫ~ZI;Y~l&)фA։"uP,>q8|)o!7>:b49ӵ|'m1q$My Zq)E4,1׺!D}f+6YkU7oIeW֟-F+ >`C_6Y[tAX y/M}ˤuĐ~񋰠c[q}I=K`=@ AKwf(8{ŸV<>>v?k4vX[/>TWE3ǀg_)X9fhw`h<ΟW;*%?kE}%J`}W W)Hu=7ෆR1Gx^f@x8d!?g'hq'/"{R ƛf!Nt숖CEs+ mi =h<I0u3jǴ^M6;AЧ>{iZ_t0 o s|GUuo;-k 9j PQkkcxH[ $ҐL#ɂOx~i&ҥ%n]êߕt&9٘a>iB#l<33WACnin HKqiO[SAO4mѤ,3qBGKbo鴁[\g))A3>N NrUWέژe4!z?t)Jh&.M lըٙの5pHҫ'zaG&fS:kA,K/K0{VnL2ÎUV5HFfVcC0^t^lkXg W"<DIZ^=8ߘ)ꗳoL:hFp\곉Agԝ*|/t!)q?O5$8;KmԦ[S?]'%+!)LjLƅ'4*Al=zk%N9橽6S7#X^;JwWSk+)Z>q6VLj6xtt/cU`?YEɺ;ބ%[R7W 7Yº+''"t1mF+?chLX.䊗zz1ꥉr2K+HF &7(hObAA~R*BLгOzҔ٠~Ḳ~ZroO?CVam)MV4UXC'g|0V'7TТ;؊tG@qDwyNC#@@>]y$8 hH o; mgpL嫥b7) zR/`O*d'Ǧ%_MƖyo}|Aem3|G߾/ S wF+MƚۿƖ_kyZkn)f/Ug/F3/x/fYl{S͞s@jqvspppppppp4AMfd&4ъhpHS5f&4ъh(~)B@;5 67 P/7Q,'`ۖh9|(M sޘi}A9LvD|wLaVWz<]F"r/ֽ7ףkJK1ų7~x&/k2Kh| Ih~pBa^]EBy{8z5eTMϪS&,=U-w 𿲦cB%](1`sz? _Ħ?}F92uGrgt0F97Bp {['Gbt\1jʪzz̧dl  G;gߏ΋/ovU~c#PxZ2%7#Y5LCQ6(Q"3hGӧZ+{"B`m_m{/\cRկ9=8) 't_8mޙLo} 0QJyv@r.D{8`έV"'笅tUlm+3ч <[w{+;ĕmHv(zN"vx!l3T/F: t-1&:qN"wЦz P)xRQk89KhzU>HP-g!r@D!ns~5OW#mH:twʄ - $0X X8:%^awˎ,DbzAmuB0ɈP k* ;{w#AbϩqtACPsL6~ tC>KT O"yOX->cC+\ֱ>֔QYwtqF8E訯VBƁTfZIpyKǁmUTI1TSnaPCYD't" yɧVԭRxcʯV(]X +hoEWt <59mы2@sف=|ϗ բˬݘ6.T=9 ,Ć+D.3kz;wJ:rN}^ۢcmRml\Tt I7DbEJNmL >A@Bju(OV[c^ #v@~LQ<5hJ@ l(lFѸUi mQM'i5h[ˊh|7fu5iyKO%$Iq|Xg!8":1m5ĉᗑߗ cDUs)D֏!kgbqWI*‰`O'19ytmLE1%ۙ6^Zэg!o2=e> [~KQXں[+OD_7Ow㰖*w.DO1Ke5i-DH'vN#[ '(m:F?ˎφxq̖g R3l`ssp]ln#.4Iùp1;qOM8o;C`S=;xNMZm⨆J&\l+[ o 6y)v c? Bxe`}2Nl7tlyąj+ i-6ķV˫Q ׎IiiKe0#|5B;Rr%-hs<&NiӦr,r^T诫ʷӤץW?: _j11Ez\2R^Zʭ#|#!._a '@Kr)կ] 8e O  _WQ!Hǣq^scZY#dj'bۚƖn!#!z[pd͡ %0Kl>P]1n>9ǎbxmacŞF/- <ƛkXҌ1ӏћiEHſp2&9Ͼ=Gԙ($1/}3D_ vzZ6i}q :ްJƟTxh~_\[L4OҴf,1mzaw_bHTw5gaӸ\B D9i(?oId1k:6eI,FD˱hFm7wK# K!LMrK,HGK+H>z셗Wڽx:؉Υzñ h@hm۠ K\܀a4Bj,1apsJL b45OM`ȼxGJ>g^SITIc;C"l?b! 'neȚC! WH- Z5_Tseo_ ftW9J~'lx59Ǭݖ/: W456 Wq=19A"nQ[',po%ƌܳȓ= Ӓa^ N3Xzvω3e%F=s YI|,؍i{1@6:**؈3i,ٽeknH3H?cUV4`)<Ҙ L #<=zgMjٵ_^s6%wO7x':A{9Oq1h$ _JK~czـ{fO=D9]aטXx 7_O+IO`Ҽ \maP[^ҎRWgy:ƌ{Wt7 k3p˨(#OrkoҒ,TO@*7f1Fb 4s\f)]¯QLW:#"}!:xa *6GǬ~m1R&ibx;Bm.Xv#ԭvrDj/II6wX7T/GWd]|luF3, @xH-U i`mLQXkޮAֹx.bṛҤ%~'֩S׿A0{07 Q}+gOX EF։-]?"PˆSie%w埂>D|tu#Q'RxRUBpaqjt9'j(ʑg8.MͲ8v>04ua Q{β˾XеkZm4ΐ{h$*:z$w#L][E4r~{7*Hll~)F}ϿVFONIyFf6]8e<;p~f"qn@+x?LhоIHgH~T?QZ΂1^q[|i|Y# cۡuBaåS%1"R KJi 7A+YS񬗠.w"H=ڎpzGU>t ِ/,4 @b̑&?k13^¯%r~+gu̶mjboc۟3=.˳YYDqUF~U*SӨ˦$4 P p|22&,lBXN%[qCi~6ʨSAbYih 2ԡ- ׏zo0i|o˿\F帟!,'Ya--,9.O@ |X 1?P?9ןc"(KYQ-I0;왟$U%~q'[.eL0 zmWaUn⣥PI>~mƈ|s)edd=]#=xXT=ONX_`nnNN/[`/D۸øuag tH^,,'GC &\ڛ26Kb&q[ŲC 6Hn> Asڅ*,zRK&d:Ka:@BܗQ2˕f,l10azޕb IWu92eaf̩"(>}xe7%ySQ+_y wÁq6鑓.yOaIH9*1׋=x\34b}f ȠiHt6~2j2ҟ`D騡޵v3:K,D;& dh/CEpjů MwnFL[ԫQe<ێ/_λ'R;%ݓOV7){{)з1EKψ^x)R&근혩yYOii-34k{A*P$,6=;ơ.0rxOZ~BK \ᥣ]QO!q;G<{2"Ǻ*:H%UYXJߴel VDko!ĭ v2$’2zSסKS7$~})4 ;zN>eBiTŶ҈VZ[ M@N-cӬhe^bCFP@Ղo.@p#xNC9߳诌}`B"Ey"Y1g38$qhʢ4hOM LJ ~iY ekpW o`ֽo4 'f,kvQ*Z.,fY?`F;k)A*MўM~2dlx+˴)0t~5Rl ޭj*ZvS/QMW-ۙ:3,e#$ju3Oƒ8*Ui@Z8v=tUSNP_pD=y/y8XAݫ~ZHMO ]zpIIVq$J;%2k-Gf߈hmQ̾xE={‡bc/iꌏHޔ;F>Y/egNWɄY73d{EC`C_6j]8wPYŅ;04~ijD> w 4a#znuYs{ OK \'^Z5D>ܜ(k6cT(MF"F-͆\<d&ȩ{ }Mk a#\e@V2l@lhPMADYH:Qf十d,xF nԏR^ TuA1\8|?Qy_rmd8&`o:/Uò;;-m J\*]~/BYZgK'T)&#UÈf / i/+k$ iM1Cwą-8yٓ14 ypcGut /k>͔&V{hyr+a%x[=}ihԵpxo&ّ#;?q~v9UԀ1b'@- YZ|vj`O3ON>e*K~T8ОI]J ;8XUJ=:|/oׯ~ 7,J˔L, viUNTdjtɊ6`N۶^C1/,ye ,GrS3]q /I?D>D~j0RI02pߐ/hk!|mǍH4qcj\S?r,M\v]h{ tmi97ebUBv?gpKEe;izOoqgXOqH.IG9\\QX(n7c o=5Jj 3-陖>뱪.l&,6;SwXJ%5^E?! ق/١E0 ʌfHї@Vl8gE`i\ݩ+pWħnkй Z6{ȣIó_U2u44Ox~i& խkwX?u{L}@2.747dF\CuL~ߚ2zLi&d:ZP{K B=+HIT8Y 9bS~5#4~-t+]2sLok ˿Fц{Ve{QD1!%:.vAg:KhGÕlv%G%"U 8\7ч=9: Z_-]ÆM SZxnʋ> cN/;diiGg]'PCBSf'83\ O,`iT"{$x jO!͙iRb9֘4(OvB\_6SЮ!ROM),L[`R, I2fM?iH ` f"%١4aCa،!m?py'!֕&H*8k`*}ND^Uٙ잫u+: /yc#CT<Ūmo|G/*m`i|I9 /)/ik3΃,@^1Ó )h Dݾ2#{{}_gʼnr㩍˟nL@vciΣ|hu,Ih(P8Bteኊbz4vY s_%pp]C/#ou.! "q'l4bqDolp]Зχ btlq5`q FCCCCCCCCCCCoA>{pr 8{ 88888888886$ǽDsC @IDATmqqG@;5 6  ×Ψ>k&!-s&a5F?~poH치@?&w@M{h_|'ch(p׾O <9~$v^Txy3 u7]yM7#Y+8hˬ_G;:>ׇ^?Y*M`Cھ^qǤ_{L/s}qRNP0pF[3[ޗ,a3ř#[_?ahWu|Y_gXA] ʛ-1&r@4$'NC!/eT TNz蠌N{K]r89IO}%AYGXk4V,DbzצI#:8$#?&| k*]Ў쿳w7$1wq(QUݝ:Çt.;1㐢N=^~V{jq^)㲎A؈2`>y%2KrgKl <0`*uêD[y† !|xk1 z g\=8l)A3C@gd{@DY u>M7ˠgf*;wJ g&&0dޙKORT7(G^f:0'ȇ DP۹fH7o"_WM\~6h&Aܠwlt.;Vhh/\WN)*-)&FiJ@ l03S6h\*`.Ovbab [ļ7P#'+~I(}8=oyDc +Sg!Xu!*coIs ,~e%_:dKgIa#g'3Q,ĕuSb5/쿁+qZo&LG( F$asl""_؏7V^ fэ4r.G<_"͛{|ee33XU~1{na{ya[Q)}SȗyR,1PPuV< ]$" oR9BGDgGsE+=/dq|xYՖ#rplm|K/l|Kpid=AqlpSiz%ѢxL<ӦMX΋OUr-&.Uͽ2Y*!4PR~5Ť _P’չ(/١[GGC\; O"խR_^0qL>XR[ך$.6yOfؽʅ2A~ǔvQWF߻rQkOvje0i,'ؽ% '#qK2nfl ^&o}4cKX֋} pLNOrXө2S+OU<+b*{g/@z=ZZ9w@v \S;q5K7Yo8V_RzýWpn_BXàY{X>c8CY2eMt|>2/^T`gcsp<TGu6){x o<yc AѿdQE~czÙtI /{Nꔏw,<ꯎCb'f'0i0Э ҎRWgy:ƌ{WCm4ngϭ(#OrkoҒ,$ϫkn r\eRx3& fvcf;:N֨N]=gY%/t.Զ1 6 A jrbVx“܍r <3 tmO4a{'!!LP4 t@HV\5ܖJ~q)C S&lg)F4rIX*{i&.!! ¶Пa:y]|FoJ  5;f}`в3jS\DIhnn-T;5j%<-M >|]xO'bPnVUwhzX\(>dJ1<{pq@-ը+n,Y*%teyNkY5r#͵|7Oƒ6ҵ//O{)v2hT=j2&tP2AӳL{A~Ar qkPN! QVޱY=|i[~ FcXb]5ZFdQO!屮QSt'hX)t{́5p/#Ze)2 >N9,_MS}|uNOFVCJSeLfbxFը\!ic9FS;e93iuӠ%t2H'+gձzy1 >?뷝jN`0 kxogxx Viq;08q[E 3G-m",!_XF sbX9U;Ñمkp-ܽZ iٱi8'-doD]y,R'+mtmOB,EBu@Z"""""""""F@y?uQ±YXXXXXXXXB_aEe`````````? "!+9xs4D㭧"^S澽>wLaV7}ze-c3"_ r|75N_K]cq1z꿰t)ߴc#X1BQÆ_r /_ s\ 3};)vTIԕoQrV:ڡ_~3 =u룰G|ZN]c \H[c]*؃%bFفHǬQ/6^IS2 xT6knm쿘D 3̲bR a:ZүDt>enw=GbA076"pi**$j`fM҃d]+6(R#3<˞NZЫ} Ŷ>J-Fc~jlۯpPL˗D_v|ar: m^Y:@g#_>~jDnpvBCcg# VF1hL8c#aDOUPn(P#,Uc| HG喪q62 oUXdh9*J"eJۑۡWVKP'ELg6=_X\ֶIdbύXȏÛv R}ʄf"; Ia95qTNA8p\"@9Hͱ S4Y &B,lͶB,dH?p|Léʬ4;5vXDC31)+`OVW }_t3>>Q1lPv^m Gl)ꩧL1Xrqv o>E%odH6t g'z8| |@W?tĵRT  [y7 /c/C1u6v>jh)fڶxG1L 0`_2DKgoGSJ1|xx#]|g~j\)xfA"EbHDRa-ƙYJr0.>dtN ܊CV @'Yj:tq* g^qLJIUp2گDQV;lCf4$Ɨ h0 9Z'G+)U/v';mbUFm nȺQAaf|˲;|5±'N<09Wg_c8| =1$DY_C V+Ğa3&B=LcH9ZkY Do,mD1cԫʔKALi/Lbp1ʮ]?-ő;0ޅ:+QVCk g"z>, "uxAp1!I@8#xLv4iDmVud XoZ_w2//t)]EP1Q>xnn p׏mlpt S#vo FM_ pDUo S#VV ŽpwF IUܦ1磊c~}3 ]'*{.MFU# z'&^f7r`lV Ϲֽr=0Ȓ\Ԕ} gÕԾbm[us~å> _0ȧJz sP#cU&eµB4j3F>ZM57ej*Ǧ,ٍ37Dt^8_E,4^ǖ;Ru r=(޹muc!>k#njUdQ|H:3u1%c1HiAcB1oe(> 퓰6lkl+;oY,@Pػp3½cn)߀Z䘌D&Q͌éՌנL@G|E J o~;d)Fwhr0u3n#ڀQ|P Y96V ?F-IҒȼ)CL_MW0{8MZ߽MRd9L 7C9P~A_c(DRU'/f#_[A7z8aD폖:7Cfn:Lg]xއbS8ke%)65ȋ:C}{2WbWE7[9Fo-Q;-5gu鏶3j.f?эr@w<| MEbDTkYN?ƥDSOza7dїs4N1hͯ_oj,kiE0 րWkEŸc .Uΰ656"ˮcCûrevRJo^CS:p㑟| l뀓>v?>iVMM<^ݎ߯p38~0g+4Q#cYIr2o=>e:;o2x4S;.F I\ϸg(oؾ@8wepfRo;:,})l}sr6nތ/tn#C1kзOjbr"A9_*' 4/Y  VDSv@ɋP''Ю=i?jh͸AiUvF6ՕӘ_K6CmL5+JłGFdCp@9VxօڅH1io ?Ztc?Œ85 2뙫 L ~!3|on`s.aJ<.iMÉ̖Q;vYۛswB@ZƪۗӺuV/V4ّd7a b{A`JhP$ hP{L'~pH S┞sK:1x <4`҈'9jhn0 Q" x~dыD_ZEQ6\-ǧg2w lQӱKa9 ?/<['&vfV6V}í9} uus8RʡKWot 7ن*X>у} nVycSGpQf:t3]|01S> k rSQڠ:sV![)+JqS_ uÂסyYx}ġ5 =kz<%YEM+ RM3 tGfTf4]S#$A#,JN*sx>6|7-4Ů϶3r0)5 N^X82M/m|LOc'7F;p* 0d ItʰuO_eoT:pBCœǻ`ֺ ƕCCHNv~E#@1 Żhѯ'q1_ 5DgiOF>9P1F&rK3)߷SE?YMV0Փ0k#Z|Uɷ㙻 Hd'j/oi,GaRϖb5",E௃@w>0u ЙQm82 o0"u-fNE<K:K8"FԮZ}qS)Ba2ȗDokM oKjz>iiJNs/oSCz*T٣ȔWb= ʒC>hdj̠`%Iwؾ2.zԧ\yͻðQz}qD J1nviZMQ{7m$HYLQ97HC ^ y<=1u<"3|Qu.a0תLmhMoB'y_oj(.{\IMhll#Bšӯhyp#P[&p?yO-dilV\*s8Cԕ8z[?_˺6rr3cYqƧn쇯3$qw>:'8z@TwFOΡ+Oח7ϓKPK ^vn8 v L1]:,,hw[ CCa+o s1 ܫ)E_yJ ڌ4ʠwUhīCJX i3p{()ZpNDe"T<K?fmS5ލ/ǗYkYz!jⳢ=r5dzH$/.rqᨰC3q8ו_U{E&QE4C8g&Yl6LFO-:{8s0, ʬXlXoK(ɦ(5gQvkGH ,K8Dr(UF^I8=hr߫{б֣֤!?p"u#b pEf$5$ei8#F$lXGBX;u9چu''n{JEҨ_TzGZ1ŅXѲ7vQŧ4Wd}uc̗>_.=_z1{tJ[ވJ/WǾH ôZ,OR偶U ]x}~AkH}CQq>Z4ٸUnƈm9޼(› 𲑾yY|{"Ȃ7օR+_A%|Bv`)}$;!U~ߎ`=M$©:?UKMā|I5v/MI k b˸^R7RWo^X1zM 7GNU|f6z(OCU9G/@ka8WAZtO-A \ _ 6]0 )xHh25א4ЮWO{8gTЫ'zp$ |:GqTSc-Bh AN ЧSZU@# tLOރWe*b؈*o(|9Yc|V c6YL D/ꤱ)3&Zo"~ڎ/nAz|lJ_/aFWP-z*4'8,oہghR/\jskrbò.7J9{0i>߄zB%92AXLt m~+B^"sPl'Lы'Be6-wr4RC7Săk!W5g딎<_F:W^nY f}X7`3:a4_~+ֵ I`I8|X7]I{>jLʛ6D" S:9@LK`=43v9wz-k61ip -_~x-ǃUWN>J|_'LY-ݣ+,Ů -(:v>:f?F>isFז mOchd,]<T Q:S>E'. w"[>]E5J {>ǻ;F,YC q`-hVVEB)8 j xIpT! Gt ff3Wrµr2jzGb68^{Bm6K?â˂>WaO=Wni$Lp q?%蘅6b~]n"VMt} \S8n~$}b6A2?>~N2WY:T : zS 1b2~mb|G]3騼|B7lM~Yp O~{U+WzpևPi+C=m%9PL˧ +8ކ xؔ%vrLH`bop(lqmF㔤r$ߊYׄF< s0O]|;>POmX߫M:3 w&7ݐy8"+ҁgK qz6;IPޱYxWW@!Fg'/49vE6]nv0h a0 'qJK!Y(&8YE=b %FavY,Φ)b#\e: Jp_;QA~%D:G2퀨7lO=*(kq}#D OLDz7i'IĎibz٘'F scAb?~N0>/l+ $SEc=N= MǎFͮ8$wLL x>0d~E75e`6>ryzU/'g`6STBFƁ°8UebanU&y:%a$PT @#AvBaF`3P}Fʞ~ށGȊ>to =^{փӃ M >KuHI_0F]{9,t~9sBN?{R}"(fnΓ9ۡe0Q'w)zUmR D luaCxjh䏤-_Uœ; \$(D$bB!'q>o C{b-Ja^Ԝ1DJs:8xb5r0g d"_F~gw6c"%< HVъ|u z&[h'δZsDPm nȺQAaf|˲;|o?gqlqpW,6pD{cIP<t$ $ID |!\>InC 2ppdBJSۏb_y?y8+V1Ճ2R]y1®ݘpKzMso%Cv)ÝFǞJf> +/aksO< mN9/6 ޕ"WZ\QMҮƹI'aȴohGFfo_IFȭ{w B{UmEFM܍~:DTv]i$9\Cq RD}[Q%o S#VV ŽpwF I5M=F%y6,~7C˶74KLQ淀/,f DA;> 3gB*%3yw]w<0 |?O}݇᫟")d2S W64ڠ/#0,U\+n`Vz2r|a}cG8p+q fp_`p?sJ3MՌ&|)w>m qf=Z9Eؔem*y$LsL`ifY6:/ˬLJ}6³`>e?cPCuHb5pQ1Ts tz8Ve=e}/WKeyoH ]c2ZXGJ#O; /S0ۺ/JHYNFoU#(mݰLu UI=UB [}M(mdLh%xt /i)^,Us+wPH9Mie *Jc/;88Ë]6A0߇dH f(k?h* )=G7|;ifoKTg"Xy 4,. 4MMC1v'ڼ3E0 B֘j+# W\#3軭Dž1}3跪UXd<fcx N?* 꽚 V| N!){wNBAx$"r{ 90I Ehx;Mfձ {}w==y\2弃e'0vчgWo/XkqbY̦*L|:MXN5rwr@ւc^}YhJH_CRs?|<*Q.?WJuڌFzmtc:Q>w~Y| S7XjT>d$ܫND1Gv'ק+'0wz vʿF&b P'QMo].--S+wRe0{d7g 4_Dehaove6^; }÷T}_Y+v@ڒ_YN?ƥDROza7dz2X,"(MžSԚo(7tX01Ҋ`bG x'Py]qyI؞{#f\ݿ6Tކ^wuyH{3rPԢ:m8Д,I;'Vrp| %@g T$OJiNnK1˿?,F?j|yT5 ZuoɳEqw@=klaO&ZD =-9 ʓ @ѼD<%T^ݎ߯p38~0g+4eľɼ!X\P"6Mf%Rn4u@}@l;ijȘzb։[hjp^aQTC 5$k<xJ gPA;Olm5Vp! Ix6'y-,y3G8Vx#u$k"cXvPcVHRWbs+5ٸ%UiăpTsc]SfhAA+*EOv994K3} @SF?h5P>`ڱ`̢aW7)څځU9ط/u똭 ]_-꙾%o& VEɉ z" G=0%N^9 1Dweɍy!I9$] VT`ߐ#uWW˯CGD=9zQ%(LQ9j{BO#JbI=ވCu8Dsrj7q0`FY86G RX'|é!шub"nl PuPU+ 0z0?%i+45L!ȧKS+z,]^M|4ʍo>hNQ귑rkO`ro8zD $#tj?GYg:`x^$3B)p"35^6iI4M ̂u.䦢Aufr4&nuk, zìh^bsXj~؈w<ċk=bES$NP'k(2}7>df<8!D/FBME.ra1-Ȧ r|Ne,E-9W|~"a#2*<w7/*̸_@hϕuʵ+yx$Im>gom.EKψ "T5-=ٜ0nP, עV6n6&^w׋w-ƿxMjaאB]=7zo@FO (IWW7Pg?>aV1g3} !`<3ϣpdޢ`( nB>5Z P\Di+ݱ tpnE :DICKLCp`DBN DwiBXL]+@ݧxgbYlRpB<>-o0.KSu/b,u2Ęѻڦΰox]¬b7pmoK-Q'$4d _T;-a9Ve |@nZ~h?qz;)%a xupjB?szMS_zS9" Ѫ)2wPЄB4I~r,l!ӧL~RqU pK!++q|~u=@"{`d9^@p&4r}9Vx1v妓]5T51ԵiMHIhJQ!|hq t5Yqlߢu/٘Щa՘̡ u7%hNPckpðHL$Y!*KDZP8=MsQj΢,숗׎^ahťXBH_-/!C4/ro5J*A;T^ݧix  k=ZmMm0~|//ү ^KU.2'Ebf IYюqhOtf݀r<ډK{})rvc[R4ޑdLCqa9p]n:qWb48'yC#0_||*Wqhؽ4Qp('vĢ84%A7e_&w>=F`lXB(UTNCrU%Ѿ @|e8S =2i} 'yQcdR)Hg܊p {pF>L^ WzG1y;澾)g=o#XPnE JBT3ͳ>iT;:nH@r |'ԤkI 푐:Vd/^em HM[ϼ-/&ۍ-VJM}`Yuu~|w  ?k2*NNk/ /VӰEWg^f^(~mt1#tJXű?HP-k8/mCSd iY.p6 }j~\O~! .ݍ3z*Fq.vAsbѦoC_ՁS; X-6{; `!R[ReEe]>nU&ػ>|HR#'D`;HTHsiO[QAfM?YfB?^<*iPԘVA?h"ؙG'TfJjWͭڑhC\7(UELW-lGw۾@\}v݃߫ qp̢T]jIt(o}o01Ň Y&mo..[rXO{3*QxJ[!3dqQ؟]3qGl|7DFB#C'bSϟPoY=NQyKE<s3D-B眸s#:Vtf3Wrµr2jzGb68^{fgY40j䅽Wyڈuqͪ8ߓf%]$Z{\u(7|p @ {^8%v=JgIr$7S^^<hqa=ܐc=t^8hwY_G(L#0]]i=V>f}ƠsGn:45FćvN$ Y5,,,,,,,,,,`ﲁEEEEEEEEEEE : Of6a,,,,,,,,,,O ybPHa`xJ_Ws:)<hʉ[O?%=f}{ |LMaV7v]*:gEm[N';9M hRWX\̓9kcEYT X6zv6F˽WҔĂ'LaFJtpx8,+.e)C!l"G_É*K)_O?ŮKʛ 0?{p؈Oh`ftJP_k8|EنY?tzڪG[lCضu(KNeJmx0pM9^[\/ (6肋 ""Љ׀a B<;©BⓋr|yghs|rwRNlI@jXv R}ʄf";%[D8*qJ'\%?YPK 25)sIylڶh}Y6f[!TY O?e^$K8k>&TeVMw퍚]qI䙘}0M'l+/6(;y#6ԓz98;ηQ72$:acYڍ3 uwEC^ IԤx,18g=8=@t`3gcǚ" C@DĘ+ÐTX =3+ /!܎8Lm:aا.4 QҜ5N%!dN0Q lğܸfHIUHWWň:XA[kR|ޘZ9O%|r(T?"nWLY،oYvǞF[8vYhS9k,%̵_Mr',xI"E?yҥ~933®2$o  * e!oIhd! @K-<>!LwZU(d*leeMrw)$_{'>aQkUl }fLZXDϒGG1J1{kS$\L!7x˕ȿހS7A8|, G ,]v}#c62ҿ/;R7u4 W'cҶy[]Rn4Rgp) 5+ &}ۢ+8&~,{0[g7ڹgt+y7KTt ~uZGJIy)QR_-Bipkjp*S=nέNhXE]N/W_}nG{#h3%Ը{g:lSilJwVݧ2n,^οLJ8v}1ɪW oȧtKreӾ7R{2z>RQL*~t G]mþXFg]<`@:*-}ӌWVCZ]G'ptF6W*V~Eq?Oaa.4Q;LXZנ^%}o)w,xKҝX~0J'߇& ipPRf䯸*px\kvOT첚 tum}%NS\5l딡HwV>~ ;=$p? 26 Q?7N@hc!#nH՝J(!o7dE/G'va:qBI*JMK~:9>wK#;%g|GSֿ/qEԫkD w>o.?ˁr$@I@0UnK굛8w *eR?LU%dd$'{@QY;#6q\J:iC)엹YFg~]qn}yZdFs;6ڰ?'?Yzߥ-_pL^k*~we~ѶAF*/6̱UG]xy]pId!Q,;Fb_j|~;^=NO9ʐ xD|c10(nӑqKsrwO[\=5ܫJH4 cO7s׮R:_JCL h p@(|Icu;w~ƙ3_*6~)qi;K c  *qfZ{^n2 5!k7:6N}(8). @a%`%"    (8)N @ H'zI$@$@$@$@ł' b     ( /"T:J eFNA0gc7j$(S4 =5@y\^hexի_}5w卯୩#M   (>ǮFN#fHbi}e9&Rvr r4l_i+dFiY⧞%5RfƂYx;$;'[ۗ޿ oj~f Ƌ1bE˕:"'W*!lonڱYo\%ɻMފ8zC*N^Ǵ-)HHH>%XDO^ѣti&=M+ZzXB\psEX3z.?/L24)/T\aأW<QTh½po,ގVߞQ/6*/R|cXo_ Y嫡f;8m4n拊8I["VEn<*&FQcޘvx&=7j<.]C_2{E\\gH J$p09YS#⣒X59$CP<æ^Uq+x%bؚfBլXז~Pd|7k1ñZ9ŒMAdH{qI}X2cCHzzǛwRCep}/1g K6/Hޗ6/zװĥU4AGqɫfijZ"?!?b} yeF1%X&H-r⇢]Xoé`iE\> ^!=,d#6HWje:(o"uFvE쒶,Pj~LbǵWh=P1Az/v~*lFIƍ55r]E^4^Mpiۂ:FݢqZ?jt*\Оl "WzUqlJ$dJ6|bja↜ȉ1` 0&`![,EY1xXB kI1>ŝqxw|9hЪڻ+*bgnxӷG~džH9hb 3FB62 aM6 %ݕ+Z#%%Ghs6lg!ej<{kHxiWGzԻ 0h:{j"T_{F3 K%|XHs3t9j F-5 h"~<|iw䜃#Sf[i*:MH0b FKr>Q,Ʒo)! !Ŋݷ[ޜ9ꕓ8UĚӍUӑ,lRS'+:RNo=\sl'nBd6!^ fEj3pᗏ1qJe쁤G+O,lY.5"Vecܠk 7l4nS_ФeH37h^bY";3¿kW WC{yb@n$@$@$@$P[֟+6 CH Nݒ+Xh0}77OҙW+*ؘT\C%4`Kضl, QѷتI˸ZڤЎlmj,(}z@hY+Ľ&fN9<ޠ섲 ܦl^zbr"8ti.|Z[j^e'Zl(}=r']ܳ/?ćKԪ?-\+N   (`V. dp1^ǂYhtk5=Sت`C#pk-LNNƑH0{;9 75Y8%gޤ/; 5l$ą,Ezb}8EpD'w^NHw3 Gght|nA|8q-eԃ2枰I.9cķ6_5}MD?AS7!D54N>CV" Ӱk\{fM-៣&H1-]^nϟy@YJ$@$@$@|m-5OM{?~ϲszQdiU%pAY-[ϊ G%AڜܠSjmI 4Kv~ ܁+[w*g >?A)t]x-a&xaz@+ڌ]K~gӿf]:ݴu9zNZW/-C+hu0[ۿ+HHH> 1D38!W MaUX}cڀ*   NN@ggV=*O   MxO@رw$@$@$@$@$P(yU䜧$@$@$@$@$@E' Ek- i$@$@$@$@$PpRƋޒ @&`x&K뭹޾CgM^ǀ"q [eiQ2kiċ{No1 l{5nr[qmGX)Hdq6˫BG=,7#WTl@S`ʊ}s(A$@$@$@V 8 HIQ}}_EKVh/bq02, NQI: Aۚ[b?c^ J/~|ǎĈ1c0TTɽڈiWnSqp#]0wU,.]Ek;k82-##frdLѠ77vMB53o-#.UWl?VLYu$[ 3Ho17ñoݖ~8<޴АHO73)#i# ҄-lz#W,߾WCrwphq79fr(H7p Q10zZlhR_WL4{7NWջw䱋 aX/[L%ek|/`yb:4߷Sj   G|OiǛKn"jJikve aEHs_5k9c0(ø>FH2 (ŎSa~6>a-Jںd>f|4_҆zَ'/,U X#Ҷ%Ishig(-=>}-=/DIo͹ g iYX䴓ZvƮZ'⫓yD^cAڿ2T+iFi~;]HA>z݆]A|!k\/mnˣ4oɾm7k;ޜXqX6@F@` 0^AL'ﭾxѥlPVRLڻ3Nvo:لi1[5и4/S<A}V1Vgߥ6n_ U .#J K$bl\gz$/FcI .D4o[Q0nUΫ^Oӻ 2 \1>ݢ-2W{2VlُbG NSLJ O_?r1$`R(ŁڭBYH5n"Swqm: +ǵuD~+n$@$@$@$ sȣ(_= ,N>Y2,O*fN\3[\Hj(3vH!y|=)}w \i)hDRzwW軴0u|K@I}ډ^/ߪ^8_n%< C¹aIܤn1;cπ }MrSHHHH @ι|7:':5&:}s Ieش-zVng&V4jW'1:2lj)8Rd~Zj;-C˦N<>wlBLL ً̙by${3K |鵧P.Q?3-8 !!L, DѬcplCLQAD|=}.19d}}7] O*N9t5%^cqS_qV^9U`E$@$@$@8H!q?WY>Mw0W{ ~J݂fqZԩ!ds^dži8r=>OixҖ[[7O6ϼ'g@X,,.;UT5M~7hsըʾxr_FW=f%O[+uR/4욨 ECW8n|!I8'6/ދ퀳'/"/UKB%ƾ8ʉn,he)li!?)K41ǎg <[5 q!WoiMע?Y4.1IgJԏ [N䪉j{v,)G37 @ #-|R]$KOjк'l9?>[+t\]~]ZZeZubiiӌr[4oci^y;|s평:>Z{KeV|L_&oۥ4V:-ujbenkէوBݑcH[Voy&ra 0_ B?̶0uSxҪI4<~լ!'c1(1P(`)۝dpnv SexZ: jf=<^ s27     (p)Xk6@$@$@$@$@$PpPQ    x4 ph;{M$@$@$@$@' ;%    G@ᙀh2w .\VbBGs@`50-t,g6ZQ> K^1]kRQ$yˌq87>[',E,\3z6οSb4iAhM}o^ ]'`JSHH-<4qG:U'.8os]־4p.] G6c}EDuom 'zh!.VU| * 3uй@d.gAד@1Oy*ûD^ zgWSh~S]:!pINSzsR/) Lmшm #n]v(PHH'3Q*!U|>"3t1> JV#gK t} ~|mx1q;m3?*Ky'~Mf5@IDAT#9+=_>E;?{Dzǀ {r:Mw8~ae;>3Ťub0|Ӟ߅i? "rbB1Zw$>DĔ)92fA~U}xlMIuZ6F`'|13#;1Yiܾ X/|%FV_o~PtT xQudhP&67ʨYqq!i4(| GL͔eZ I8Ѝ Oƴ bP4nʣbaX[Pd|7k8=>{ÇKUƽTئS&"lή*,jX3kjuD|T<'f38æk9c !|5,w|QQO 1MiƼ1PM܅'J{$box$\4Ȑ㒂LEy l#jks)bȨ$j҄牲իqGЄtČB4tkI"]V|i;cE0,|-2QY9~Yl |o k{toY)p6IMrVLB^/ Yf҇=%m]2ڐ!`;Nᣕz{ #K;m F-Z7J_/ǁSҊ2e|t #{O>Ҵ1Z=ybI\b: Jڷ{+CwZQNgKmdo]+gIVeI[EJ}yٖurmØx(o+~D.2w&H sr9Ӟ Z4WSH]^ik=~*۰_a*ž/u"MꤷoOh]lJ~li=KKL~ /cǀQ/[x57,Qޣ`'fz1{l%>=Y5!u[v/^~GΌgĞQ 2_)lՠ>\ E@cBF$KN{%"?Ks6lg7d\'ggzX~K/Kާ-oΨلi1[lO?얼Hv4+F䛗t}?;N^8r.Y1~k #jOY6ՈVa@kt<|PfU% Æm,>E='и)PΗg;c1hV|9g_)ighuS?ɛ)i|ѢY @q$Pt`) CkX/VYr0GulPFW&]kXrH)%BW0h¤EEBYHlu:bٖ&-3[s2R4c3ij>9{ yؤhKp+r6,k"bj3pᗏ1qfX[F r]2\婫#~ӑ<$RSPk,ymYP숏A3R d—b9Cm31t,^HH=CQX<~b]Ep,ۢ>1W3 ɼ\k_ٺ8ㅓd'/x饪 UpViXrʳm!9k\7 NM,sf?t>Hɼ~>ӿ]zK^neӾi9[̞eSGfoSW#n߼1y{ʑs]mW]|zҗO2oLF-~P#?Җy75lr!cŀ|Wc]盨&G"~LmCT^俢eXzNJȿbo0L @ nOlQXKaqtVǾ\S7tHHLP.so ❔der'sKЂ(s}_N?aKEψb4YkMѲ-X- @q&+ yt7    (d S #C$@$@$@$@$ 8$͐ D%HHHHHD     u3 p 4C$@$@$@$@$NuF     pN@fHHHHH pΈ$@$@$@$@$@" @ :N@QHHHHHA8qH!    P' :#J 8' I3$@$@$@$@$@8QgD      A iHHHHH@' (A$@$@$@$@$ 8$͐ D%HHHHHD     u3 p 4C$@$@$@$@$NuF     pN@fHHHHH pΈ$@$@$@$@$@"Q9f"tŒ+D|0h89!ZHQBWx'/3/GTN?h4z&/q{Er}]lߴ%Bҗޠ-z1 ȿ}0L@ `49K 19F Y}cph o=&7 {7W?z|ɇ|6n1>%ʿ}(̿ʸe rT$@$@$@$@$@ş3HY0a-)3H\I"z_bܲݺD %Acq%Y@+.|źAq z׋_Ή>֖ψ+$zY?,~<ālvQtYyX8G4N\&`MͲ73hшm #n]V HHHG;Fb/L?a fQ/מkĕ,_ Zn_v68/&nLjma-w6^@e7dUAj+JH [Wߦ}~'f ~c6HHHLpDa钮x( rn/cj,\SO Vo䉁/L24)/T\aأ8>,۩Є{XmN 6+WM{n╈&\d:xqudhP&6Ѭ"CڣK +jcmdV"|hWbCzGD)-6*qq/U7ՐUr|Q1)x:~ bP4nʣbah0i7o.p09@̚ZĪ1%i,6]mt.U;>ۇ7F/F~- B㓱7|8V8GO?;"U柃#~Yl |o31Q{ùļ%$@$@$@$`\ҌeZw֮,$l^_s4? Z"?!?b} y4pk|k c!m{w>?`visD}ݢKF5B_c2!h_w*쉃Zizf)!ażyRqz(iS2 ZN7J"uFvE쒶 J}yy&J MVr@ii2*^J0(8ȼmUCڶ`_hig(}*O638Q{sX|8XmQc[r_J6|bja↜ȉ1` 00,=g+]ZcgPJ;>t<РU+wk?gNxӷb nѽmYH|UWc<‡?هk5cՈ605:=|UĞQ uw",M6 ,ן7^|l>$߼qw|E+p\"c`iM8[Yle>ßⳳ:h\K곹MxiWGzm4>S'n| G]2.ᓈ:F{|q?5D+6؋S9G8y35ceƪӄ* =,.쁤 ?]<7g@rNz$NgjuAt,M -_ C4(:tR D#g9 ׮SkSpHoˠJLOGccHMJ [9rjNVt9k e!c2Gk"bj3pᗏ1qQNᔍ.*,$=$–R+b㊲ױAnUM}aBa;#ͼ7%5-?؊S{#CL<=pImI$@$@$PW9* FobʦI7]:JuEk.2cxc,sJYFPv}eCQ9~${_){6+o﯊G6WwѺ@ĵ'0XHROS\a8ޒ+Ss^ ٬d;pLzN\BIYN]S#6>j|t.SO-~)3jJYKi vQ .g=\@eUX=i   + yY"b1-k8K{§U3?;p:IG0Z㇯DX^@iߺs >%bYT MΊEKѩH MB8"%g~bz~l|'Ƒ#I վ}%sbdH'li7)K41ǎg <[5 q!KrDQX߽+v7CV" Ӱk\{fsXo ( Ӣ^qei ױ -_FOxcp֬=mAuhY,ނO~6iк qMB~`Hߘ+7 D:%p^Zmi Zڌ]77q687(,>IWbbUOFJr69(sV*|%?nz7-gZZ'Ė!N4ݺ\L_YC$@$@$@' #?cjc`{JȆ 3ð*>1f (7  (8)!ggdLێ#,~>`%   G' $@$@$@$@$ 8)Xq6G$@$@$@$@$PpRƌ @% H::N$@$@$@$@E' Eo1 Y_D8IzoYh1`H\BqYZFz8ӛqLB#7vM C qLkR|gDlWZ+dz,Ynj@S`ʊ}cPF    JW@ZO¶BP۷*ZV/v@'~ōaau2uJA0X ܲ8QbyC;v$F NFOcݼuKi4t fO@\fu*x`_Qn.<-">,e-[[b9 8 +4DXDK )iRtǾ]V8#9+]_OMbgGE2 +_zfKp2yw{>RLܿ>wU@? F>7~G9c ~!&aSk&/3jYb9 NA3*ӀK1əN(ukzOkнj:sEDl:>W;YH.:Y᫸!P=".3$%OaYv-(z:*<٨:2S4$)8M]:i8?>RuV3hX͔eZ IU0 /F~-s>/\K&]mO:@PCJ9҄-lz#W,߾WCrwphq79fr(H7p Q10zZlhR_WL4{7NWջw䱋 aX/[L%ek|/`yb:4߷Sj   G|.1Ӥ7MDjժײJ<,5j^r)al;Q҇qc y;1|Q#le0Qh9l!}`[uh}XiLIEå 1UrO_ "Y䫔Bָ>^ݖGWi*} Gk;ޜXăeS"+c1(F1,9pKGV_u6mh~IFIY]{7q{ۘ@q)ؓU@mY30-fגv{ o5H~j6 w]ъ۝+y9ꟑte^)R4q D,֝L/hs}L^6iayoW#zE:w䛗tlheKDXw&\ gk? 1ֿ^qSs~Er|"YCL L|K~{@|Q}H^@X\Q:68X5$Wx*v?_9Ӽ{pneGq T_|;4&-wdhec|EZ1dH=$w,eſtoAŸhA0Ə\- s-|頔7(gq|?vP?g~QSN1v=MaV(oōHHHryEKں$_i:+_fT)U3ɜ+=bB{"z+IQv[bW[[Hy|J*ljnyh2QT3bh:>q%Q$Lu>^/ߪ^8_n%< C¹'9ܤn1;cπ }MN1G$@$@$97_8d 3&.%oǯN IN\BR*6v^գۙɤ,Žz+GĮ/9 qbi7NGn-q*dڎr |в)"S<ϝ%s`~qIŌ-͊$j1!"$vA(5tΕm)9o}]<9g_// &\kI)=\} ?`%^P YD$@$@$P'!+}sW ⇯-ov'eLzBf~0祻kqzl#3-䙆'muC<hSq?%g@X,,.;UT5M~7hsըʾxr_FW=f%O[+uR/4욨 ECW8n|!Ic|͈ZS%kLM̚%#l ?c0V)ލEL"U-1=;-'E}w& q\g&!.dNT\]56m\@fӸ쉚/]7Ν?UaZ|ٝЧ͜$@$@$@ł|}H{w,=EA.ںVojoѯsnvuiiiՉ O3ʵnҼ1yul͵?&>M44f:Ixmr|iN}zKX٧j㺕~وߜmylɅ\cĀʁHhm2Gך ]75_':qLWxnj|Os` 0c@\%HnwRsùi۽'O;:bF`S9|/u=#$zy4ǝ&  (2n$@$@$@$@$@$P S lHHHHH`g$@$@$@$@$hwHHHH N@ v6J$@$@$@$@&=<_h{M$@$@$@$@E@{ oyL]l/NW&6!    (pjnmD,˺ÈmAi lk77Z71E$@$@$@$@$%X&r?Zwu~z͇:?=|_3DzA)     EL@\}PelMZ¬t7sbѻf*@qõJ߹?[%}s,!    x D6+Q$_'lh/vz:з2     ')qpjV-;_з      %Px_ƿ}C^quf|QURnZ&;Rt%)]L$@$@$@$@$@@ᙀdkeJp}j_DڹoteЏn߸wChaHHHHH(TK.NY?B7ҧy9X˨,'siR?+lhHHHHHpMJY-3>?j$Tj0oL7*7X7cHHHHH(—bm{"     (rg$@$@$@$@$@v(<7,EHHHHH6N@{    (R8)REgIHHHHhh'    "E"5\tHHHH6 3hܸLm>:EhҤF5:5[F/3?~~6zUD^ǂo/'pé$30df ;C8w9v0V-x:q5}Sp^&ܵBH|ck*Eܻ0o!; TNa @& b):k:y."d$j}yyVQTw}>϶aګg֯ huE?&Jq:q(жP,X_~.<>Y;(~K%c1(0"2lM›nGP$:XUqUJoMGi2n/#jB x:Alh`7Ŀt=G/)g aUgh|ڻhX [Em,:$zD|Pvgˢ0蕁7ǭ)%]a1qilD2eY_ycڡ͛ OHިH WE }zZSEOFMФ㓱7|8V_ԋӐ6Kb$Vy.Nc8)US՗˓.Cs?8{Ro.Cw3tWMd|HtBnLѠMa؅HvMB$~k`u )^QGV/mt*߮╈Cڗ%_V   x vm%MYHSVJo1*m^1Ɛw{Y^NZ|R>mrnp&iE?}SI˻^9;pViq[s[HҘ˱)[zDV@ž?HA5\/ҷ?8~mhVhigw`Q]bC ƈ]Aa%v] 6MhĂHe+ۀU<ϲ3sfΜyg2sgR܆r~q ꧰"܂aܦi ȵ٧U?oAUnƽ ߓUm Toao_MlD}r.ƣ?w&}Hi>۸xTM\=Hj:3 R{&gXY8KQe>@}/(Bg}=;l- >ntCbwp:rl*̭9!Fasi r} L0滢3gq)JR]HD[JSvlQwJU/=;2{`|.?G~1h& f<%{DfZʔ%P8(2a 'f^KiE(V}%ee=aB$FoVwVG@u|Y߾*5O?nGBIȭ: Wcx~ZL H(Z9Y&RؿC D|rnj2CoaP 1l2d$F! ^w6(3F^F0^ÕbtA\p8~B*jYGqƴxQU%Tr`֬A\u=^҆Oiz@T |#aZdwԀtQeǤQ7)lШ(e[4uOc~rf Ҍ WWkM9#eQ.UI~#ؼWe=g R>XW_ze%5Y*F1FIo@JG Diï1Oe\5^ `Z5 G?X719;?;k2v/*NcBgHbT'+md.PU'>u%mlj;Y2UV*n̅ke\؄$/{y+a,ņ'e9E*VW/e}&D"@8 [ʎ2gbd[r+!Wd,[юv#ҳF؅(벲8Mk'ئ6ׯ{z{Cՙ1do5)ꦓǎQv">]GX^I_ao_MuK3C E2-#XD]FP'M+GU=/U)"@(:+ ޘҬ.lLa f$lx7 m܊΀u'>N$rAK7k7풰vPS l,4b)U$A[tV[Ic0<7'6p6MaCΈo-P2VI3aX|X]?Ĥ1Wؖ'cʒ\MTDz=ެ6E>`A=<* Ύdwok-סj]dGqҋO4)+훳21p#HPnUR@3܇߱0l.q^/k 哛J8~Y0_zJ?e XuZnogGw(K_+b D"@~ǹ#{BzUF-^û{p$B!N| l:|Z1=V(ʕ+Ob6Tߨ4oIQxH~:ԁ|Nnϖ W.cH;Ŷuۊ=?sjhMcyinR>_ᛞGkw Fkri惚ww}ݿ4)' D"P (L@ }mDp&ŒeOS+u"@ Et VU5N=1wBk؛T|"@ TU"@ D" (<sC"@ D"P h/Վ"@ D(4)PA"@ DM& E}vD"@ D@` cn(`!q9fqh5ǰpxLAMXmaar !x]r4~o9,>П-&*?KKG 7Dr)&fqK,<#7|mIN &]~ LG&5~o ^~O`^th as ^J_Ed`뜈;RUhn}Ya΀%<8h_9r82[WWLk$ J0=҃hh\B Yl7{%[x Jo؛ 6|ZU~bQ-TU%=n盁}gF16bj +w?IjAr2Ғ_W I_!/Qn"@TM@QTމ;l|;=y([ 5>\Eŷ|kGqoZ3{f80{zND;S~jFy *gw${I54o lL}{Sح8ӤM?=n?.Sէ/7x=Dmes;eΦzy>='sO^ gHM+kJAfXpD-8( H+V+17~DoyM.V?fW#1ڮ6j~>6$!:SKĽ]ۖ@Pv?e walL  'I'^%Flڦ 䃏ӪODQd9m>T?Y?zZ:͹lTP߾l3xp5<6WB>Y^7IDAToln( Z,+WDgBʱ[zƓ PlZ[ }k|?UR\$j`@Z~!ÊG7k `13=0"-J>վapRgѨ`ylj>ɷwb<L`2̆QXȚD ]1]J}L-NB2wu-vW'iy5g0blNW11>3_obqk,Z89 k*KҬE6t<9 ⷼLAY-}Et%H|@h7(8gh%״Goհ4=/CĢu:ðΡ:`D!JCmM1!h/O84߳zyuM_g  <98kFB"@ 5uK dumzI{lugucH欓Pq`*IƆ.kիێ8yFr .RbTCXHJF;yi0RQ E.'&/E~IjΩ:<ڠ 7w8n-xʭo]еG]<ؾ[9vg N% )S ? _(2BnIx|#UsώA4?Yf=l5 `|v#E<|x,F|)0+;_]@ضn6RyvgiV5{%Z&f$ CkZyPP?&ɻp+ .޶W [Õm|kXcb'eQƺ(vT11߀C|.˅Z4D x84舤+0i Di;+! >76fvqf_K,"q8x/ffw=M~1r.֟x,ުٙR~"@>>ۣZ+lT1~V 5⓰\Y#gFаKsimb?P,ܴSC8;?D{]U9u}P#%Jϯ븗X;zƒ0+Y UʗQ몟{ÂosB(naw/a.~wӻKT9yțo-ے%$6~Vo9,ݷ6WVzs;0XRg0,G6SzVYߖ@3G Kttr=w`0~rw0iet4<$o^Vu޵RqUSVq zgoU5b>-SMu=b7FQU" Ǧྯ-ZY~=C۵Dxټ]LIlN42w;Vuu:jG=tap0O@ԵTVX=[dg뾨S&5DO& 430\ fRaѿE{'vHkĜ ϶ݓ}&;gn06/AeAfO~TIdGֆW\Y:hik-m MΰDͪJEixx]L1;s얀ۚozNJ7`ߐO>˖KĶ#M=Q~l]+ic`@{gy;'tRlPzI.u ^H}7t2PFfPBRe([Q%Pmΐ=N mV^¶@-fTd'XRH}0D]6m ^U xr},M>$BD09a(к<^mK<iN)O_kb\xtg.sOΖ}W@ gȝ$ DHU(IH/[w?B,J,%?)Z&Hh+N?kߔtN! ߲%fh"YcFGੈ zI`E!hTw~DS'{,W ?EHOQE\NUNolנi%%_TLQ*^\g&25l ⻝l/MV]myE)j&7Bxi?Rn5I_ÇWmԫ۠*ZwǶ$KYǸ9j6 Wh$Vy" v Ul:9F2>. &qq~šKQ`&re&*d`Շ֨1j;Wk /ej4sc\L5(8ujHNˍDz NZrnζpədWpPV~WRdw$Z2]wmU8,Uruӱ3-~M?Du:Ӹn0M]Ѧu B ![-}D??>c~_F~n%d$3\.:TަOG:WPǀ`9a\+ XylEý .]fhL^^^܎/2Tr^zT;5ɞDxe K-Ej,BS{flKQ>ī%j<`sh⅘77dJeQϽ?WZ9jAiMZ4lAc_-r}/+|DtAI49v=3wD;py< ea=AXױGk6-v!& S-/bC`]p :3Ҭ j7d3%о}u3?տ ywxX[Acwkfy|/9Ӌ~6ɪT*Q?ؙn_ͫ8+r֩夂~NBBu dQ|6B5`k6WL$,E*nGq:eg?XRO#q:T':bultv6<)֌K܈EB;]e)g},ޢMM1ğxp^{)NǿP[~kO8; WN0Mhf҃N9r+=zZ4ٌ+{eqƝ0WynDʹ:˦p/a֪2pYB>:tkTmᝀ{W~m6GwMti;r`;!H8َ YE]gT8ӞLljiJ]7 _u@`ygumIUxS8WU@#yM2kC)(86vJŲloJ:%.ߍRr)? !"g1G*8Z\D[~f +F0L~H]ѦfVܢjK?SmQ/T[!3 axT9uUUOK-1Ö*kdaN{r1dP7-t tHJNewkGe>MXb=ɯVtݨؓPiF%|o޶l_6L(Ο}t\eY\pSyqPO[vPmMZtw6IĪtdT'}"Bw~] V&Gdž!]Q|wI=M*I]9 D@_' Jz@^  B}~Y&H}yK6f0Ϋ/$_aQ"Mʯ' /U|K>C wH\A^ߏ Wl|/LʢTu0Ó%?E|_Ds_") SҦt#غ5v&_ÇeS9UqgOb2qrYѿtT"_Wy=pnKc!{:WR`+9m򼠖C4O >6"  D"@@!"S- D"@ M@ bMD"@ D HmX D"@(hR[l"D"@ EErlMz>KجOAwDΘze=C!{W 3<_S\[E_~]'`e(&]RS"@ DZkğ{q VsNqz|:C7Χe8I|~L]omK>ṷR)F;ץן\U|P?K%mrǘ7FZL]m Pt蓤 P(T}@a Vas(͘E'쾛Srŷh^N."orN92׋0cvb؎zQ+%>WY>FbU|sSzZ:Y҈CqjI_HG 3wHUf#D"@@'}b&`-ի~C7 nePV c#'?ڑq 1ip (-|CbܼQqqćA)<lh~c*Dk @kUߡs.Px FDX$Vf18=u p.4g%Y;<mr)"@ lƨ 7feSjxᖏ[ɟMEeZÖs;OfQ( N],ۂfi\Pr2RbU!ҰT9;oWӀ [ J!x38m\4o=sa>Pϛk(k)#-_Fq&܂aܦi*tf(6VNLϐv&f|$k? tcH1#^߹"S?-9t|[YVK5Pd3tDOTp&W_~m~bjPoTlܫ/Շ*87ĉ8Q>@}O`!,xfl^?4QLAhd'{;6L*+*̱;?{ZO ao%l,|?FcC_l0Az_!Vəɱ+Wo(QKcdp&;~cDT6Nv>caLV457[9rYN drwⰶ,a3 \*fy6g;t豨ڞІ3Z?6.=SH={)e=aBXE"@ D0P؂%1fMj3Pe|$( %#"63^q^DÃL!L,-2#{e{.+fNOGjQau&!a@MNR[3#Hv~+il F{L9&E?Y? <7&.Q@4TQ\fH ~e:`̪ 9F1F@#741?S!Lȁ;#-œ1*U>3|V/2.gbY!H"@ EdHZVmCo 3"ؔ(2}%|W |C,;j*%5L*^45ƢTX1WYRxjC0&%"Ũu,;k;k2v/*NcBe-% eR&'xf+KaXJ3R?iӖ_֜y_XI=W y&?SSS }]K0,%MHBG$@ DL@k*<[Qi;V&e Ü^/I; Vh;BblĹ%ꪟć_a؆nߑH耮^;61E~ҽj{l ׌]Vd!8 IJq%UN"tpE^RQiؿU4zx,BɧM}`V>;/q1~omy_'oei tF䭧xkl5yWp p:a6.K">gL*d XuZǹugG?D9!` |dՖ_K"q~>^%~,X1y{fkZQMÞh+S̯}M!Lea~"Ԭ6E>`a@SS, D"@ ~}`,6;WdD^ރ4ߢ)^咔woUl6"'З m՞TT OCs&c[Zt&@] _ZrOK3dbyt8 أؒi7zT1έ'z->6J˯;ϩ6H~[쎆l¬R$D"@rN@>M}8BGUH @] GjD""@ D(e]솙'Nqa_}<9P'5F4e+eE0]LYuD;( D"@ HILu^z?ۣR)hb/V 7G{(c+'ӗר :JLa9/ENl ֥9?E.6jfMEY |&bSG}Zn^NC&WS՟iV{8슛=Cԉ("@ R]130"F.%moJqHW|?ڴ>,g:sfonW՜O>=PM;]cIܝCi,t_RT&WKORX4oc:[cªN!neyp:*AhPk1^"@ 22Rsr r@r6XgLY.vN4BY<>Wdd1F03|pKAy N(i}NGW@ L4㖁obgof꜄Ike|>U>YX"[ >~s'#kxn !؎O1CTfR~ۇߊ7jw[4{jLɵOץVow}/.u9d( qTsD)ͦqo5yxx> UWnj9eMgƶ=3d,C=ڎŵ2N=i9 'W:%nUw&}Hi n\py=Z˅=].QW>]ߕ۱h$W s!#7`979{DnmtKi͍]9ȾUbcDi:o =ug4ܓUr[2-XtNB&E]MZ4ojKeH~A(`ґi[>=w*53I]]]9sݭ=0* uv-u~o˙Vyxbc{e:o"Zu:_A*SVKüG} ?^oJ\ϰ7pn<"3-)XtoY c9^qbK7ץa<əgbWo(QKco1hk~aϥyoۂ&"wV(&A̟k_.qsq˩$@ Dq* FO%tUA`c*ƪy-ϗgu,|H;+O:T2ۼWe=g RXi 2| #MF+0;45ޭ0h3KI~-Vm;hՄE1~Rl sn$W#$"Wܛ"Rj7*\FQܾu`8]?L[#D"@ɐG.*^Mg@ـFkdv>Ŷd:%`[dMH"[Hju:[SY;>}ƑSeaT lR΂=@ŊjMk7D) a0yݲ!bZֿ,ݿM-ɲ!/00>'[!ufBih} D"ܛ/PZd%w?AmwQsPpj;w G̑& %䢻@K e>mMu*eWm;c 6LS'!^_riL5r /^eaΒ:MT6m##O%2 ToQQ굓xlSS z{Cܿmgv?>]GXf/ 3QCCnuQ"Yk5OG}|8+`|1j>\ojM\-#{Q5l^,~W^N\ۣdçh6ꃏR4ӶCusz:y.go_B}h N"ks \p9_'c9ی5MP>Pp@܂:Lt)08W{{ aoS#di 1Ů[`_|N&D"@ ~9@"@ D" ȧ`}ZSD"@ D|4,ةP"@ D"e ȗTk"@ D"Y`B D"@I<K}*J!*x(Fah7VbSkg |x#!O?N a~x?s8=^/v 1-覗3dy&@6t1wfbޑ|[O*sSL/qr9B9 wuϛ>~J D(8qDX&.8ۼ8ë'fSz78;>svPD̔L>x*ԧ2n Hz~3Vg)+&ס&em^?]ZfbhU8bӾG[xؚЂp}樅O5war:aSK. o&!i9A.k50=fѿ~H D([JqHÁc@naQImYǿ{>hv>ht$gA$㓦6:4 %=&NN/ohϩWOAh=U'#_I2Sb_bQ9nF\y;Ga¨Q6M>m9c9}i#? DE଀聫ה\IldcP [NFBvwv|2*jRtwMr4aۯL#xld{"p`po%P*)]G,Umת$NC1>;d, `f`^OLo*)KFbd:(fo(%SrC:呙"Dip [|EtD'e v/;?,ePޢ*BB:h|Sq.̱fTvݡ2]>| N[m_VIkzQL]?4bĸբ5tQ-Q*6 Y ',6<_W1K1R!kVcڳ󝂍Mw5:5>7a:gg1#|$\,`d%hlK/|c2zƯ_]%Gqwvu p.TP `(vIIf</ 2R"~ `#^޹A[1o "*PH|W-Z,^xoSHlg0=l6ḻB0O?n^V&,9dwsq` gUɜBҚQtZ%il-^"6pa Iq l}C__S*ۓ`3Vhu|Ya[Ae#bo- 3[uhlWv~߃ky;MπmaTtߢp/6H& DBF@n\,σ´ (bHFIpo+lp5[XM9fed' Uf_ZP601CF$ D_$_mbv*@.=3p>5nsם yФ,쮭ke\ձ_ٚ' `c[J}ur]s=z5DZ&xw,ۣo_^bdDz}(phCkae{7Z-_mv{a`bl+Ĺʑ-g euUL/Iɿ3S8uJsJ~Le8Cۊ!wKs(A?7ۡ" #)QMÞr|Xmrp#HPnUR@ӝ<nBMfuT%6`i{[gISo>Wa뗑d[prus3t&K^&ݰKgDzX UȪ=rr{岇8&Ψt }w1Ȓ;s9n{dyLcNվt`:d-tome[}o8J]w~nH8SB)  DK_/x\>\C:DhӃzfƞܼm!9¥gd#&>|:mry]Jj>\ojGdc'0ֺzh_'r&ͣ^ ,ؼ8oO92ʵ,I`jп`[]ЁGo?M6d6Tn[j=idɈ> G?@=.>vW`ytVWg1{ʢ)(9vD4a<.-ex)Cft5 )"@@$@ٮU=0$i4t"maG2H[9IENDB`structlog-18.1.0/docs/_static/structlog_logo.png0000644000076500000240000030077213100107304022220 0ustar hynekstaff00000000000000PNG  IHDRnr iCCPICC ProfileHTǿI/H .lB(!$*+XQHYQp-`[.Ȣ*;#}ϹsΝ;_ G$JHf|=1qL|?(kJDմ>٨nMy.P( < 7㨵sE,Ԁ,L |hS> S|g2'"!G3|'셲 7C99|e[YM/oK q񿕞&~jd_U5K0!8dINEN3W7ieai1&ĸ:pu݋fb l*y&#%4oJS1 H@Ё*'vB@$t KJB eԀ:p-8.k&A/ CTBZ>d YA,0(B+P!TAUP=3t:]P?4 LlυY;G`> y9/Q dh#f DB8$ #F6!ab0N?L$Ĭl”a0͘ ۘ~;Uǚbll ]ccO`/b{83p &\׍xU)R!Y- L"X|q!appp0FT !Dq9q+FA$I$gR)TBj$]$="#:d|K.!!_&?S(&OBrNOyGR n8ju zI&g.ǖɭ+k%Z(/.X>GX W DOjr wFi!銛*^QR+(y+j+ .͓ƥ.8!MOӻ#J6Q˔˕O+1Hclee2Ҙ>+qYn2[M%Q@IG*S[5Uujc5|j{.M4;`au05G545|5D5^i244S4wj֢ih vjzTf3Ә% mum?mvvN:&Ǻ$]nN=- z z,d 6  s  Q\2YƩ{o&&&&7LaS;S99ss5e53י77ns[ZYZK>>WmWkS7]7~)_{Xx=Nx|t\xzxuy+yGzy?4mm`s{U(eOMŁmApЎG8043? [N _~0CGֈFȎ(QQbƬ+mE]`ׂ .2\lѕj^"X<6>:`WN3NHzrws_x;yÉΉEϓdWOAM_JeԐiiMB%aBfƲn(_ԗ阹+sD /$$Yttع.5 v.4je˄ˮ/7YqV`VpWt^ve*UU ;[3[6u,{>z}[F^n?4npP#G]7n^+ZhQX\uw͖K6oIҵnmmm]) Ѽ`]Kv]))M-WXZWkYrYOGySzƊ{x{nuXQYXe`߽*ߪj\MvͳڨΟX?W_ၾ nm ÇypkYcU8"={8:x\x ډfyyHKrK_klkImNm'~1)S姕Oo=C:wflvQssK:9 ].^s|{ΗO]qr*j5km]v]7ot=-[n{ݾt}ZOpOwodサ v̓csa۟GbF߈ߌN݁6;FCG|H0ꧺϬϝ_<[8b('%Xh7 MȓIIP@D./EjnLHn?$IEA'Mw&3>mm3f )>sou_*iTXtXML:com.adobe.xmp 641 755 Nɞ@IDATx}|ի-U,˽ލ1Ř^Lh! @ B?$@BH(;؀iwq-I%Y՛,޽۽۽{9n̛ܽyIE$$  VJgA@A@4D  @" J`tYA@Qe  .]A@A@@  !KA@A@%P  a(a8eA@A@D 9  @" J`tYA@Qe  .]A@A@@  !ag  'OR^^۷O{PQQQWYY֞zh(**)&&Fx%%%QjjjW(##C{QV{iVg!J})tH" |ڲe m޼mFk2AY޽;effjiРAԥK`%  D Ҩ :ܹV\I~AP9ƏO}u[7D^A@ȆJAMj*W*|ܑb8n8M!R8zh>A@(N>[xZlUWW_ IK&MYfi/  VD tȉ܂M`?>>:tM-ڮ]YgE>}ٲK!a(a5YA3қoI . όrU={6]ztR||.A Q P__O- h A!kEꫯ;"#%5J- `%Z%۷ӳ>Kϧ#G@bwةS'kn,wwFB@N +3hx޼y4w\l_|" JOAp/O(1 Q^ъ""Z߽"Z'j TY]GxUQyUw+k***b#gnf[%H#FDQsܥ!G^{-h:%RZSm5/9!&h’J+(;:v<8ao+;4\aA@OD q^(w}_|bGg#-2:'SIgz/,}SN^1=X ӧkc8B Q!،ҥKOS?eeR)ԣk[jks_}iy5*=i{nk) {4y@4'm@# J`tjzY=Rf{ߣ}H*}fm9-ivs?""Tq{&77@X# J`Xtޭر.-]}M~WfZ[jժ]Mޓ'ORn^ qX{˦,pbzz:effzeddh_r$EW A@@H*Paav o_{;<3P_FVW/uPOBoبBQ߿? <  b( AP(@pJJJ,![7~H7Nv@;j;N IGZmݺ5KS rػwo# 8Q7$" p:p馛>.m4od.eeRBݖ)KC173ƍGC 0G^:fD t!c4{dVGz6VT)u0ZO`B Ԩ"[رc]v} xD t WX}d8,ȠIPIIY5}n-p@p&?P 'NHs" NN-}u<piѢEȊgIStkY5Њ{ YL™n3cbQ)#E~fH@! 7@_~9!N # J>6rG^s5T]m~Wg@tٌԱY:8. zl[hZȈt!-MW}rJ)'*QVWD+h޼yZ\B*‚@" J`tYJ70\mTdkg7x"sʍhh:7EsaDF!p!8q"-&:npP9aE~wmTؘ;Kv+!6,Y$ ,{wnf~P QBV Yhɔm 1t ]k:)̎F7Crk!v;=VA_(efBx~oo(N i͚5픚bA qt!Br$ȔYfA H5O&MŽI')6 V*W$|G<ÄxB%ɣ#9 -4#*;iJx"O)$!aK†'hŚ\fxO=[n{K.zlr]*~i-444%\B[dd<0u#6Qp"xMxkhbZjmk r7Ѽ㕖,$8Q4"s顇b AԫW/1g!&L@R{u8I=Ba/Ew[ vM+Bu_.D@@Xw#GR۶{@SN5%/Sʒc B}vՎI#~ (o /"_>V;IiP ID 4  =nculBbVL;v,DHUJKKiTYϏ@u@@8Lo|M5~IקOzŃ/!# XO?{ӇF9 (zB`ƌ_l;860PK` "07^x  L"Ӯ ]D ݱ1@֛nM4eDJH ;ӦM3}|qڷo9r\__ϖQ$x#zFFu8^d FԠgɥE+v<,е^k )'B@SPțpD4p@šx8i*U :l0oS&xY"L )$< -%fٰa69rw@NSTZE})ȇ`=2xDG.rPۅEw\(͞6~,l\X@ `b>b^ 5UGTo#}DXÇӊ+0F@0pw |"#ZYNAō쀴rF a`($ O<&w-pז~/B97Q)f_ !R_p\¥Yzerri<ȩLx !`E"Z஭ Af3]? hykc u h# J`hH˩x?klSѡrۜ),!A X.9yy~jZw(VZ N"p"a?ܹs;fڐ2FrwE0E`ٲeG{?w@8i+Uqc\dghN0gG (000yڜgZ஭uҵa4CҨ-!3$&M˗JU0ר%+j~gP? /Wz;^[- { CiJ4(͊h[J+Ehd޽;!6nS I!W! ;.,*jKi}$~XLrrl `7Hy%nXnox]8q޿?M0>PzQ="C ￟ݥ{PbgގIj^k<&\xJfXhRL #yit kjc:ۓ~yhJNM23g?&Qhõ\z5PzxۧͥRc_g={TOb&|DMbc p(E)q O'Rf6Bs"-b(VFkOkAz뭴k.V9Rv^yϬ*=="#%evܙ(""W @5!!`GJL=I&qIH>WP~a>:,e@ !3lٲ rHQFItJ Vߵ/w+7n4J> !UV+z$ʼnGEwG+i064o{O;^{<Ν5H;4\;i֬YB(;a۳糍'xJl'Nu\O9\?ԛBn~P".P/pmk֬>[ *mpbDH#+,TٞaŽ^'i8u   dŊ-vmM?{9']u 2~ΣJxHQ(*`˒ͪ2Ve vu>&A dh][y7a>;TP\ C )fcҥKϧ*9h(S(  7M߯;MSzF߷- @'- fq؄Fdu)Hc?5|ER!ZxWnv=G*%,!vR外5K 5[>)jc#0>H<{t.a8ŋ骫b\/`7ڍ0x6''ިi,>+4+QMD;˜HRV$u#999=^q湀뭨rJ@Poha/)j) {H5O'LC*Orik6F:=tr"i2FnB{97ް| ouz a^iޅUvjQzz:}aYuXvCmDø?¡Aj"[B#P@6nyHS&C^Nzư(\‰ް8U  P|Pço_Ғ-h Ri8tmܸ; f L[ZQie=+@Nٝ~ń]CPE-p 7;x#Pu"hɓ'OҏcÈ{%нc'7A #= Dy [̻;FEۻs\8rP .m8Ev90Ysz8!Z ܉oEo"u!h"Vo{PLVmzm>'}RRĸPrC~_TXXxf…8uQXA]i (mJKKsΡ)&A@@ a:YZ~p٨4k95BD|so ca۱N$PlLkk0ǁ"0BB@@wHkײs[3NЌ\b"c: ksc<p^BW= U*̶# JKv#|rVT$ێXdDh>]vtH)+!Gs^s4/XXsJX3А>NN>װ{%=c% l2;_tpܢVTU~m۶ Ӽ~"Ox/.]9Ԛ?>{^7"n榦}p%08K!ݕ+VjݍgΪLZ w󗗗k~TѼ?sp ؁uP]d'΃P):*ndyCv^n(n5ZS#G/]sĭ]2%$=zx@~@uXY]G"`11.n|%'DŨ /8ؕ -h RiDmb%Vص#}4vksQUU?(]7% nPHmO?tŨA._[^(YZ ׳j%G,2-h¹tLq'_u8>UUպޓp@xt |м  &l"{D jD oqɆnݚbcCc@|QZd]xos]0 T7CG_|rYBkeM;R `9p +JS\j_aG+[pF?8vFpNc 9ِ@?Oi˖-s`%008K+6 gVhL]ڇݙ 0U%~›Ή1K'EW 问+ u!htAr9wu*u<^yTUUxI;Q2"qGSG OePN9>XC/rS_@N \C.9$G˃nE60Qb6mbڵ#U0BGp͔69JSc}nrN];,p]r K]L)Oͅ?>}G/! J`fE`Æ $ư@Q.`N)!w3&dU~z*-ugd 9t椖x/o1i%8p~xP$ 0B8z}jݪ]?{23a'? _]n Q%AD_{BqS媦սŴT?ޡl<+9z_kjkk C=nOW ;rl!cJr]4ͷ9HcAdQ  ko~C0M[Km'yWiޟ~1b%-^PRSSm۶LIIIoX[{lS?Бb`4QTq] nݺ5=(9)}Mvr~_-RT]^Ous %)CfJ6tgXoN>(},~a2BNJzMi ¨PRDNH&P6 ,4Cj[ۘ/ψ>M$艀;5z tOAN#?Ϛ~goڃj'P}o];jY0T!o8zL[N(].L*~pw;p!{_H{D뮣*ӵpwL7FkBeAԮm=~EK'͚z+C~A̒7o`O HY~v 4{Rve ,+V8LJK|Wk:4L W+<2Ҽzl^o8]n*Q9i}DG./q0҆Sx31N@rKcz@6T>E 0؍K/D˖-c7sѴ,j⯐`N%3rHNk$sʘ%2py͚5;%JX+J`X :[VVFwu[0WAF;ܝax‹-Z[:+m,"@U0w=@bK36 NxO'NARoAYr!YUVꉑwoRvzN~bWؾ;"ގt?b%1cnimVl?65:w]b{މT 0JȪ";;.\b3?i׮]V]:GTn`zH唣G_zx8^?q%w;"!\v_%t< -<4ah:u쑽}[] '9)"Zxٹ:Ua# Oon!u]閈a{w=7obr3Q u Sii)Bč6CV=g >^)W;q0P p%w;Ҽ ""W ͛駟ֻ{+?];~.Ef-kvE>E[†R cOo6t=s'Qda:=}ھ} aiQpЃedFxOSGfxpYymDnjĨ]zV[ߙA #'P GKq9.q r2(n ŋ٥/E>ξK*  e7 F( ^x۽P\A_Oשα9Q=1Y/ʎ`p+,J`x?#2S F:@_xRTRg Go3v w=b}c /;uT& ZB !>N eU6C ?xP#`Gv@D<;"<3eP(<$`.M֝:;B> J w 9%++& ?Hz$;9k" Fфa鬚 魷b (2 lEfhĩ0FA^RX;^E%e Y%W^:>p?sbb=rX,[{ Z'|GyQ#CȐyPbtNM$<%rhc6A蔒F7l^#p: v}HIQ*+ JJ،#׷qjZcv 4yDXa4޳+o7p0cJT'sz5ͩAN8z"Cڵ_Baܭ$e% !%J '2g3HDksӱw:O ܞ+vkX7(SiD~=TdojH9xOBaT]-&@2V{TSyW98Yi`/?fWcz%^, Wxuh6oGf rpuנ;wJ}@Fԭ#/QkNF@ nJo&  }+3Z9ZbS{/zu/x^t-mr$A @2>| J#-H&+0>T;=T]SO*Cqhu,YB*黕efw__BVS<_vԉ.`aCUjjEݟ0e#wLװ |{ve@IDATW^ӧ;NtT JٻQt^sHN>ő]GB@[oAwQ""KVqOQ|@7;F&i*}P Etq:xL{/,:AQ(IgtI̴7mf<\N7EE//%48Y ȵ62C[f+C})`ww^R-AE`˻/:i;ɸLeewU$ŇFrn%Pw|`# o콅T>_{5RWl<ȩŴxb={k;D 7S ęczݦ^#UnIn"*S_(IR8mTc}+k\wE 乖Zq@蕕Ƃ{I^JR-\:zrJ%;ɡ8㶴o~737hVU%-\ė[G-5*7r_`믿ER|4M5anؙ٘'TeVсo>~ۊ\l uDmr%u!sh|jzlR>Z9Tl.\L6o9Rt qFS4j@V4̞Uq0+%0ݮ;vvKv=9}Z+\%cߗ_(9動+hߗSysG (! }vB NpVyݥZܥ[ S%Ǫ\Xiƍ\vvvRk 0Jvnz={?b2OO/vW}\voGݢځjuT\> `f٬[qjVBf Kn7P!Uڷn CX}SY 6] ր`g-4ǾNk>߆a _vl[u5U $MR9%rX]4b={~p ٹs߲ȄVº:4;q(\Tp-XMmRUUD=0=p =;@?E|0[_7 6UWWӱclC["b.F)Ej ߋ_AcLDQ֩*0a Dw0^h5aP_EE=ӬBIBM0Zٲ,[˷I t^픇l+ "0!$ bRN4)0 [xm;'HXOXW:zuHI ؛"/Z:a&-FJ߂6S~:KwWszUgf'uꋦ,m*4BpwB뮻,Xrؾ%0l|ǟ{9BvM N@CҵRꋖݧe"8筯v|?Su[@(`(VB ^=݌-CPN,%pρc)p6Gvѧ*5U[$B*gD=ws Npպ}79SK4e7c =?Gp`F/![8=~=P W9+l׬YC9f'a_̭J >=Y("nWߕ1titcw?R= s&?d kz'^pMQ]2PNQ׹yWGL zR}jl ٦SO9ǟ@7|tہ"jEA sui}g[`:⒧c@ shr`C|Fv}3Ȥ~6?e9/\:Νېx8Yk>(0?#,; d)>^n! `#@4`h7A j-+Gv#a]b}zHCs鄬G6}A5p?tkZ9obw0js 'o!'J`H=oiҥʇ)/?:2G~T ;p*22үq+vmhg eU{u{[@$ d-Ghm)Dq]:?%'o>B`рH?8}D t`[ԇ~-BQfZ[8啵ʨ=+L!0eDF@5zH_@ /e;)BqH)X7X?zĺ#؈uLȑP1z(aaWaN +Π^4R&*sg b'd `FQ [Cp7x%>zusg"*o3 g]ᐱjժ! %!he;wLjgԍu%z ܲ `?7vW!#Y&RVqg p04P\2aNG@' d׿]FhaULk%ib~`W_ %KEfڗgg fz!NZY]G96,0$GkqXfB8.Vq;?;W`Mv ۶m3 qظq9cTpfp괛Ksaϴi(U=**C/"8٘!8~`Lfv#\6U)=cW%_xvp*g/HA2O[D̸2WqL]8UƐ7lx 279u%mփ8zl#-hZT3إnM9=fvf,Rٖ0^N#hj?X?YȖ@P`' >L &>DGc!tyCf?.;?wrTrE !C]@:mTTۅxcq$ jT5a`C؟8_~֩¢$چx \s(*ɾ-A߿pi~4z`Wm9[dsJ2D…D 6ϲ2zWY%+ UML)q4Kyqic*0[o(Mi(PY iǑQ#GccweL6.ugޞ[HPr<# ~[Zm.G:vuV1H'elÁD Q6Ǘ^zmHw6B0_l*^ \nȗ9!֝O>&NGѧ]/feՅu%374v,6LH+6]&O~F8(Ldf_WvՐ~SE(a&M2\~r=)𵓰oiu;B`J?GzWaXof ;' ’Jմ7}\q͗U}B+h&`a>qhTZZau5(>3Y 1 - v`,a 0 VUtRI;|ݥ\M[Ϛ~=bcu%aK.t=NS1 "h%ҝWe1Z09y/4_|`6E (iBJs~2D_Tu*<3iN!g1Dfc wu. =?rZ_VPxJ+gvi;*sSy?}T&;Osyq  C~*(?vr!r>.'<5%\@:<<ȳI01﹄AV̹=BRճ ˆӪ1iB}7P@;gK#-N8"9 ٰ!Mu5uR!%QETRRbyPǫePN\†d5qS>;ӓocol#NTR(84K?qr3͎=}]@#***z&6#U<ca4-vG`x-.f.@J6o-'pj>pmtH%nW!3km|T̓D1  *g8afɞof7"!2k el84vm8h2"uSWBQP08`jN>>5b{jkP8ר[vi˃y ;@s.a`A]'QۤXVv$ Ei SLxQFCt$&FލQQfn5 bȖV}:" /.{LZ**LA |h , =[eسZ]!浑"X7X?vKxG ^S /``P;\%Kؚ1(L6/^̒j4W:ӄ)6&Xl#{?ΝG!Q yV_~G u[Ev,m|z/a^s }E#NɋK;at[Nm1_Ra{ n*X% gz 744hNh(8&RUU4P$x}tᮽ(|U nO['vZ:p& _aߏPNTzq:v:a8pQvS?B<pYqڱRԱ];Y Iz&"|z'{5=l lng)B**Gqqm~;\{ײ3Pxq ~gLF& F u7BKxG[80Ɠ!QS~Z0?rժU~ƸxF2)ax@a'CC+O0OK艷5ZiP 7pcy ]V C =+g7ЗM6Iyk!lj9P EWXs@ԿpB8( 'ퟆXln $વgo8)C??Ѻ5|H>eYEh. vLn໘Fߍ x!sԜ%v[Jݧ vW啣)"e ;vE{tm˪4V62h#ncEY*z5TYm](woI}5;9(Lz˄zZ)xQNN!C8Rw^=Ʉyj!an +N 2f 18<XB00v;ު>b KD߆KO>c<`ܻ+s:8h~c-PJ?_F3PC2_e_LD-^K.[D D pJmb3.$ 9`1<[3WF[ #~O+rQ]8hVe:t:ops:Hu5*[Lv}WS|(P`psN]%W~hNyjA &qsO~w֩mc[1(G v$,J]#PFTv#FlUfh?Fѥ3o@;%,]YYIӟ^+Wҝwi 3zПn%R7ϘFA}bcL ] ܫv2֌dmi[.ez sl^-f8Xlޮ( i}@C]Zy!xAXKGvOju4̋hP>~sB98Ę2xs!4%O!ba̙MÃQ7[%}- {Ӵ+c}ju1oRavn$mkV6~*D4;Hpsȑ#~z+xD t0'$vM-[j`hoq-o#Sj8QkSG_#t*:l`wJ5L+Ui7np 8UN?NǏ'.!!^zu.?~㉆-003f܎5±;%rHhqK".4p"d ̴]29#J躠npAŊ, 68̄g`HU08𑗿6tly#Yl;h9RBc؀72׭ ૬.n#v+˴ExXĩmGZ5EMa{ j8+wv NT(wBai 9VGhTN+D4xKS[^p͔mW]%Ue8^v-xKwZZ3:wÈ2FL@|a.VG?Wto?.mP*4lq];n熨*m]E!EÆYIb >J70b^c~; 7}B`*^ز#/TH"7-$ԩ ,EG°'v;C5kPm-(AGH[Ч_5$QZ:nj_Fcy橰bE;8rE! ޲#13浓 ?GOF ke~;JC fz[;yC D^]]M֭s%0dS믿fuƍGr57_7_uxQE<6J8{-B<` .60_1o1D#^8 I|~1KA6NNݗ,䦐nI@I󩠠UmJ >^70 󇲃y(T?f=V|a  <|żua7obw.`^`~`!KOS7MGo~3Ms9ˇBrg /7Wܝ,)y~&]86iJ.ޟ._;viγc_q1@ 2b"rc[ D(h> qj)c$Srn(QAN ǻN?B=^@=LSoPo*Gʻ;;!c9`|?=z*(zg :klSM^:pBВTW@۶ι~tV1@K8{gI6.^fht^jBr%or #o p.pnE ܘHK!GHǡ&Bp굒A?\]%lfO淒?w_;^K|bJ7L/(2G=7cc:k aoV(O**d;+/v7~(Ͻgİ*ӁY<Ο*.DG͗$< 9 ssS::mD _ tgm#v2ݺ |ƪDE+H ^؝G(0Kۦl UJW1+JW2:^َ9XN9݋IEй ,&N>Ff rLЙ|`s^}mm-!',E< 9\M҉-{xϾ^cBf& bGˮ#F49WGuR9rJ+_IWш/̐VVG+wBtƀ4^B@YC(.]9E twblM PȾ?3Us<@PT *-(`=)2dLc MuG~BAaŗr^S{8}`Yn*r`q $/$!?țFHMB wccnȶdI,w1kjl]vΝ;>=ssK*2?|? , f zk70D~<(@_zquU0(W[u/x֊w$_K%qDa VBh2ӢWJ4LprţWz*:SGe*wҟlif5QVLEk5P UWi55:n[ğl`m[ٮ?RÄ@o @}\>#XEè'B bPvP__el,.g^:V@QҡRE *Iƃbr^}V<rf="ˠȖmpga x`e/=Q; Y(VCκo3[&0Ec WcYmFPx'$x  IgNT&YcRK'Sv!(p=]7cJ{Q0YQC=hVϿ{7f !E$r[n:Xis~¥u 5x,566)ck)cP)sL _9yXvڇuWE-">s5bVWc~xRߕCyb%Ыns&!4\vz:"}48'QyA|>Kц t.hm}(ICq2lMb|k[V 8w©i>N"º @_]j֢@w&rqtvx ˰ kvcD ~DpRNPݩc[ᾉ% X$c[D/ ނ< <9 V²%4iH=T.(xHrWڑ D|M c%]D@EWP91v|_B [Zʼ*CX*9Z̀"(v&p A -/<pV=ba; -ǧƧ-ިBCy(THOT?o@E ("4@YA•/qjR+"EV(nNOORvԌKAty5A qƩuq6w4rHZ~ꅡ"ӂ>"pUe$T@ׇ؁i +r4(i@\[QD6P51쪃&6]Wv+NU*vJ En 1^Bt Kѵ~5P*0uㅔ@*X&ʎJWJ%5P9cSVBThlw}.V1R=>xGU89B8#R3cPȁ,LXŗl*,((*Wp}`Ejr.9.-@91_I=D@М\9&KdydTxUQB7uc%y-TASCO@D:- uD1)|$')N7)b~gƎ+lٴXlL):SG삔",9/: [3>ETHML,-%|J%vEg(q"z>2M]]5clD""RPZ"s# fr^`.:MKU E=D0HKh"+#nk?{Z甞!4C\AR$](9iBKSe wҩ],Wm,#2dFp'W7(8P=bR Ip #WݴO53qѹ17~{HI7,X s[jNCLd7~[x²DIIF jmDD-vprZ"s}􍀴iD`ܹ'u[RQG+h'4NAs)#zjr<%ϤH mwP y̎<1W0x'{Xe_,j i AI8#㫍 e<)!4j_MS@ȿ&i`Y"SDcDtDiZkKP!0$oZebo\<@&+?xR1CO(7 1{rmg PvN+|EZcø-9n.XQQAWGǪ-!!!Fx-]q\t}5ҳl8Ac`O!Ї u}E뛄މxD\)"]eB )b"mO8K%P=(%%Eb#ѸA5vD-{#1Mxz z-Tz^Uew&V4AMu j[)SG_Zl^DR {fD?4mκ4^A{&f P\G#p'ՍDbN(WHr\z-x6s2"588G,ƥ {xA=2?-@-hxo.\(JPKdǭicImkb1z-TvNZއ^d5 fR ڊuD<R&5?^{ӅO~ ԌY4Wcư"ԲUp3}UZqރ@u]m' _$D@[ JM)F 9jG@@*ڱgx(z7Hau-x=)GH~v!y):;[`f'+-0 j>V:[sФ7pvzj?o6\V٪ KŲ9dz ,r.@@*.UMFկ4M~;!Ɠ2Qd+[dn hOSW lXq>iИ`ƍXRQOH =(UO>MAxnr_iٚ&paL!INGoGivOV%k>) FQJ|1di3&7dŻrzn;J蚶(qDk'ņ*HVt0SŨ.ZTTͣǥQ{nݺOÆ 4IC4}L4?tlvmkZIgEwR,Vd#p [9~@jC$E\?fa)=#NX8Vz^5PkJog'r˗/={jho|ORh:ɝ8sB汔k?!Ni7:xmR$D$[ |vsE0˦mXBECQzXsrd9a_b+zAR 䒓iժU e{9Xw8QVm/ثTY+|ˋ$SzO{  Q[oja^*LK &h\]-!IK9"rRsy3+<)hSȗxRx9*V˓koS ƫ)oE=BzSBӆG\:P%eΕ} Q%PZ}wۗV\I( ׋UjΌZn0yTx=kb @o}y'Qv(8tdp;Q?v(|Xt}ڃ#X⹠]`!ΰz.`Т iYhyr{3j<I,sWC)))>R ya^uUg͋X~!$O"''F#/6+~t8HƫШ&iJibRVk {ps`5LOd?f ߰G҂H[J H ! JKKY-vR'vpHrSiÆ GbO#d֓o\єU @IDATFO1>mC`8 G3\Pw+|Rp+JK=PtvF}m;xڡ]=.KɌC3:ƇN :&1"ȴW l ȢϞ=kW(Ug:|rZ#>fv_ҠyynrO~c^ﯬܢAy1j)23>RFzeC l;zk*;~!X䃆NyAawTS6/.x-zv K%k?~СCľcW7~=-ᡁ Ƶ{kڭO|\5و_s?GvV.us-mh?M.KLmd0 c<Ǻ&;W; 21;8#H DsҕY `;(XE`̘1V۵666)?]8>+*9a=ti-%{$Q~;[7""4]=rg:ۏ1/^KPAMρ-ijiuȤGdc`TU!|XÇLXD@ I$,>EZ`|#22Z( vLlR{4Zό4SD:a{@?/ތ'Hx9}ޡidi4֓7b_zV,dֻk3uŇpDӧw%ۧKU/3pzŒfO,g-mu Б9Gs_8 n9PHĠaϲ!8"ήpв|,G,MvI W\wQ0GU@P VTNTuifnN@V0uص>]jZ@ zyƵȤrGߌX-p L㮧Ծ6݌\qTS~Ҹ۶zѨ9QUq:u`5-:(|m$ٺ'8 _1~ebZ 8`]VZ*'kb mkCIw rG@jUv.222(55DAjn_q ?7wjN!W Qa$yKwxۮ̡3(sUZh }($$>C%Dr\a3$g^uJo~9ZJg{b !ŠqD}.3p#CV1|mڣJEѵ@މzX"0F -q;:Suu5EGGwe'Pk%lHX[bnf]Ɖޭ 5 u$9Nށ<'6<(//ץ(c㳦YKك&KakBF^ZǙdWX t6/CQ!#tp gK믧 лk&''wA(kd,qQ:`Ŵ%ᱩ4x]4jPA_g.80_n`/SUۻ'Jn9R̟֏3?KDb(43JDP:Ng'&i .ىm5ReMjeW ȱWSXI>:Ӫ̕@fr0*5,5 A(2wGuNgG65|B2D%fع#[0fmԅ5;ʵytNv->aьU靕83Hh[,.bZTJ=E Ν%,R mI JXYiiC'*(=1BQnf L}G( !`֓1spZ*8\s42vGLy=TvcEUG;($؟" JeխHl]v)Jp쮻_W&_1wX+KhE'4UDe;\kUDθ`."R AIZ!%ƦV 5zzeFsh@D׬T^@zQLDkri&:Ċo!_ױ밐J #d}fpGY%L{ak5xƏo05STWY>5ֲe3(7*V@3skF---e5ouj28x`B '^fSѡuT[;Wq܂ch‚q ~|a>jժKF"_W7nܥ6Fscbs+]}Pv-?nPJ3iAgȈ~4oZ_d;c n:RhjVa]g5П^ݠv OvZl_L~gh"zwtYt7+9!C 88VXAŪs!LaKb7-41*~<Ꜯ3XRQǜk!A`Ɖ <Ѷm?y RT[]12mΜ9hJ[CC-]-(!c0 *>Sc1aMb >3>s%%w L([svw @bD F 7*elp7)ci2&ٹ曭dT=]̙3 ڲj{uHHg ˔b{8Cÿ-oDĘNkv6R+ckP# 47P)uBG!;دil >X XLbhRޡЎ'bcN 8I ܏o|8p*Df9tgU)?v&׮ӴdfE5ic/,+6<4w`uONddUK%PowċA/ `QA?pFH(ak c$1jNâ; i@^YۤLf~!HBA"^@yh_ aA8@x6mɏruW~""_qRѧ(L8CN^?rd@?"'r@,?I=aA~p= 6'yܳj"I롇R2&݈@a+AxWɤ9LU ?[8=w?*2'!eoolR[W_K3G(WZwoXt2 ^ 5r رä~R[MMLKܸYOG{ōS2VAs:OP {(wjAR$87ܧ&B8IwMHǻW_U]#gR{596}̰"8K`Ul\r+hG1`ԛFqL> ZIIIL-Mڮ#cя~ ?ؘNSܤ8u9۔8J(YS,Q Kf˪1\ף]i߱Ml}uriΤn!ut=2,x \{m`e!JqW,B}Z1]C 0"⋪Ӗm@pguM7$6(.לNFvH׌Ql[~7Yo W A LalAK@DrEV,^n ӐqBo}]:\yZ&{l;V K 8y#?NsgSpV8\g*@B}矲ߙH%Ѫ!`9ЛH%Pow3k,~g^KܣьYJQ.;?<{x}2)@iu 5IKW,qp9;*P 2{ ӘqWQJU5U09=b-:ܘvQ QE}!)E"`TDD7Qdg3(y4gzT׊^d S=Qd1w)_?lk/[ /\lJ u|ӡ:vnز[yapȌWTPl% ?_wt?.}t':Iyq<(QAI[fla纱߽Ջ"Zoڞs@$"R AI QQeR1 B\J5W\+/(_Zbn`+k4\xJ$(T&}%4uhM>²,:(Gx} @Lm&{6&j"@5qчjvE^UW]dVWW@I@«ׇ-NOl| 6oP^qaJ0*: CI0w;pp(qFy>ĹS! vz$EHVoٽ)<b>J̟?^y ulMK@ryiÞbjFuvv@}/,^dE3shOBp#8&_vNs75>­&gݓc+MlF(33GeVC<|_|0SVDQTC-%з\KXD nǡR8mNSydhgQ^: _T=_xug+颫0&^9vF16, `W&"T>,T븑P[ٓ) \"(?+L2E33Uh<ϙŌ\]6b&zk3=hKKdEr3ȥӜ增]療܈znN/b(R*iQ$;Qi8]** ^k0#9Jי  LG%E"%P~$V7p=V6T1D W2==>MՋ.2`(eBv`W(c_l8YC]\? ^F~(AOhŊo….=x6 ,@']%(zq^D CMNWK%P .t\*]f˥jCR1*bV Hj&.j%tRΨa G1Tb H.w1N@RErt|[GiutJZtzʼnv~x9^\lyvs$D0vK 9=c\D LR$@DD&$4~xڴiPKx}=FY6AX 1^DfveoV;աPPnRe%[YDlaCȟ*Xv hQ\gR^dm%_Uߧ/gcwi]`ӵ(?PgZt'\:2U; ;Cr.)"R AI9/^,²j2 ]9YŰjy#[!pA\ Spͷa#Ϗםo/54☚[ڔwl0qŸ@,s`]R:8Sy[Z7+Q0U?SRp_Xzt5ww}F+>SSGTE u=y? bљ QK%@Y_yĐʠy;(WR)55TOwY%5SIe8r`;j8Eh]K-pf_(*빴jlt7]8 \\'B3bN&vHP%eXI|m6gd4!Mm@m?ABt ~ v| _J6hZm$ ^ ?e(WD]Ȗw(/TB@a}h0 7#gOg]8J sE5鞔l>:S*p K5A\TPE---?x 2R ˝y{BJ `@i3oTo!G eBcNB?^ƒ1SiH$aZu9k| lRYc3zjR2v^k\)DYJ8QBk=|!kwR`}s"d \!7TE0}ɓ'ӺuTǂZC /_cs2 ^?^kg7` > ce02\N m.PRҭ"l~NAXVB<߻?c +c0M^FOQ\\C.U*Z'j7~y$wl_P\,% w-[/Lms`8d_}o pa\]wゲu8/5ٟ_.@5q=ZebHeJȹsT'J]J'8 [ \j6Ys`~/41DJHͯ̿z@IT__ON2fuߐY %ar~+ " ZvWlhЭxt<37KSg}}Km@J^]Z Ƞ>VO Ff0P(PP$*@JJ |kiRrFK&m`:zhW8KeբV+p 9j %ЇM;uiGޑŋ,ެdިY3΅e~PN6,.D&tψ\׿_Vm/>]WhJ;d ޶mIag֬YL!Y:q0U2H[{=+xPG]W}MaH˅@z/?gSn&w.=8gR$eDRh#k׃jbzu@.b̙CK.U]YTuP[elȝFj|V3]馛n4,\I#RM ʆ5q 9dBiD Ջ+zB:T͈S)"[Mvs51|A_w_]^"#)m'oě(&碦^~e6μy(--Ͱ+ɣv1"IYקY[1_1Yܱ@J;Oi ᗔp ńq3Lu;TUa%$ƍG&L7l40;f885lEX, !uuuȶ~,ڋh{P>nbj ]T",nW[ʕuD*rr/@HOa sp8v 6l&.vQgs.ao/w1@1#Hh߿?婮lLJ)-T;CPh XS^"Sܽ{7AQاOK cWrߒ5Gt?OLnV~x$\h;^ݕ@P+J]pPwB1cHw˵d+-OV&.J%o\ Ywq _n$ Z[oEߩŒֿ_|BBB ݕʒTqjIwvshW+(͵`AXv7NAa(zx"ǔmzRu KXD lja-o@A.-ZDӟI ޲& u("۷o^z֯חp,&%д6SE.>+p\i4dO'~kbrΛH%}D녋9#@W#,P[o~_m:NsSHpAO&*$+VI KN=&־EMu5MO9S;9`KCV9A U?6;y9I+ڳ ^ilsJJOC@>tkZh">j_=u 3UQc٪S{w?c-V3@nn\#6k?<4E'4;jGi,/kZ`Iv 2n4nc>A9bUWPM}]ב'y!}Vh9zLpU/4keS}Q,?8򗿴 {yw N[7i1￟,ڟ{9 r픜Q;`UmqltXA&j &,@orC`̘1BY ~2:.cqyRs]wG}d_ltMр vCU)[?~{= ,RrF҄SJ>A" @^q.0]mbʁe#p]hvzTB$nB 77^~e 7pYe.ڂ#bɯ?]ƳF JHH)SҥKMcE9njhp57P"RccM`! 6M6ш#,jgNΦ&S(6E&dPxt ՔKFAc uPe򋪬w0jmePBL(%&u>5tDj&MDW]uj?wvJ;іBV#rBAi Mz*;ƒkP#42AQ`/_D >s ᅤc Mb{}[Clv++pG'e>zh|qf0CV^dĞ5#{RD\:_ilX ,9C M^Yb!SrX;UĽO6M[-/&xcc-*rNKvHwmm-JG~F?>jIk2a.%KК5kLI#L|unBKǐe 7>k1| Vs~E "4r s):SG%OVG ֧GwTs?+A멲zІ/- SO=eޤ6-}}ߠZK ,_~RZ2Gigyjz7ak4I%,&QCkw{@SjGjkJBD{jGG0n?zr :Wr亽zɓ'a̭p +HkSyjo@c=FHAAN.GnNTi~ˡ_f&T,._*˧ԈKggee)mm_0uuu[o߾F%].FFIC O?u,~Ī( ݏNԽu-G1y!/CN/@/rEٳ;:KNT:ƞ2I[{8ʨUߛMݾ-8󖊋o)aI3bA ,_ͥgϞ -g8O"0eDO!%UD7_3Kt7L Wk~;P*]t5"~d5̹ްis`'ܨT 1^ľ}hV@dS,b\+AwXJ:uRjl-k}7q[NO, I Ӛ1ceff24]ג5Gv;v-@K׵5k n]S{ LUUU) y/:[vF@͍59%9M ;v"0uxO3/ubJFB=}|uyW HЋ4/nEt-b!eK +joeW䳑1rn |hI/Lx7SfT rsPFLv D@ #kR@{hڨ^>Ҳܷޔ=zuȣR (}qX@/҆ ܲ~ݨoF,vP~X9n 7心2j)3.G*\3Ճ흛;ϋJʤxSΝ;/: T1~wp;T^93LSBil Cۭ'UzU6QɳJjgATZg[T"ۼ z饗^ [\q4o2 ߠ'^HOa_m:N{Rvy::,Sri]msnN_Kٚ$&&ҝwiz]־I= ] 8/j٥65Rs5b$Q%PwEsڽ{wgN_^P (~O ʥ?ilso|;&P ˿\t 8"dd-J/%8$4͞W6ưs7C{QznC :"N=L &N*Usx7%лo}oɒ%q;;x=`>E3O8gκ]:V]Lp5{Ԧ dõ6ƜtG^Ǧrilʕ+Z9sIk{[3ܻ¤MH܅"J >`.3&"ؽ/"(O?I7_ Ej`tJ-X93$V@̭Ժr,3xb /k֬zT1>X~r! XD!616+o=PB&PnU\.jҽ{wJIIQ>6LA^TO(Vgyj&XLزvmvo=~oYYÔu-Gu<X<|%!Bcn Zm`2Hn?W,T]!iT]H'ЎΉAArrrs}{)tuC=?!QTPv+98_SX ~\L5739I;"mYD*> )ف~Bg56QY1Ѐ[(tP||[dEhj^rHsss{ﵙijR`n?7f2UdX=øY9gꈨ_~q_1meFDD9@ZsuGqMVLU%Ga3䮗"39™ KDNaJRz⿒^1;vӼyȑ#ƇF׏мhQ'p} ?gt61m)ѦѷY,C },auVkbM l,ۼ XQRDԉt}t@>|pr*21m)//G}|Me.q*%Q(=4sSX?:7Jr s['D(|6Wq=[q֔f1*WU.Μ&PI;("e- gOg?5B'K%P&P>?TʕS骱Y鹄 6?wj_*SJ˙#.Cp4HrkYIH@\_sw՜&G&' RRQO.8@׊XzVw%Pn%(_~i{C~PXHKP2\^t[c̓;l)--ւoH%-w&i 4@)u@PÍV:ulwebzw17!{J[lsLf'uPN=FOb\@x5{鞹\JTGAFP[a6zǽa?4ڒS K@ HdN*33@IDATei! Z~ܸqFgsSZy_LfUSS(}qUH|]F4z-+ZK"0kgL322Ś7lǘ*A<~թ[Sז[+%F\Xlq8K?kXI5M*yK/d.,-(v׵C׷tSN6g^+(o"@a"ZB3V#FX& &޸f2"Fmom"6DӬ0yQI8z:w{Mɦ}/@=+s++ @oHøeW z`,!iXMb-o%p̘1VٷoI;aVܩ&tBq=LfYS~LU6ٗ; Fv0@t$E)<'4ɓ' t'z]O>R3;}wԀݽ1ʨiw~2žAvJG3W0f͙3,>@ d. ̛f?&2dƍF{?n"aƜrK",l/SNe.bZR sNB6c=F͎'F/f ]m_r\Z^N=/ӎRS5:g6i6550٢I$l1[/hJ$ n:ʼn KkC $;+dp9ʮN=r8L3+7EOXe6+\v)3l2K%-FEMI@p8%d*ǎ3gX]5\cx+q^}lD@;P`p, ,)_YjDvvEU#=I=%ͥ@Gd4_v !0c3E>ڗX5ם%YH댻)/%k5eW i}YDV cǚCB uH@j{4U4^sL~̙_3JnUuu5~JGAAC[7QȐ嗉}P"fr0.\^eACp营-%&&٥KvQi&ӃӖݻ?srɾܑx } lY֜PRn^v!PvP@@D>R ˖-Лo~)3U~O~zTR.C Ϊu:^YrLOXSOYdý:jC#עrF̦&FmmIvi&(M:fqzynjk- X=GŬ~@R 4 *B׵^K*?h>I{&Ѵ؝&]Z.;f&A(gmxPOXLB>q;w|Ajk3<+An0}>$B}L]>s&k4N5v7whmז/6kD:"Cy 8e̘1͛u{·&dBP;J9"At;g^=.L]cj Y+ЗӗiN cP!= eDuU=r[)>ݔyذau] x@֬w`zƚ3X:Ћ{2v_?O+W4nRg`#!e[5K%0*ks#bƓ}'}~8[H֋)xX4H['WzgB!PYYI?>p ͦϠYs(PwoZ&-z :IJKxv3N$駟0hR-,V;ֿ~P7?TA5k*!XyVpt`[.Z"`+j #rC\3*ܒԸ_?Fld>C???%@}7s=ʇ‘aIo]EI W k5OOhnm| JMk,)}љ"]moPDB!<w}#_+Pr {Oo5 h>(ro16Xe>=NĔw^[N3kR#'W&(8:UXq$+ۨD{qU?nv-|~r,]JH10oӮ](##8}ߙrE)TSFf,Zp[[9 7(6AKuLڵf%]!T]ԟoQ{uI%P%[n{ck ٮ:8 +_]uO*_&h+p:kLT{s>檫w /:dX= <GJGP(wp s Rex>ڮ81CCڧh$r)L\ o8Y\yJLw{\yO'J99Q({9DGrrˆ1|?"pScg85^LvR>] cIY$ҵ"FomE{k6mwA^Ϸ@2YN[|pF0f͚H%-|7Ksz)@XTlaj/.{ 9  $}sP0xo$ae3s_CBFb?,6"g?kF6u; S`}3bJ0hL JYod2oRk$&SϤHB!^Mr-((`gPJO&h\%Bֿ`;K|GpPN''P* "SL5kֈt}pN:E]wݻ6('AQBPj/*+ex4qhŕ{OFT z",M'*84-q/Q)6U*?GbpYm;@~De4vX'@=k׮ йsm:IC{u3MA*,|Iilam`>MEbO55mCQzm^j/Lr%N`ѫl;xZQNA лty-Eܧ;!70NN*W7^tٲe>HW2|s%Dž)YH*[l>9aZrt&x)٪Y*S*qَRzQȭ.u 2QSa2Sw@^;yV!pBem]e@ vXzc_+D]V liiQJvLxl˺'5Z9qq5mTg\5y/oZhA[bjӦ իW,ţ^])"s1N(AVQevY4Mݩ?,V'g^'P|s=!b~pO..Ԟ׏b#MT.v%k8Qn-]C~Ə;6(`L:ZO [5\R%JNNh}~1o޼ޘZ I .QS|۬} V`&dzBN4nXsGFFrUP?3?X'蹬U4Wݨ,Al F`F`R8ekf~*j HrC7 7b(  mFg:M +Jq\kJRChrN4RQx}hZ(^j%-؎*lӥ]ёT,[[^I|o Ja]v0(t:t&MXҩS8"BB  bD`DǯW* ڠ}9?O0=|p:񆰄[n͛gz*0W)ՅJ_ Lq&eiҐ6ê,^툷^R/jMj{mct4 (h+p+kAYh{ћc-snԹکi>s},X߯=܏}@{(q P E;9[~Cƨf~m]P:X1c658ڲe11xY3f -XvԎBtm @N@jTpJWyid0Pu2LVUq)=Qlʕ7/'r9S}P/4N< bH'OjH#rvҺa wN8Ҿ`ɭ };ɜ2Oj!>uԡ+Vh }2ggRJ`ii)5@cV-]5l0Z3ˠs.57<PMtJϹb@liVPnU%黭hATf1(9z"ZVUL8M}%pA\ݻ)+>cZkϙ?.c \@šWep66<zsocGYU\YĈ?w_.\H}u?үCF 8l0-쎁tyC!A]%zszf ˮy/RYTU@j(+TzIwZf:;TiIPe%Vwz[jU-|\v{SQv?5 T;dNB)M.q*KET5Y#{̙4r]D_=$@XK CfR8DeKEyeaDžL^ak׸xB&V%֗-q!ydBL:B]=-[.1{ {sXR=ǫ[e !(۷o{U*Ҡ-x˟OL `|QBsJt$B( Pի̐@V?O{\9$ɓ'Ƞ`D/g:ۘ ՠU \sVdbE VAo6A惷tJ6Y7p*6}nf͚QU)^8m5^A$@(ޣ%b7C= .V9zV~ggeVCJ3k@{ExʠWAؖnP?t(}ȼ%NoLX#g{{ *fV)ZagP\J[- w!pZԑ .8+;vkq9Ví}TUPs##~<0ari>|_%>keg8Ƞ}Y66RAODUٷ;LDyV;wY r<@ʥ7Ijϟ?poU֔Ty"Nl_W\PZ0%]I5GsK3Ms$Ze :شiS)f4' 9ge߻>{Q*k*ڗ[T/bWESg5uǩ{4c BlH{'+{0M *oؔՋk"NE1J)fk@Qk\zmh@˛z>}|BU"z3҇ߛ;iAh{ v37ǎӸA2?߫*6Ѵ+oоE2CTϮG|O?4=3W8,;w- W;%V F^xg%}Ck/͆5@+m0MFP 7鿾NzN;>ZK|<0,#-}x?]jgt:u7IҥKtAN Z<"'sowmcEՏ;Ul)y;|e '< O>})7t-^q-ԥ&* n_?(}P|Q] (K̒Zxʈd͔p,eVLL 7+*Q[ǠNWE­XT/?(իWqa ܻw/9b洯5ũ*7Lmӈk,9b3WQiܒiˤ <=rCS}뭷""TJuXkQG@Nx.mcLS@BƪY"h$Rm%.JlJ"7*sXQߛE#63VRBݧJ9ї\ڰHz¨ s7z_\\L锟 zSVFu.T7_.e:$D*EA U6p*DkѢk4tJJ FgYXs]"W{&PXYA컳7kfBq22R[72MkR:UcE,VvܭuQUjoBp[:cm۶xo 6Zf9Ǿd &~,Z /rSrxꩧ!v8h@d7h1>4촁* "dVZ{L#f m{Uh3ݴ ~W.>mMIC:\Qx1cqyĤ :)TՁ~.\+WܿwJLRN (nGm9x"~Qىx^ʹ$;G<\>rsSBBayQׯ_OH.kS* TDH,kŲbA xJ`4/)A8\*hP~udDي29vDDu҅L-y B&z;6zC#2?'kƟyqÖ^7V$TG&dd tD%]ܾ+osHx饗oF ꫯtQmPt|U7?j*qWP'LNmbjϔVq*YʵRV?YA4yڄspC^%ҮU955` w2d%|Q)M]/AY9ޭ{s?{]6m~=add ۷"yHI+Z[RUЬ?M3"I#يz΄t f@(pa;6ֻl]#8Uyy+UD-[$ ,3<"TyrܠH#DFlg"H( mNiYrⵓ߬￟~SÆ : KѣG)66ֱX; D'EuWe96W N mĝܚ1r8${?^J>S*'8B;%E*z#e+kVl-96Ub J z>f %k[narVY瞻| EB/U{۵֫dzv~bܷڨk%qJ@ ]]< P _xJ0G w5/*6g6^>ޫ@X}Y͹7IO)K]P4v@:v*'sp i(u6/ѶyCj2[GR / Tʲr*("w?'T1~ZbtdlP"Vȁf͚58x 36?ES}YH7-yװ$Vj眭`^:p7U>`|ǁZ"w=ԋW_ oquTqhe$2-R~ɓ>Ezp4BHJ|8^{SepGcz NG*>|]VYzp7ә|-Zgb)*3hݦyb;J$}\(e;m1 jjV)ESILޮa_sIeGP-lV,ʪJ҇װY?09RёU iB .fO?TwA E,t` ̘+G_YJ1BU;ldD/?C0#<2Lk";l'ԩSGZ5א8]U葐q%HlB<|}/cN_o}IYew&4GST5 }^΄7{4*S9k=^g?Z4\0?0R lf@>tKg4UNpsۗ }5' G$Rwv$::RRRef͚iR:~(OxSJ%SxY9iP֭%=WOFeK?R}}S| [ -è3j:L=/%?>a9ws߫M)*zV@hƕz1dC!<B.j߀2Lx<\a=Dm nKVD$Y)vm w4qD?~N ts~;Q*Nн/B3#;r9|Ţ]b޷ 0V7U" XHWqpYd8[@pDJHk[+;\ !‚WBۈxujcU4>M(V*^TRJKv\{9.vI\/V./UQxD-X,~!mTAd((%_=hرt뭷Rll˒@*HA64MSK)Kݣ2i=PB۶^VU+ pcwh6P:Apx]56mj 2 JE)'0ۃ Fm_NZ5x!2/(XF@T_۷7fj>;EKGV^x+U)7TpYQT;@1{Tq' )bʊ] !ߎ^-/,uٺ Ja Vn}FfI=Nz'XO<)P ZIʿkv Ta}J!7͎ODA k7zu̹eT~kڃitگj1TGMCGkM;{Z q1XQصt  {òSBX\"ԑ^qVJNNnqݫW/l-#Z>x:ʅ5 0aݻWw\$67>ڡ QZ ?< ˠ̩ꂚVΥ۶VZڏSj|eWw 9 WP:jB(EEENT #[J uG)z(־}{wBmD;, < ꫖QOs"j^(dP0MiOT߆NTY56(8 ȃ:QDŽh3] k' \"U`/..֬Pqȋ\F%,יj39RCO,%Kh.[̨<0g)5a饗8zHK1; "7+i*|ܲfI fޜ- B 9FV~T%P \8s挫I<[6B9K\ i$JɪU)7IU} %˄J,13.rDrDŽ(ә(Bw3%'%%.+P ]ndrɥK4qx@)Do\\4XWXa)>njP %/Qf*RV.> %R%`wmμCԹڜPж"FC:.上-|- ?נpi!I&9A@PR(f%AMMWۺ jK䑜MtͣNͪNq%Mpgg ޡ2(; ׋?EZ)S{Sd\rhjѤ!>ЬT.W.BQw] GG!4W l;KqlfUۚE!^Iѩs#l jǎ-OG33D 쮫 A3GNժlF* Uߩq'nBJ]O=OY~w(￷y@kԊbl;bE*Y4L%P,pR1`Gݯ_?VOonl;w.;ww8I)Z4zO6WA-6>FJ9uJ HÆR)LeE ??֯_o) $I}NB+UХK??ުܿ9G֩NOӖ҅ôo,egff^)Pɲv{6@۷/{RQl4e֭ەi:sL/{@)a$g fczP:JhZ:J1pp<(A'>Z BF.]P =)GA*pӸe˖qtkߘ& HwΊ7{^'Hk-ESFx{$FX daP$pğKB@,Xp!ueTB)))ԻwoB>}<]YhƌtRB9]$umހЁoL*(w w8C)a@VVرZoZ&\?yzP`vveѡc'*#Io~I}i!]Tʻ F q.E 4Ӱm޼Y+Fl=5p(@IDATQv*11V[PP@_5}嗴h"KܥtD#uo3h}Fvڕ`߿뻑7LЈ潜6< ki<öJ=QR16jO}RFsB` % *Gi kqp_EJy" 1۷W_}Ş΃'hֲݦj /.@67 iÆ Xx2l9o%y@~Qe]EW0,k_k6{ 脊nX䏹+u 5c9waQPcA)͙J j.s*K1b  mvLTdJJJtt2 g233iӦM~bi"d"P^h%aW@\%d$bH+Whzy'Тu~5׈eFxU /:#nXb櫵S#`Bp-J`⋜=P^K.*H[A @_vik2q"d"YQurg6Q&Jt 87:zkܯs5oblmE;ٙ˰Vc^P.\4"ؑnXu,.aqg>mv&5kՠVZ_Rs~^sL]vJ#s 8r @֭[)77"52h(fe\[[=}M-zW_x>֯]FM̋ͻiA~2`ܫnj 1McVZU{)G =gK \K j#A߽{4[}|AM,((ݻrUU\YSV5vXtRڸq#ZEtNTBF͙4c.v-_"< vvEmn!p۠W3q^#~qdϩX<D%[j%˰bCټZXY+Z#),%ԭuyYJTu2zY =0ʒNPDFc;QдpA _S U>OO(^(VC kr YK` HN@ [@._QCt7ߘZZD.#{:G 'ׅz}cHށG \U9!y@+}^G i,C>_#msT &D)FyOΞ)dμ|)P 6?z ߫/$(!SLlPO8))^ @qqL`'1 M l 5UCe>2*=O߬"$Odcz:RX0ND4n@CZJCfRHRs?-^[.>}޽!U\[l;9(B7x!4@:wL))";e|I,`B6>G.9^S7疹|ER,T^kpIb}UP9kLɂ9*TuEGTf#C8163V"^(,8ò(!;v'xjRSS%b8T8x钊,׬/wY ˺7VJb4=<P\ @d33q7K X'\k4wE]vFC0F@ |M6=J*`*T# PAPSQ ğpcnl}mŢtaIEHldU6Y'FY3Rhy簎ĵq0Wղa=u7)\K\:t`4cD G\i߾=U~u#n_i'3YYY AVFt /.""/\fC+Wq#7IZ48U-phUegrSӓWhA͢pkqY@4rld Lc@;cǎiI0E@ ٳg_|uTTIp6\0D@߮]a>q4i$zw{}ݼq] Jѹ󗼶@i)#S(^넜9{`Ê&='460{%焆%gZN'%?`#I $i ^T5k%8"J m#J`ӧ{( '?la ?YZ2)g͚Es}{{UPҥ]]N 칵lZ[E)s{fe8%$p_ӧEeGɉ5Ў 0LZ2l' s) VϚl X`bde'ᄈ׏6nHjբ5kְgP>[6huE1hrRi&~~?5zή6⻁]Zdgf6j=5*Ʌ##iӆ."xC@>ސ {W^y=hqђb8@{~XO?FrCzd_$/nw `;%Gݠ yUdaLw&tݽt%P8 /P^^k2s>(T(HɱEUV<`# ۘmp;kBlR:4UŨA{1fXJQv%$uc>\K`ת-C(J w»({WٳY"@!rnڴ.^F!JLLv-DRŸ́?v'e:.bxv֨WPfmTlw03)9ܚ U" ˗kt7sDDЄDHN%ЅD=▸VxA pU(,,4͝;W1tPpرw*VN6VXAGѮz뭴sN5joݛY7:浍KL+ >vܪ9zA`fLڻHjWo]D'}yˆCD=z |>E#$;r8/saÆ?;A T@Tf;55Ui޼im$i޽?)* _ٳgkVEϿiUshsz 뼁i;z*wh$_|g2=q=:us]\}r2rOG^ ^%+4sx m ;wlBX)Shߡj՜+ n<޼8p !܄zk*tDiGV f #]RP MAJ9)pݞ;ϋ9VqfRWmq?1^7 3# 46;8>:sS`5x=-_dZU{ S[E_~+ r#P0b\׭UEV=CBW1\ νSzUgԸΰr)x2w$pkK*Bn8 !v<! JBexb,CR+wÆ ؞w4uTS(Kwmyێ'X*"΃'4zwJ3CU[5+N>=;;Vc\%>3vEn㈙xi5GżRˑ~I< (i! J`}Pm͚59M  V0$p>SNNω!>\ўTYv ZͲ}ہzP(AcFX^LvۢSgYmk30scl%Joޘu}* GAIڸ%ЅD<lٲeX XGy;**2!"?Ѽy69 F̠7/+%gdRL&jP D}[+gp-I5k㔋[w˦=7)Jn1o 2>uVZf 몰"4 @=.]{.iӦ|rzꩧ\,Ӝ9s\d;+5K*&#:y'F36\ާ/a+>[e22ZM9"=R41 NۗM=-~.4r zG>?s_tA {ZÇnzmu7rHJOO~kLGTdV.`tjDBuFQռ9c+Է6T"ğ;3)[;#ABzP)G6B@(7:GK-_'"K_g(|hG/g Sw.]7=}JJS $k$\fZ`dyV@Ў@ܕ w1'OBLYw*IH c `Oٺ8;+X\{rPc%KЙ3R@_dhA'OÇ[Nًly$I\FgϞ+Bj2 KR+}TF){[:S*ew3u.@)5%%@FטW=D%ք+xȐ!f&m Q wTR%d96ʜo>-{co] :q iӦݻ}VvCvU૟gPg؎sTG1E <:U_'m|0=,OTĉ S"YD 4 [-&_DUJ >VFٟ~%KkkxPqAM}LY^p-)tV{GP-W8(\*A=Wt8yFn ekm&On+ wΦ~Uy!m6+X>4@VVkܹFܺu@]f@΋roIwgӇ{. Oz*ܤjUk\rJ #ٶ/nI%qݎcF͵)))j+OHG7G WO_@ٳˎ;ȪXBz'5(v skosޟմqs{%s6ߚR NdYei;͚@bsJ A UY9rP6K7d8W 1`ClP5olU5kF~!a yqKXpϤf:Չ U7PVU+IkLN&N7Pŀ{CQ>bFRFP>#b$m?KnmNvaM qp0Sn^#58csCL,;Ш)ƙMDŽ(R%/7\r''qύ_eR9T@*\VyCڵ_;3T`)u_F C:Pȵv=f&#V9Fj50g%gxQ\kȳ ` }JV88cKA5Tx u֭r@/6UP% \KWw9zzR| X D ,JAq` %m#nd?h7oi嘠y{oJ&BYNjuָNXT DիA45&91J uAA 6mADFvbSy_˖-5ݻ{k"-"ՊmtYXPjO&c4i ,ֽ9%IU5C/d;%mO@XN[UW^N3ˆ+]rmpԩW@|@ݫW/ۃmۖQicwW͙fNz>I*:uyɭ:wl~9K?ID@@OD=h9"J %i~zJKKb{(,DwaypШ~233/ EET_XogE\FsVh.(v_wԣG u*-Bscǎb:4ȁ+O ֝SEGGkU?F\8}4[N BCq׮]Zj7?,}!{#+\GZ4ѶyjPUmzի:V*G\9UX\J&!/1ihG=ORA(}9KzĨ98[1 pv\EvP8"9%iEJ&%DoH W!\k>p'_wyG&sA 2Bĸ*I83堘]R.zb^;zOE wPeCჵ1~vX&jҥۗfΜɲ& xڈG'ykQ'! (xEs=Z7lؐz-7ny.3$_Wxb-qWmk\c#)F4G׏(k]@\]%"K$%]vu:Ñ:)i=M,,>K9J>QBG#;V9N glWXEԘ1c< 6]yb5E `j0fqq1թSǠ*h bNgm8Td7| ~!m۶H02(J2|N/*5ۺRee\>f-7[]BSND5hР \YACVPFZ" J )w/~<$Ad(XeݫQ]2ʓOnva@(dS2~`[@]Se)M6,{AKNNnZ{f͚=CRNǎEkyAzCǚc\?&R:ϛb7==mpw}pݺ%69 }͚5_ݔ{57&wO.H QiǐqFë8D#E_.ڵkMDp={Ђ 4RUVщ'1j .EE ^Ϟ=5+_ qr2 D 1ǎjՊ!_~aW@%8'|@1"" $*R5D<qedd!3׏E  ^%+4sUDfΜɾ8]Rdd$4tw;z>} k׎y=z` ፀ(yyy[o\X֯_E9s(33N ~$" $|rŁK@I%BÆ 5"?|a3O)D!!WDA@U|gZnM-[4O:#e/''>L:-x/IկvN/  W! JUpĘ9ϟozR5$)f9w]"zHҟA@F@@ur8Hߟ֭[gzZjiA(b~`@V>J?0\DA@(KD ,K-\ n޽{Ӟ={LF9%# #X?Xe P'￟DI7i! D z!^0+RNرęyM(/IHH'|RR.#  ,D dT@CrM7Ѿ},Mx2(t#Dv=)|ꩧhBOelA@Sh mc(eV"d)P _q~skT/Æ w;0ZA %0 ? +GA+Wwi w$,\N?nEEEZ7(xoAϴind|A@A2Z:2/~ >}IPWn{ Jm}:uFܼyso  00[a~"|/' 8cbbF!<{,^ym#<%{YDA@D 2ϭ[Ҹq^ZX;b( /Yf@Ĵp(}H3r3fFr7z #;{e"pC)B<… *x{iӆ>gA@A %0wc-ҬS16e k$ȩ]Ϙ^CCϨ,@XAǎ7P%A@l# JmkX͞~iz5+fstwѣIbcd %^AzA~4s.vԉ&MD&L!{>r}A@AOt`e˖o~Z~}&t׮]$״l2&(A@(~79s&uҴ|.pSS׬Y2\PA@D ](9lذ^{5ˌH {)P /],{UP ,[+ L! J)ʾիG5}oFM:၀>*#lA@A %07U$ !$A@A@AD M%   `(FyA@A@BQCpSeI  ! JBr^A@D@TY  FhA@A %07U$ !$A@A@AD M%   `(FyA@A@BQCpSeI  ! JBr^A@DR0ٳtq*,,Z*կ_իws*Uq2gΦ4>7᳂GÆ ud`Crrr(77W{uQQ>|l˓0O>_85|vhԨRfbŊ?tyd뮻%K|@+W$|_KRbbnS=tZ*T)N>σ{ IOOygf_[R;t@Æ RZP])@ϝ;̙C;vД>~?=~ȑ#ʕ+}O:3g^F 2D'NԾ06(+jJ'H%hQiCZ%5IHiϞFĤle,S45%DELES;s-{}vIE_!+A*T!_tN;خ]:^x駟2;ilt͛S-tٗ^zfpbKyjذaަ?ޔ ,0ՍtB nIz뭑,T"K`%լY3E!~08qDsu"C`̙E;P<+u+ETd3Xn/{;wVݻwWMU6dJRbB rhWb_,Nu7+% ŏ@HFE1Cv\% Ň͛UBx]hvvx衇E]FZh:˭cѴu^x+xV 4 %/N d.ntPc qa QuJ܈;Cm&jtuVC>xP zMоce!4!F wЃ)4"K7CXxc >4/A@dO##i>2$ƥWg-M԰aC\//Ybr~{$xiO^۹ha q3ĕ1bqi㛊YgJi~ȣӡAKheCQ79JK"s8[ F8^B Aκ4$HNk &|v"rIM %Ҿ䚌tCx@k/B`52uHCۛ?aI !{Hw!0*ќ/ֽ|i>FB}u% !p֬Yk5\nX:iK0 RYI#;:uZ%KDƓv)vTAzWi8ׁS Rp9$0F >v6 .T۷Wp*<`kɵ5>?.YJӏԉlݛ!R"s"MR y ;Vk1I}%}& na^y晼 ƨHR2;/\!c1h%LL.#^ƒWp š%K?0]ӯ+:hGYD? 'ʬHx(n9sslgb=y88ڍb7ɔr&GEn 3T{Q!P'3rRVugY]F\CҨQ(.#xذaFy 8vtD(͒իW/E1r$<9 h?qj$d##"A1l!LtepS5G?x a.!< 69F&P-{"e˴jСMG.?$ R`$"9YЍ+A)  5G2x[Bζhmp|pnݺ8uk /@ ,ڶm8N\ǨH9>Y`;{VJp>#*I n:yf=&M(x "uM`.k.E) UN뙬/ZDX`pg!\B Pp5)6i1sVc*B`_ K <Ռ@IW ;j2觟~wؾnժU}0 dͪQFnP} .lnUJ@ uzޞȅ$z.P!mt衇NVsbmΝjŊN:Z<0OGB KYB ?^|ɪ_~)$EAZV-P&ub![vmc!ПxPY›RwyOǎ՘1cԒ%Kģ|hB@p_`j֬$YF#G#>[ݮԇ͸cЧNw}v5{lPfTÆ H GA@ȍAjvZKBA{&u>e%yFODpZlB XCID ܹ2r F !ܜhOH#}Jhs#=dGPrexcǎ e?]re;+R#$?>|}\x# pcE! v0[XfUD@ ygΜz&!˖-GuL` ᐓ'钞@8C p@\!w;X!1!cѽ?cwcxzjw6w}7+SX $6|plKl Xb-[V1B}j„ A3oF  t؃B `΃܈hnN`p]-WK)r_k"p](2l&?jmצ8#{% ,PzRQiov~n 2M駟9)u!A I.bJpXf6{8 Ԇ]`@?"7ѴiS5ydqF{yB vr/\MU6"&=XbE?giٹ1ڃ@Q{FK_*T;e˖iד G"?Ca1:8qH@jR'J*V8+V֏>Q5X/\>YLHy <Ǝbg̘' -4իWnBvppZ%ߗC\m*ORh@oz?}Dfӆ+6jԈr I"vmb|qq8u /[lѭV^tM]v ᫯R?XuRa"P\9$ɾq r?lܹ3]v)dҥ:Ͷ[ߨ:up?UX+Gj:\xZn)PoX͡^zSX Gr XnذB,[]zDS+ 4`?S1`!  Um-ܢ֮]VСCs3UrnHΛB׳ B{8Slnݺdo_r7^qK;<~~iuEiՑ PlYW8ǐ_~Y]r% &OjCB 4B(ƅ@HK.͒%KԐ!CX1r6@NGd.b5kdTa6E N_W_}N5ce7mڤ6פ@#` AB yg ^_\!{ァ8 )9fAvp@7oN3 ƨM ­Sn]lwD%| /fG,_^h۶-3("mƪuYf"qơةR:Ym,vM4h]O<3IWtyc7qٛog/ M oTӦMq2Z.BѸî+`6e&?@9Ejժմ˺ ւΝ;CC4J^_p B֨OT-Z`5 G;|>҈#?̩H@ (Nνx/b52F<4eN2B*?5ckXf \L1@9Y8,45u%q++nmoca CnuNXKk^铫H,i{gZ]jzS6J͚5Cio|jϻowf"/a#(p(5Np&MdWO,׿;=D5L6!MTG.Êّ:%9_,ueŊ,ܻt ϓG#AzN,&M;rŠ3A҄:|0΢E&#=bk׮](y(R352͛!C颢b#m;]1?L8Gy;vl,jQ B @P 칃 8f0Tb i|yT1! #cNpQ99իWgE%ҺB &#K 72=\z/@!m^kQIq?ҥK1z8d_v0 6(S^P|y8#s9lpι"}{Ǜu|]HzؼaOZme"EăsmwC9F` ' T#! /4@ԫnݺHj 5۱֕|)3SBP/nǏӫWP"{=8

SP!D{<u :Z}7]~qAf-Co[nGD(+43<vinSӱ=c>+^Qo=y>;=2Yڳ`MAP&1Ek߾}˖-ں"! 鰏<E0wq?8ˑTgU E`X؎lp'!Q<0gB=Ui#Uؤ&^#n:-6ܓ0D^9#5uhr5X5؆ʱOUӹafФhy׼X5.y3YnSX&&F8KR{w\s=-t::Q; .:umȧ}OPl)gɰy]~0#^7|{u瞋Nc; j&m7:DR)4p[fأŒ6µ5sLEw.k4{lN}B_`Xb,˺ ~ÓeBwCYnkrEJHd氕HW_lIRV6Ax x#y=$d1ኞi^nF1ۨ $G10es3ͭw%h#?Du&0 vbQk.s>0늍@h3<`x׈GDQF/#-bv I9sg׆ɣqM ={*QzE0w o>^x{8vq}Q`O^1LJF\-JMÒi4#SƧxKN7$͙3'@8Vp&0VU`aĎ_cǎk&5qkyJF?szp:7n̈$۶&ЛwhCV\x4a7ǣGR( LvM ܬc:s˗/w!I˴l $>nՁ%JۿSR%][ 뺋"קwKWņ7NXfM׃RE*׼ߦu@o~#IJxסH$01b%wqX9ҍ]B&kπ☰t}{/[,Dܤᢣ0CnŋC@=!л~pіQz= Oi$̹8@[ GX)b#+,z 9aJz?c %C>MN^eHB<,Hm2PH@ iӦ:R^yɧԢh[Y yBw{СEuc;"'ȑ#KB7VHUm\OQ) m@:'|;&w=Y~n޼y[ ܭLڬ|v=~uwDꌇm_J,LG{3:gۙ9_A+ 'A6.UZUկ__ѤGAB!z{SȂPA#::p+E "a0&2GAǁdHpq3 r}BUD6Z9 2P }׼yԆ = :K7F/>sUp +TuŮE%@/f!剛3pD&GC w_4A R!eެYoHIAr )dbA39" _}ݸV FMvTH焏GAiގy_Tٸн2X-+̓x\T G|p0WuSGǻZWVc}8⃵I;g4Rn8\H H)gDzw<N #!4kW_}S]6@H  1"-k]9?Kؙ[rQ]A@A\W %]*CA@A +r)t\$B`\HK?  P4pnAҏ   l¡8 ș!# @,Y5i$#B`ĀJs  P V/!ԩ][AI=A@A@ 0y4gs4sn\B΍A@A*i̙, ̚&$ y 19ԬY3N5vwΝ<ݼTA@A8XljܸB[]B*W{-YnUvy  Szb ha S@ $ y Ԕ)S$hΝ}jլ+TzɪxAO  @UyfW]wutұ -Aɍ&CC  wԭ[WZjߥJRmڴQ:tP[V&RoڴijȑjӦM{rᇻ1;0ݪˋBi@A@B`ݺuVZYgj}[F !K}ԨQ\Gn;^aÆLwGmKA@"E4v .t?(`ڵOG~FP?ڶmںu–/̟?_-]TQd}6_]vvA@A W>[aAѴ lR&.t&0*rNA@G[~[(_ x3K+b A@A@>>rg 9/ V#B7ިoZ0! &5.ZL5qDu%h5UAM!+  1I xqǩgyF%-`GC  @^ 4mׯWC/AjƌcJ`%(衄A@A |4i% iCCPICC ProfileHTǿI/H .lB(!$*+XQHYQp-`[.Ȣ*;#}ϹsΝ;_ G$JHf|=1qL|?(kJDմ>٨nMy.P( < 7㨵sE,Ԁ,L |hS> S|g2'"!G3|'셲 7C99|e[YM/oK q񿕞&~jd_U5K0!8dINEN3W7ieai1&ĸ:pu݋fb l*y&#%4oJS1 H@Ё*'vB@$t KJB eԀ:p-8.k&A/ CTBZ>d YA,0(B+P!TAUP=3t:]P?4 LlυY;G`> y9/Q dh#f DB8$ #F6!ab0N?L$Ĭl”a0͘ ۘ~;Uǚbll ]ccO`/b{83p &\׍xU)R!Y- L"X|q!appp0FT !Dq9q+FA$I$gR)TBj$]$="#:d|K.!!_&?S(&OBrNOyGR n8ju zI&g.ǖɭ+k%Z(/.X>GX W DOjr wFi!銛*^QR+(y+j+ .͓ƥ.8!MOӻ#J6Q˔˕O+1Hclee2Ҙ>+qYn2[M%Q@IG*S[5Uujc5|j{.M4;`au05G545|5D5^i244S4wj֢ih vjzTf3Ә% mum?mvvN:&Ǻ$]nN=- z z,d 6  s  Q\2YƩ{o&&&&7LaS;S99ss5e53י77ns[ZYZK>>WmWkS7]7~)_{Xx=Nx|t\xzxuy+yGzy?4mm`s{U(eOMŁmApЎG8043? [N _~0CGֈFȎ(QQbƬ+mE]`ׂ .2\lѕj^"X<6>:`WN3NHzrws_x;yÉΉEϓdWOAM_JeԐiiMB%aBfƲn(_ԗ阹+sD /$$Yttع.5 v.4je˄ˮ/7YqV`VpWt^ve*UU ;[3[6u,{>z}[F^n?4npP#G]7n^+ZhQX\uw͖K6oIҵnmmm]) Ѽ`]Kv]))M-WXZWkYrYOGySzƊ{x{nuXQYXe`߽*ߪj\MvͳڨΟX?W_ၾ nm ÇypkYcU8"={8:x\x ډfyyHKrK_klkImNm'~1)S姕Oo=C:wflvQssK:9 ].^s|{ΗO]qr*j5km]v]7ot=-[n{ݾt}ZOpOwodサ v̓csa۟GbF߈ߌN݁6;FCG|H0ꧺϬϝ_<[8b('%Xh7 MȓIIP@D./EjnLHn?$IEA'Mw&3>mm3f )>sou_* pHYs  iTXtXML:com.adobe.xmp 755 641 1 &@IDATx}`]Ց{{o4cjP@)6$!d7B,) ^mcp{Xo;,z*d<}sO93g~=p&'==zN~Fprr:=]1i"S!CLRH39jsNkIm{pvY:?| "2U2D8ҳ{ͳSN$=%6ilhjf)((z萾hW%""B%66VAAAb_yib)4M0$Gn)++C99{.پ}<3~%Kϖ ̙3%11QHtvxb{4M@Q*e۶m;?<-naG4vut9 OeDLzH4AIK3ΐLlA7+AN&qA`ùdՋ/(/#uNWߎs/ /t]<\bciήklFi 1zxIP8wIMׯnA93{jEu8riH_i"cqdE6o,<7$Z N=enJGJXzxBv203只U~uuVkjf)$0 og+o 7/H993\yzzꟹLxp?si"E#zzz~_K/bCT|DV/!)7p)Jidy~WK>Zx0 )l9=wHBbGݕ%%%Ibb*bbbG$ @ea4wT :quDZsCC=$T9X]'K3dѬ8I:F0DjԢkpG&ё,E69_%3 jkrTǀŋ%%%E%!!A”K:MpqDvt {E뮻NK(5m\#g΋s%.2@<=\AX6}GIb8A/r Z-<(_pI:{ ;Im-u`dxiwK]m eժUpBIMMH!g4`igz#N9259LiB㷿Jff$\Nhip \5ugKl$\:ArʪU%Y%-Z✼;SB}ܤB[ WUYz3>8MlðGýxq֭r٥IeUL)MΖyiQ.T+H-Uzebl ĶArA#A>ϵ[;EKdסjilE%&_w4awd{n%̟?_}Wb;MdC )׿M7$0arTSdy⍽-" PQy)W흠$/_)%Pȍ-PhA{(,q**%5 m>W,ݸ9h5>~\s5grJ 7b;Md`}1#wuH(DZk0ҕVx )i tZ:ʖ})U !w^LFhԦYFb(}ЯvurZ^ݔ/'4Z"$/P;w|ߑu֩'OԦ8q캌3?ɭ*iiral`,^1-Ruwu-}+/|X >Г)BgKx*pȼH6iX鸅@£&&bE m8T%eFi̚=[~(il-Oub;Md6'yZ:P /Y(A~&ED7#Y5x=gkwr~pauXj-@,.ɑPT􋟇M[x m7ZG}B&w`WKJpkظv}ɹ瞫|H~*i"7xC?|u')//'ːĄXqsRCf:"ē6 Grb/));`XGy5.+Y,~L^R<$#=A#7x|[ߔE S&2 !,`hӊ=--M7[IP06k AD6F4n4JSںeF~Mh,*iL OgB((0;lF(`E0c?`644`oW^yE C4#۫Yϝ`֥RUUmƎ~_ІH}7LWA%U/g/,Yf|gwLk"#u e˗Ut}EVTYES HZ󯩩IwAl 4rgKd 5#Ceu<v%T?p_g?vmu93N>sMd:{bs0mr0CCh t @~VkD/ơGO˳=o}@b+)гf>\#]nsKdfjjjd͚5w^q n=t)U`}N3!g !^da Dѐ0?"L,'Bh#`M'.}rVJT'ci;ij: wIqMGCvo~Z*%J3fza}|.Cn@'c?,P\p2iFk/"9w`~g. >`Xm* xB A d-Pk~@5ta4CwpX75>D39b5pXZBa:Ȁ4\Ww!J`L`$ם`X"=)@wX\"A%Z^a0 xk?(@S nZ0 ݅go ~x=9̌4{hE`SsGd aƍzG|Y1/z3t[RR>C̏;%$эq2wOR `=J+! ,ֵ!z{C<]cQA#sS#[ϒg#ٙyH_ʼy8Ӂ>wk2#na89[ fptENj@ocF!Ԯ.XJYcDEub!(-"rcc|.=/yW;R!I T߶76bHRg i&1a:玓_A V N͐$%΍=1QOxxxlͲ)CS僓N*ڊvcKOEe}B*G?owٰ*Sٷ$)1Nޒk7 s %hlDVv]Z 1t%%pڔ$":;$JgHD}`'˦@a'Ծ7Dff:@5vI ,y ܑ$x Vz$h +^%E~ cm(`(Ne6ݠ(goIܡ3WШ сJ{˱+GA"ťT^$4Fi^8#C3.^܈FT,D$ D`ƶt _cD~(a.k`D(knE6wɄXOfOң< xXπ BH4,>mUm 2Dd"ZmUv7b!f熓&/4t D4$>U/MjrM{e6+ëqw$rBH5_fb!-ob21~Rv 2,j1لrPuK7O}M(%28pzQL5B61"ϴ%]r8(`!6)6& 'Y'[BCCi[x";eoEb ɉSw]_OYgRd]=>z :fBarx}1!KJJl{h -m!ӈ:֕>y6?/UΒW#I\0َ}EMD(knj7{ €l2Y` RN :NF)_0&`jHN}Okq'Q "3VYY)o6>41<8G:%V#&F?G ;)As|>ЖQH›bҬ(s= /`i/2Gsd<.+vųgfSO=ӍN6|H_O+xBcMɪIjDqUgޱ }5;'IHMT$kb4>u|l8vm@O^ȿ9l)QW-}x6]~h*Vs,}jkބ>DH.)x#^WE5Iu7iŵ &)x_N" :cْCºniیG-Jb /}e5.Sǵ܌).<3M#|ٹ">rU?b)OdDCd:͘#C>"uס'r:f]_v"I)*ja&/ϐ-RpMX]j4P_sD5fD?YJ{w'17'n]<)t>I1##:ַ)rg-mi5񲺚*5:6"#tz{熤 5Zu 9 ,9Ľք$edZ߭($TqQrHii^~e#O'RV Qۏ<"͙{G"F`bnr8Dc,P('`jB;w㬗> 5nYϒO?; .L)#HIS^vYG55L Ixωi'e9sqcTNUyy֕yN$w;A\4 =xY!_?oɯ~;ɦV~WB֕VDަ# -l;(\FRcAGH R$t! 88&7HTZQg=sS 6B6OO'!Q6&// 1G[5q}@00#.1$5CPh 'E.X<QciLK}fdlٺ󠼷 ٲuL2Sdق`N$GaCq"ItK+1A^҂M^DqXYf6ƉgL7Rd99|gKN hS";e ?f'varFDFOGm.\, RC6D/E>``43l :@D >hnLsqHJF%O/(ŎI- v՘0&8B(~e?rB`Ik]$'OALMA aqUYDH0eee f*a0#.D;I*j[4&Ȳ#ji Im* EMnY@6f`;W͈H r24D՗}HCbvdUh)O>nvp2Ñh'2=ZdK¡m2PZՄ5O5qڄ^Ψ"mf{'9mmFpm/l] Hon"MOQ ϟIc,ugsĺtXiWI|l٣EN8 .0wuLU'894ԊxeQk#xOiU#IzO}S^ EBJB(.'')6LSƉ:*?sgKsҹJln6#=]Y ,b V z$}0>OI^R"ki&gXڞu#g?{ zm}td88!;oGKD5rɘd=ne,/UN~Z CwOa[\(\<VȭОnsȉY ќ#ӭ$Ej ºU}چCIa|o rB,}\Ky &FR,ӻ06|Tϋb&EMi%.'B;ۺoUQ0/mj,a>G?@:Df7LPŞyҁ )Ѿ3 @S.b| Zׇ<1 BsE/տٳfsN&-t?jpe)b=uƑO+W"6/_L@@0v̎ha])cojCE@GPa^6רusUi`G|jڮtBۧ\,)>F*KdͲTc!(\q_a pyw{  tMzq? s9C% ˑaz B@kP^ZolT|3`̟錴P@zDp+ܔP݈@<S˭w<S#Bd4=yǖΚ͈BCɣ/UŹ섓WtbЀi).cF_ԎB2PwIAaHg 7"C',R)BxϳF .?OWMn =={YDQP kf=Bx FWTUI'FXp?E/nܿI!"vX1΂E{ɨksԍr/Zu,oҚv(4EZ$#0zMvo… uUb;mc?1m'sxJyI|y;\Gw}<\?Aݤ}l-!F&Q^zH`g"ڃ-֠6̌]€1d´&ghGҾPFnaH>u,3 c`*(0 :7Í8yus6<n={sRU :Oh(pf dO <c=}4,,Xw^GPwKXkC`aPK@Dz\fT(eՈ_GjpyCzqJԠDp#LwڦvíU ! @yyR]QÄsiGd8"_Wz]km3bt6էثTgPwЁxP M1BԮ2m Z5aq [k&JʵuS15̰sKfjl>6C`Tk[ $. nU}(3FCCp[zZ}SN@G{t<&M"#裏`E'g"w/ꗭ6~Ca[Hۼ76 `GflX;vJP#pF% P3HF@Q [uXV\$^{L*s"^ b\S txwiT1嚒ħ_#.X}IM$G|Wd:0A 6R1rDi.lxaZ\P[Q'KfƠܕ`#!3>lל)aH1%Y5M_Ҳ硊@0!!AڵKC?XѮM$ KNQcX"L: a_OZak-me)JH)rP%֙}I` H;e[VrRYrK񼓛4sD֭[Ⱦ/L"3 '&$96!#O ps{.xudVZ`dr  )+qLe94^6͌V =‡zHWUUaPPԀ0!I*!Є}@~/e 09rN~&T˒q cR{~ust-Eixk/X6qӳ͐"ꫯڟMc]6DfDEU͈Ȼ8#Tu"xtt7ٛ ?]o`8U~gWZ8ގ;kjʣx܇ܭ,S-?.;vGI\3u!88HܝztmW s5AO `p?^/}%Xۚ:LȬ97K/B~0ohTva[g!㧻jK`r 3S8DTXT2;zDf 걉839R90N,gGCjTSU04!"-pllFA7 kn7HwK_ZÔA1$c"äm46QU$plRUY3=ĤaQ#0E R2C^$ }T^14 0OD仈$&pW>+UU=:5_:oi=N"0RAia54JSkt ,ٺrZ+ U-;)Zℨ &8/ %<<:%>)KAWTm+Lp=ttinOgrxyy36E"ciX[ Bxͽ'N9$[6,R4czt,lBQ:x= Je_sB7Qi!OpL. #MMT_*:Y BB"Ly"3jۨU!'BZ/0G(hD?K]enԦŹOVKPw-/i.8 C0;+:eDNH%+hhi碓^~|B-XGN st-f=Yfi>Tf$۰*S=GKHwRnmlmǜ)Odkjjt=FuR:3)i)u\ύyKsxZ ,@.ƯKf* qL>ʓyr5xM)uGCl<>|lʴXjHoXKLfdOl|7[oM"8 ɘ܏m5Mg4NzF W@S=Bź}D GkGQM]sd7GP(E_xdё 'crBjk(C+bOȪɈd!H<9`!_|f '.j~Uڏ|c)Ub!3d۟}|Fѐq; 94'K4EG꓃?oh".2:p岾`-kjo>3T(|a(6#}PҚS1)9[8*Mf4cZ(̼G'fDw BT:HˈGrIlm;E:C?PhЛc}։(p:fM{Aon-{(!AI BB춸hjER6hMcfM6~$}Pbt8ռsG u4BsTYgy},6D'31q֙r& Uf`;.i!0R05ct ? "rBr5`HhghD :@+f9՗5N+֜&|W6ScڿbKv;!֥ĕ%3ƌ<"~`T G}hvAf,̈ 66FuTt@[810k-F#ǟcx@ 4L:r8S6%vRgp`f"Use%rfg?zZsmG |: r éd-8q(*s"1ܯW:e]5A\\PqjMD> ganzG}l s~aV=4vd5BH AXjԔq2'Bv w)mm(è6D?ZM81Y{8̢BBAl娺<ϣj8rεؗWuP! ~-K {МZ2К@t, T̢"'fz2DA-]+@2a_Ay|hpFixP:(u3_.'iD+UQ 2@1^IղF j|ԁveK>4tfQo2`ѣ B]*Y\ٱN6D 1j V 1pF-K{4~Ʉ]uHlIAjSk23-xP(H^osu2ΚFHLΉvם?G5Q"uւN 68>O[dhD0MՍ}tcj}|"( b"'(neOo[*@p\;"O᩺G`]a{Eh M`̠vf.>$sכDL7S #0nbts.k-'T7 HiwaφȺמ!D .m>o?,vF_byY4NLDJ1Ps38L=ڶ#,W"&@: ͝a\ 6r-TPf°)47&G OR{|n ʉ}jk."cO?Esz8?!ktw|߆>b3rH'2X<9sDbG^< @dd_^MD^gQɾLCzblgfJYjaxklHNΠrY.yLSV yEi 䝭Yr$q5dK1`+"HODwpZp&ע֓w:VKGW_k!'LCB4ʂ-ۏ:'z]S$%a .Evה=G75P~4[gFc)Od\s#^Z.}\er PҰ4Cbam_Э=MS'}^|7G^?9#|ڄY`)n>&LID톝j%f0rϪrηr[}A'26DD5kBsXJ*%==MAV|ka5OE{nQ.G'B66&Jp(2h pߪ:z@>lSJCrvSQCTC>a:ӆ-ʗ>$A'55F?Cf:FPp0_D!@=ÕHN{!ͯX'UW6l9*Cղ:h!C'ms4#YG+:-HB7Hw}qc(BUȹqkyQj?Uc&:CvZeDj=k5Sy_FCd=w馭p#NBiUn<2t-pVrczξcX KuϓMqw؞{'K ϐk}5yyBuBD%O +!RSC:+*;(KށIvD#QSJN>v[78+E'R x\f#f,O/'c‹aIɔђ\! C{* Ӓ88$Btt$0UIz+17ZghϜ0C|*Ęyir$H͈ҙ>c4LH3Eupl M\+IVKd|{-β^)ܱ 7CsF Kbqq @}; K2B"p}HB+YD9uTd UDԗz]4 ,AJƁs1n!099VzGPO`a͔j1J i&o!P[?U I=r>IP\!2/ rQ-*wL9 1tJQB~YBw}$2yٟ#=Q7/B064-:o~wrBV @KO+ז֚馮Uw09r؅x|08r\OS!c#iJ*nˆ5\hoBw~+O-^ $q2ӖL#rɒ%IQ{7P+sj=$?{vd4TQtz|̼ΊJc3bc#R]%3#{o|vy?׀!RBt0XhXg(\*kn$-Y;*@l1|u/6 52s;ʋJL?QtW DA;6?nE^کw0.IJ*7sa؇(\\>5ҎTsZf:w\mKr1x1C=ٲt ͵)>Y8Rq:Z 0_6hMܧ'\X&TdJU^,< 4Tn  ;Of-Byʿ-Yj ޽QoVm.Ej, ֗>م!9OF`pkA& "mOK^ 0ĵ!dJSooHc'B0j]9ZiKd MMMdg=Q=Ύu SD@c_0!EGGQ Fk X bcsDy^}=1!}k_'Jɘ!b_\ݽFcݯ"9w9bw}_<|]ޚ* ;49K3ƌ61(+2~C 'YW"= sd. ͺ{v! DjuD6ȗ]e}ngZ䪫_X#Keudp3kʎ^"y}5\ F]""a(>rmE{E]\aFw}UUUįZ7/9l4M)]xkGys-ZNS D\0K+Ә!7c%ui)'p>F` [u.3Og^kAusa*mm {HQU[ё*O{"3"gNE\dLV.DD#3ҕCs)43 pŇ`K#SIb EgbnPDȈ-O/Q4ȋD</_@q*s3>P1#O U *^cki`WDbaD0;{ck⺏9֭VZKʺV\1aʓOe@Gwt6%EQ4aZ̛7OV*Ӡ?Č4a(F/'%qxՋU&*7%`BpsϤ{bFw;, ! Of˺v Ezyw[lU.+'&YAġu] 0 <ɩ6'G1\{!,Gbr1Fe!~#r8'}LŻ|n< |w ȸd#z#b o˯~+8E#sI5"Hkg٨70/?v1⼻X8`gdt *7kyNo 03bj"PBom XY3.9h,/]2WM!8!U] .{ه4!b;*jTAT@?laLoPcVp1 d80'~'!܎y`T.82%=,zo]#|%-l~,/|-rpS GJKKK~߉Gpl] `ɭF3s(pn' ƤB`~sq$"KjsHlh0]CqbXD(z".T\~ʅ0ݥV }ðwR]O+w蟣͵уN :)H*!:fg/u7bӡ_r wFt@HimhRDFVNe:IdQºIcw+,w*U; *;4*Iy6z\~5CRATX|fٖbCͷ}ʝd[5;Km#Ghr_U7##-U \[ya(=X7ɝrn:v\رɇ1APJ-!2[ nI_x4w:_v[nfڅ _/O>$taFbh$ ˁ2{~+[6o|;*^Y{t6JgvI +>}C 0i=O8$6$9,GƒߣmD\>nQ k$*(\cmiTITO+z4DF$%$Pd9VӒy>ic5rAdn#bakDl*9pFRfg+@2Yj䝍__eeSW,mpSŚ00$4 Y=Km]DFwxK|^1aКwj g^}^g^j#@|.L yp ڝkLA›0bPQ#;Sfhfuuשe2n&әhS-ǀO~r,q1̙3)eW7V@cx6 NmŸKdl|M?^'f?.z{P'cI4G>+6@PjccNC8,DFW7RXqX30݋" {Gq GJ? rHDĽ۵ɡ^+^-(RXVx!VZ_CZlPKPpVJ naF]Q >.]W"_F-!;E&H=Ru 4|H`qgrcK:љ@.AƏD8euCŕlxc 8Vc{=2=&b1T%,y=;eٜ8 7Sqd.%b/˒!/ o,> h=[*HX"ɜ3_JK*W^yYr$#!i\SV#ba8Sn0Q͘؆2LxGy&,^xXbgǃSq I<zZ=.Dܹc#mlD h 2:4>iX;""E,1wC枣v?HHp|χ߱ ǻp2FMQ:'V+SY%YFpJ ܊-hau7eV___{lZ {Ε+o wKk&:*Bܥ \A(C܉%XOZR32΃G79;W @^~!]TˈBpU' |{ӭ>j}Z{l'yٶr̴RRR%V)s ۠{pIDkWBfdHQU 4lhǶ 2P'J{Edp4 ]oQ$| ־=Kc2RUNjTTYt$a CTΞ$߸z<61a̘s2ads4h'@ZP҅9cw}=m&n02;Umהda-!u NfXYuށjIĸ45yG%~՚qJhȆɳ>+?]|㏠%S2m-5ph36sRr4?כ0g.HGոҐgH`cG 2#euwA\X4~䐝K*eZnTTme@<xXMwOxJ=q\s rwhc"kbn59S 䑰C &grޏ9T0#3o 6\w{Grs& va:F cg?Qu/\1&ќ76c㶫Sjog f M@π 'JĘ](˃xd[ǫQۋQ,\ (PQ 7_U(rR[-"TD]*N0'ō'>Ѓ>@% ؿ*Uc(I^bMǃ$P2B1V~$ 6#/D^ @giݠ,̱8a=b"z"kR (2>ܤzEjւsd-sp]yTJDi0 bUkW(^2 Q1WÞ{| ׯ;q$EØ7Vsdΐl5 5I\-Rx̣|Izm7"Guv1"qDmn]-[nݳJ]%[$mG?!2#g@\M.aGfsVABp1*;@OE!ڨzro"7*0Dj4 X=OdժUm6HK’r, !׬'hV'N~< $CTEeGwɳoUqa@t^tݒ _o}񬬬.[mH# {~h Z>$>rXuKUN݀Ƣ UH:ɛH` D њ#g*'h ]0^&j-kHNڊ7y|w[n2ɀhkY`~ x1#'> -IK2{g:#&xP3k L~bGۖU@y]tCC<#qir_GxJyۥ"qbNoI?w2crX|">t 2w|)--o%:2S^#YsjR[[nl΀'ðy%H^Z5={M[!&Y=ܣoMKMCB⽹J@TntO/TCI1<1;rw朦XNL\gT3 ]>S]|,5==B?ټnu%v Sc$2uD%ԷfڵkG_͏?@?K{cڌ&H O7-%B.SflK.i*]Ld8R;0"u+kVJz衇nq lӦMrM7iz$dh?2kL'̠I 8hB)_D$?^Q.[=Kerv}[+-m}$U{R歾A{nUtwˡm(]1M?ԢC"kc\^Cx\&vȧoʒ  )>.V,ɠ~Pft:8冓3Ll|+= JA0͊Y^G?=uϰ\1q=M)9ɾ $`KQ;k^_/N*`O68gH+/E]G{|L(009)Q Eqny":c eO 2_ZD:~`S\ub#~R͖bBQ+cE'$7!Y@z Q (E}=K>l((bw ҫBI( !$7)ͷ7nIdOkS̙9s3.|Ah}}콮4!F!4amʫ2%fZc6i|oZaY,b맠v]ӔqwJ2]3a/"hi0X.h-^zQ;~11{ 2"FhqE VEa:9kf<,7&ϒznۉh Ϩ//,,9JbzXtN~CMK( 9@m+,⠼XE~2$stɍOle"ؾlm9p8͔^]ۧp09,::\]Mm+L_.:k$"~hCʌ^KOam1/GqWMJ _G6?7zo^[w! ޙ~Gl}o(}eRRmm!E=:m N:hN6hFثC:3{p Hfi+t DE I 6Hg7 ,17o^zI>rq`< ƮxgYY{k9HAiْQX;w^F-Ldt. ?y:s$Z+ZWUa{xIETkJzn!:]_ sO+E;'dEA(𢼹j68bp16&B2j38%"_?v{Hv="8 Z ӦM <,]$ՄaLxA[K al9Ũ#%\4^'s~[d:̃Gs }#K{wO)$ӡ^m]{> .h0^>eʔ(O2effE{n6 SǚDO%SvAy) "PmMG%{ &d4H°得c$EC[tyF^EF !}p>Bv٨m/I%1L[Nf; $ԕ4ًpƦޞbyQZ#ړSZX۰_P֔cE`/pu,|t8#$B=$ |Jؚ|MԣDF݁EaO #6dvbQã'cɲ)Bꣻm GOhqHv .뫟 A)NNv ^5:k@;CĿj1 6NH"'MʄC}̽ސCd.֪p Wf^q/T']M(B&(6o1XQ~L{ 6v ˴/ʺQtH@s3$%OԶ{_EoᔸsL/>E O0kV(#~,/t'J *Bg&k~W(JPEވ&OI阸{S?üV@^H>:  ^h^]ڛd|hɲ vU_Q_8#E:Zΐ8Gv: ЂщqˍCo%#=s@_|YMo\(mJ")G6RM_ˤ涜t3v:̾C)Zi*;WN!-SʭJqoPS\=;Pȗ6X 9s,rzcҖKm W]҉ yÛcH (\.EL$ۈ߫ϗuMBj%223q?]*)Q}/6yS*Xt63̕cQ LGľ>zg?y#I,I  Cs 7'~Ttw]f: Y@y1qUR}wر Ec۾,0K>ʢ"o|LZ ptUW>:DlԚ/q$5W;<;ZC{#!{f'BuK}7ձFjY'g{8(e~"ٟ'/$fϏ5"tH4p ,BwK.Gw(AsAx{:b%OfAV P 8S"$>, `tqQl![N~ߘ<],"C?sjzGCpeŵz1дk ]!;ǎ$mIjYK{4bqXCD̔o=c:MkifOg[s&L] d96$gB' ^-Kac*  MHA`8DtU!4am Heٿhs[󯸏#7~ڐFpQ,_O#3q`:"%7LLG'mV٩vH` W_hl.Gĸ/H VߔubeK$0'-H&ra A?6ğ{NY XaO<|=Mi!_9ncncEL{`r;xK֣sh*MNP~GpD Whƈ$ 1αf=4q8|=Y# iN/- XMl {9g=7Nc`SL`  AZ"G{FP!EHjPQ 1`n'4VYOV vzOvqֻ:6rN5MxgAh:VD( $`9PƺHq׀{4 FF=F2I¹+/b"8\V!:,f1ƞ]?n/ +6tZY=s;.g`ɐ!g)#ɔRkΖ͆hHfcFS#FhW3g P4XUO xz 7RUӾG0f/Kď?RS[S:lѮގ>`01BY}@&ˏG) adT3*f"4~NPE+VS&a _X1W'noC2IDAT~D'KCX; T,M9>P8#pM'$!p9]WT A 4PF { M`CZ,B{B#2L'+maEzjs q~.H,T6gP_Kjf3zKBS3h`Ѽ.`^t,p#ef:,kӸ{ hNMO+}:5@Dȷ%{Ɍ"tq( ,wӤB(0P]&!>K\ `bO|BpsCvi/ivuHɓ?xՙEgH-kcI!R!Ip;q 8F~:D--;I^\1RZ/#@I@z- A@t &G!~fԺ Ӭ:3>A`D="{NR%Ȱn˕3Mb提'[gߌ>Qx: i״uUx]9t !dGq?׾馊w)=+qj pf3Űus8XIBE0PFC8R@# C<ૅemKfEdsO]> z*7BKDtC̀. 0[*Gb,:7bHӖvNU5*:+g p2)F`tP+qVό"{p˩,fbx MK˰IǙMvDtP2 /ʁfk~/ (#T|$~D3,#-ry:#8EWm] 0f/bd} hsW_G6le4k4i!I?8cONBɝ;Z=:zb~׭{Gf;[sfjOMd}Xā#j1Io %|Eg[ΜlRt#˧@|%chHqmIg1/`EsZRqBi*fkI|!Ϝ ~f8#4iþ{g$ )BM^^tlmf:=sWW;ublOK鄠B.vb偕(!BȬạ̇̄^S eVuO/N6g_DN]ƾWu>N9e9`\C iR"cSQ[ 2ai"8]7{UUU뮋cxA=C!^u lR!~6'/էNH+!xaaNhdpvg*r"-~NPR2Jl`yyj>k5PH2Tab?ThRvMEO=FرcƖֺ!1r#Ad6>X1 dYmN9DŽ z0WDLf/:bxYp, /1=aVa-{UvY$Tv0HD5T" 8 V3BCV:d{{뼞O~24)8=:dҩFp 0Cܠ",n&"KuxE]3(`mIP_{;ӓ̒cAzRp"imWiCx6wӒ5dFmW Az`\ĜU^],6I;5 ~m cN˴I^{f Q]a[oBÞhLiɩ7E>TvvJф3;Yv}~ O<餴'T~_;# l{ sK􉧬:EZC)$LIXc/pd8pVwfdAհoEi`l&a-Țl&a&+2C {Z ~\9B`"yE`u8$_8Nܹ{ӐɥS$f DXH 4с-rrvN˾"hz)N0ib/ѻEe_uP"ߥޞ25b&965Ahcـ',.t.읦j㏘ &!2#+_z݀mͷ[m)ؚׯ_G7 9 =tY@:}IޮSGqR s>ؿzfQLĄ6-!OTV5GaAފhۿw65 `D^JWM.(6σk.]b- "K7|sMQwZzW9)jȖg87}m|*}G~,qT@h_`_iS+w bˆWwvF8TcCk` i/d:GMӤ3,*t3ҡXffPuh_B\i̎rYGahq?Uw<>ilvbD2IfF,z&&"&45T#>m0!ڌuL Uғc%}fq4^t㎋kc>A#4 NUySZ2Б-9DOG=3򕯄ju]S3JƷv[p@:CU?#L9!筴/9j}'WLCu`:7!}wT4lVj-H%4ez)׾ߖCu-GC8m'H6)DwƭOz0#υ:6&8Ȟ&f|Mg3iQ+R<1w'~8]~wI Bnl)oTCkS.693ӡr^&_#;lf̞5нDEG>y1avQGӨ+!ȣ7d7 {Ǻ؍B4KGvWOc̀4{]8/Gu.ب;{;m5k.ZgO<ԧw/y[.pO>%.mj@C6G 4 yң2q>Ys'23>7=Dn…q:N~%$gqP,|s!8ߖ ïFzJQt@3e~}nj:G?z'L#MK+ TS]{KoR"C}Yg?&h% 9Ŭ=1A u{`+C=?&Rysf~RR~e츰{JGbp[ NNܻx-ID 'T~6鰘{sA*e 0<ef`@Lq-h 6>>?D޳Wӟ-޸pN~m&kyw\7,$o@M t:}ƥB w2x|wPU)Ca,m*. Yozֈg$_ta h⊏kqvɓ(5Q2wFң @Ciяx"щm /0tlPH}&h&?TLdhR"n믿>p ELd;St=nub4WBk+2s2z{n(H?|r1uV/~Pa3#F`=G \yz Q<,bjO,-$G}t <վ>hr"tTf::vO?b}kZG|%M|&o&, Ho5 ]lDǿVh@mhDFMhks{0Y!|J 0O1Y ,A,t>:ttV T@!2 hBcY?sɡ]@F&, X3 ȟF2HkC0ЬC&to؇qz b332k eCC>O3/j]9i<.S>^}+ꃁfGdڝ:4`#9+J;z&3y&l"#_GnŬj Vh*bYgy̅Xϧgy6=#a宜-'<C>EDŽ#ՅfMddXS1sycdc-%RKtV*L@ 2WxU³<>+bu}"bh?Y"xePyEVM+-)(LkRo33B)uukRkP yb<9s*yKR}+- 3I(ws#V'愄r iXuXSmxG+PONޭ,c+4#"3RV?=3|6)?)()iB3!2ڂƛ/鐷U|^·i6+2\k S_րЕ۴toT d0:* &(ޕ-}+4<JD_izN85/~+MU/+S|||'Yo"@&.X)cb:_|2d?6#-{&M|Jxf|"Cׁ?H֐AIEӪ'./YYH78,H>'N7OB]-߰`5aڷK d,"uv 챱FǸү2x 3+Ti̠ƾOߠl\a8/ T/"s tF+UjPF+B^ۉؖ鸡"@g׾V씘@ կrNu~jr϶T΂{8 7OYH3; ^W_K'tR:쳋Hv}\7p__aF~' $oݗ\rI42䯈-| A6{t7ilq,2r>uy督|t0ed?39[uԉ6f4#jq9'nlCsF#82ۮAl_6B%!Pga#>!'J.tuצY3]eAT.hYƟF " R)Q*勰j %#ǃ,j,Q+葖FNq/W5`K_R1lUUUA@<_{FN#w)u䎺:JDkc #ш'Z!>;VeCiqk5D;)Y4ExqG؎8ykP(z ͞^N'p\?OD5; be֩FȜY3^1aÆt[_N?ENK/]!w3PȜ/ Sn: gŋ܏`鸟<",e7ym8\vۊ$TXSO=Up g捕I9rdѣIҀuRqE-1}p`0, $x}Y>Bu,x?ÓqNe]9ЮGiVC=txp<9?Y#j|oP/ "N8JW_]t+;T 8uX"i8>K k7;ekd ?c$ @#69(v?xT l<,#|H|b &|~-}2,i$;,(cHn%7L(%VЂ0+.Xh8v)a=0Taָ"5yvبC]aCWZ`(/|x>S$p)@gn^}f+5e##=мް݆1 21rU{e4ba EWfDf<?2` Nj`p#sEWUeB01[-ydU#]:Uwf ,F t`'i ֎YY+著w˖J(0Y,^.0aB(v6i<#wI0C g(sf #*r333cVDHnY3eϏ4`N;PHh1-sob9jH?IW\9:6gvq $D/CwNW\AѪ_ӆle[)Z,u14F'@J9!Dad(^V}+' ԕ4aM5cSb@9C^R|}O?e-v@* 8u`1|kK [yp%8md[>(KdNJ+,q?PwF1X ѝ20`[^[!-Ȟ 18}{ XSnTW ;*((3{L8H` x)ya'f\>ϛ0aYOaoTRȜu!ȇrn5wij#1w0;o5~@$W[ S/::;I?ee& }땭ە}Axp*~XpLPoffIM1،,@ 㞿B 1=г?K`rM@€И9 mPn<# uvl& H(ΚTṣlzdGCF )ʕޱ\Ka}ThuqǓT'Q f49@ S6 G2"nd¢*JVW1,d]q[wP'y !=f z t id]5xR'1hP@!H93LD`#1O`&CCga6cW_*/'Chh%5 e/M\ޠEו)s\%3LdK) 9GRYGPt{`{: (2֍H|tBrw4cVgk9W 5SWӣ#ulWW\w]}rWXr'w8(*8JU!Z׍%wf5s×^J؀Y¼ᬥ08ҴWsDyÐBPX7&; ӕ3`- PCnh (uKjeO6aa703a9>kfTo1rOZ~ Y^[]8qb+eE1"{^`*o RDfPFxe6+B<,)nY o_k5QɇGYiFX=ang)FnnVY"0; ( {ହVqbZt%_piV iVo9Ϩ}Y"ˇX17w|卄3<%.]22C]|ګޝ4i`EiqR٘hoML28r;lԀl AJ$l'\{lDwܩNVfc5]|Aɏ_{wᎂˀci<3c""|O8/{q y~ "p>/;5^=cC=~$2 ?o 1r?%~k4m+$| ͈#r8]rJÔ|5~!ucsxϬ}sà}pY1uG#q,ԓOE:O㝸2~;߉z_bȜG:0w\?4ҺO&z!]|l‹|$ݎO3c[b#J ;/&?c@[_)qK8^]yŕ!1/؎DkQ+ j &,pjS~eB<^hR{ BpZcEY,GX7ǽ EyQ|.k1ny晈F,jc|/ׁ(uR 5+;G1luYsH;#FŸמh<Ͻ6 ڮvO~Fp Ehܚǽ _8 NB; :}^^]A>N-ԬnmFeOTT}Yg?m2Ju<xb 021BN;T 1~d& 0HdSNÇOrhRL $X(3v #)q(Θޠ{ٯo4la1j+r1RԝR)W.Fz,T`}Ĉ1L}Y"}g{WtXv l$n?(8dg="^@lV! Kpq@K؆ihP?@arUZHRU]$)xv]?F2!M~:v)G^R'[#yPa{ ;U*s>N.t({O)LXQҳKU0i l@rx\,*z%ŴTUV&<*G]iՒM!mJeap[C ?]yW[YjɈ]jK(uu x])|W W#|%X9OF\/܇춡娘Jt Sb^pCC[[9 K k=ɽc=[/x{1Ӎo%m^Ȅ* \zuhjU%-Zk嫌WuZFuifZ}_JA4"0_y4P7&Dg}'ED\W$2qABo岪bpQ|+[ki)/@:Xf!TA݊C & Ql_4u8?OЊu  -,!Z3a+@Єdk~`#t͟6ZЕ&nOu9aeZȄ$-f.@f-DhćːCtH{~+|}Z/Z[1bQ<{1F b~s0z_WSfUς|rmk%ri}vb@BcfF!;,bb6Wc*Ϛ W8&43!Y_#6huh k*=`8P"Z%hC4!F[$2_+BC2,Jy>t` ,|:֕WJK]iifݗ~Z^Bp|c ǟ7y_{rI0Z lIENDB`structlog-18.1.0/docs/code_examples/0000755000076500000240000000000013233032005017617 5ustar hynekstaff00000000000000structlog-18.1.0/docs/code_examples/flask_/0000755000076500000240000000000013233032005021056 5ustar hynekstaff00000000000000structlog-18.1.0/docs/code_examples/flask_/webapp.py0000644000076500000240000000176512751555250022737 0ustar hynekstaff00000000000000import logging import sys import uuid import flask import structlog from some_module import some_function logger = structlog.get_logger() app = flask.Flask(__name__) @app.route("/login", methods=["POST", "GET"]) def some_route(): log = logger.new( request_id=str(uuid.uuid4()), ) # do something # ... log.info("user logged in", user="test-user") # gives you: # event='user logged in' request_id='ffcdc44f-b952-4b5f-95e6-0f1f3a9ee5fd' user='test-user' # ... some_function() # ... return "logged in!" if __name__ == "__main__": logging.basicConfig( format="%(message)s", stream=sys.stdout, level=logging.INFO, ) structlog.configure( processors=[ structlog.processors.KeyValueRenderer( key_order=["event", "request_id"], ), ], context_class=structlog.threadlocal.wrap_dict(dict), logger_factory=structlog.stdlib.LoggerFactory(), ) app.run() structlog-18.1.0/docs/code_examples/flask_/some_module.py0000644000076500000240000000043713231353733023760 0ustar hynekstaff00000000000000from structlog import get_logger logger = get_logger() def some_function(): # later then: logger.error("user did something", something="shot_in_foot") # gives you: # event='user did something' request_id='ffcdc44f-b952-4b5f-95e6-0f1f3a9ee5fd' something='shot_in_foot' structlog-18.1.0/docs/code_examples/processors/0000755000076500000240000000000013233032005022021 5ustar hynekstaff00000000000000structlog-18.1.0/docs/code_examples/processors/dropper.py0000644000076500000240000000014412215562225024060 0ustar hynekstaff00000000000000from structlog import DropEvent def dropper(logger, method_name, event_dict): raise DropEvent structlog-18.1.0/docs/code_examples/processors/conditional_dropper.py0000644000076500000240000000123113231350532026434 0ustar hynekstaff00000000000000from structlog import DropEvent class ConditionalDropper(object): def __init__(self, peer_to_ignore): self._peer_to_ignore = peer_to_ignore def __call__(self, logger, method_name, event_dict): """ >>> cd = ConditionalDropper("127.0.0.1") >>> cd(None, None, {"event": "foo", "peer": "10.0.0.1"}) {'peer': '10.0.0.1', 'event': 'foo'} >>> cd(None, None, {"event": "foo", "peer": "127.0.0.1"}) Traceback (most recent call last): ... DropEvent """ if event_dict.get("peer") == self._peer_to_ignore: raise DropEvent else: return event_dict structlog-18.1.0/docs/code_examples/processors/timestamper.py0000644000076500000240000000024212713565206024743 0ustar hynekstaff00000000000000import calendar import time def timestamper(logger, log_method, event_dict): event_dict["timestamp"] = calendar.timegm(time.gmtime()) return event_dict structlog-18.1.0/docs/code_examples/getting-started/0000755000076500000240000000000013233032005022724 5ustar hynekstaff00000000000000structlog-18.1.0/docs/code_examples/getting-started/imaginary_web.py0000644000076500000240000000077413231346705026140 0ustar hynekstaff00000000000000from structlog import get_logger log = get_logger() def view(request): user_agent = request.get("HTTP_USER_AGENT", "UNKNOWN") peer_ip = request.client_addr if something: log.msg("something", user_agent=user_agent, peer_ip=peer_ip) return "something" elif something_else: log.msg("something_else", user_agent=user_agent, peer_ip=peer_ip) return "something_else" else: log.msg("else", user_agent=user_agent, peer_ip=peer_ip) return "else" structlog-18.1.0/docs/code_examples/getting-started/imaginary_web_better.py0000644000076500000240000000076313231346721027501 0ustar hynekstaff00000000000000from structlog import get_logger logger = get_logger() def view(request): log = logger.bind( user_agent=request.get("HTTP_USER_AGENT", "UNKNOWN"), peer_ip=request.client_addr, ) foo = request.get("foo") if foo: log = log.bind(foo=foo) if something: log.msg("something") return "something" elif something_else: log.msg("something_else") return "something_else" else: log.msg("else") return "else" structlog-18.1.0/docs/code_examples/twisted_echo.py0000644000076500000240000000175113231350705022666 0ustar hynekstaff00000000000000import sys import uuid import structlog import twisted from twisted.internet import protocol, reactor logger = structlog.getLogger() class Counter(object): i = 0 def inc(self): self.i += 1 def __repr__(self): return str(self.i) class Echo(protocol.Protocol): def connectionMade(self): self._counter = Counter() self._log = logger.new( connection_id=str(uuid.uuid4()), peer=self.transport.getPeer().host, count=self._counter, ) def dataReceived(self, data): self._counter.inc() log = self._log.bind(data=data) self.transport.write(data) log.msg("echoed data!") if __name__ == "__main__": structlog.configure( processors=[structlog.twisted.EventAdapter()], logger_factory=structlog.twisted.LoggerFactory(), ) twisted.python.log.startLogging(sys.stderr) reactor.listenTCP(1234, protocol.Factory.forProtocol(Echo)) reactor.run() structlog-18.1.0/docs/development.rst0000644000076500000240000000260213231351150020066 0ustar hynekstaff00000000000000Development =========== To make development a more pleasurable experience, ``structlog`` comes with the :mod:`structlog.dev` module. The highlight is :class:`structlog.dev.ConsoleRenderer` that offers nicely aligned and colorful console output while in development: .. figure:: _static/console_renderer.png :alt: Colorful console output by ConsoleRenderer. To use it, just add it as a renderer to your processor chain. It will recognize logger names, log levels, time stamps, stack infos, and tracebacks as produced by ``structlog``'s processors and render them in special ways. ``structlog``'s default configuration already uses it, but if you want to use it along with standard library logging, we suggest the following configuration: .. code-block:: python import structlog structlog.configure( processors=[ structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M.%S"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.dev.ConsoleRenderer() # <=== ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) structlog-18.1.0/docs/loggers.rst0000644000076500000240000001127013231347040017212 0ustar hynekstaff00000000000000Loggers ======= Bound Loggers ------------- The center of ``structlog`` is the immutable log wrapper :class:`~structlog.BoundLogger`. All it does is: - Keep a *context dictionary* and a *logger* that it's wrapping, - recreate itself with (optional) *additional* context data (the :func:`~structlog.BoundLogger.bind` and :func:`~structlog.BoundLogger.new` methods), - recreate itself with *less* data (:func:`~structlog.BoundLogger.unbind`), - and finally relay *all* other method calls to the wrapped logger\ [*]_ after processing the log entry with the configured chain of :ref:`processors `. You won't be instantiating it yourself though. For that there is the :func:`structlog.wrap_logger` function (or the convenience function :func:`structlog.get_logger` we'll discuss in a minute): .. _proc: .. doctest:: >>> from structlog import wrap_logger >>> class PrintLogger(object): ... def msg(self, message): ... print(message) >>> def proc(logger, method_name, event_dict): ... print("I got called with", event_dict) ... return repr(event_dict) >>> log = wrap_logger(PrintLogger(), processors=[proc], context_class=dict) >>> log2 = log.bind(x=42) >>> log == log2 False >>> log.msg("hello world") I got called with {'event': 'hello world'} {'event': 'hello world'} >>> log2.msg("hello world") I got called with {'x': 42, 'event': 'hello world'} {'x': 42, 'event': 'hello world'} >>> log3 = log2.unbind("x") >>> log == log3 True >>> log3.msg("nothing bound anymore", foo="but you can structure the event too") I got called with {'foo': 'but you can structure the event too', 'event': 'nothing bound anymore'} {'foo': 'but you can structure the event too', 'event': 'nothing bound anymore'} As you can see, it accepts one mandatory and a few optional arguments: **logger** The one and only positional argument is the logger that you want to wrap and to which the log entries will be proxied. If you wish to use a :ref:`configured logger factory `, set it to `None`. **processors** A list of callables that can :ref:`filter, mutate, and format ` the log entry before it gets passed to the wrapped logger. Default is ``[``:func:`~structlog.processors.format_exc_info`, :class:`~structlog.processors.KeyValueRenderer`\ ``]``. **context_class** The class to save your context in. Particularly useful for :ref:`thread local context storage `. On Python versions that have ordered dictionaries (Python 3.6+, PyPy) the default is a plain :class:`dict`. For everything else it's :class:`collections.OrderedDict`. Additionally, the following arguments are allowed too: **wrapper_class** A class to use instead of :class:`~structlog.BoundLogger` for wrapping. This is useful if you want to sub-class BoundLogger and add custom logging methods. BoundLogger's bind/new methods are sub-classing friendly so you won't have to re-implement them. Please refer to the :ref:`related example ` for how this may look. **initial_values** The values that new wrapped loggers are automatically constructed with. Useful for example if you want to have the module name as part of the context. .. note:: Free your mind from the preconception that log entries have to be serialized to strings eventually. All ``structlog`` cares about is a *dictionary* of *keys* and *values*. What happens to it depends on the logger you wrap and your processors alone. This gives you the power to log directly to databases, log aggregation servers, web services, and whatnot. Printing and Testing -------------------- To save you the hassle and slowdown of using standard library's ``logging`` for standard out logging, ``structlog`` ships a :class:`~structlog.PrintLogger` that can log into arbitrary files -- including standard out (which is the default if no file is passed into the constructor): .. doctest:: >>> from structlog import PrintLogger >>> PrintLogger().info("hello world!") hello world! Additionally -- mostly for unit testing -- ``structlog`` also ships with a logger that just returns whatever it gets passed into it: :class:`~structlog.ReturnLogger`. .. doctest:: >>> from structlog import ReturnLogger >>> ReturnLogger().msg(42) == 42 True >>> obj = ["hi"] >>> ReturnLogger().msg(obj) is obj True >>> ReturnLogger().msg("hello", when="again") (('hello',), {'when': 'again'}) .. [*] Since this is slightly magicy, ``structlog`` comes with concrete loggers for the :doc:`standard-library` and :doc:`twisted` that offer you explicit APIs for the supported logging methods but behave identically like the generic BoundLogger otherwise. structlog-18.1.0/docs/make.bat0000644000076500000240000001175612204211413016425 0ustar hynekstaff00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\structlog.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\structlog.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end structlog-18.1.0/docs/license.rst0000644000076500000240000000141613231316321017171 0ustar hynekstaff00000000000000License and Hall of Fame ======================== ``structlog`` is licensed both under the `Apache License, Version 2 `_ and the `MIT license `_. The reason for that is to be both protected against patent claims by own contributors and still allow the usage within GPLv2 software. For more legal details, see `this issue `_ on the bug tracker of PyCA's cryptography. The full license texts can be also found in the source code repository: - `Apache License 2.0 `_ - `MIT `_ .. _authors: .. include:: ../AUTHORS.rst structlog-18.1.0/docs/thread-local.rst0000644000076500000240000001406613231347262020123 0ustar hynekstaff00000000000000.. _threadlocal: Thread Local Context ==================== .. testsetup:: * import structlog structlog.configure( processors=[structlog.processors.KeyValueRenderer()], ) .. testcleanup:: * import structlog structlog.reset_defaults() Immutability ------------ You should call some functions with some arguments. ---David Reid The behavior of copying itself, adding new values, and returning the result is useful for applications that keep somehow their own context using classes or closures. Twisted is a :ref:`fine example ` for that. Another possible approach is passing wrapped loggers around or log only within your view where you gather errors and events using return codes and exceptions. If you are willing to do that, you should stick to it because `immutable state `_ is a very good thing\ [*]_. Sooner or later, global state and mutable data lead to unpleasant surprises. However, in the case of conventional web development, we realize that passing loggers around seems rather cumbersome, intrusive, and generally against the mainstream culture. And since it's more important that people actually *use* ``structlog`` than to be pure and snobby, ``structlog`` contains a dirty but convenient trick: thread local context storage which you may already know from `Flask `_: Thread local storage makes your logger's context global but *only within the current thread*\ [*]_. In the case of web frameworks this usually means that your context becomes global to the current request. The following explanations may sound a bit confusing at first but the :ref:`Flask example ` illustrates how simple and elegant this works in practice. Wrapped Dicts ------------- In order to make your context thread local, ``structlog`` ships with a function that can wrap any dict-like class to make it usable for thread local storage: :func:`structlog.threadlocal.wrap_dict`. Within one thread, every instance of the returned class will have a *common* instance of the wrapped dict-like class: .. doctest:: >>> from structlog.threadlocal import wrap_dict >>> WrappedDictClass = wrap_dict(dict) >>> d1 = WrappedDictClass({"a": 1}) >>> d2 = WrappedDictClass({"b": 2}) >>> d3 = WrappedDictClass() >>> d3["c"] = 3 >>> d1 is d3 False >>> d1 == d2 == d3 == WrappedDictClass() True >>> d3 # doctest: +ELLIPSIS To enable thread local context use the generated class as the context class:: configure(context_class=WrappedDictClass) .. note:: Creation of a new ``BoundLogger`` initializes the logger's context as ``context_class(initial_values)``, and then adds any values passed via ``.bind()``. As all instances of a wrapped dict-like class share the same data, in the case above, the new logger's context will contain all previously bound values in addition to the new ones. :func:`structlog.threadlocal.wrap_dict` returns always a completely *new* wrapped class: .. doctest:: >>> from structlog.threadlocal import wrap_dict >>> WrappedDictClass = wrap_dict(dict) >>> AnotherWrappedDictClass = wrap_dict(dict) >>> WrappedDictClass() != AnotherWrappedDictClass() True >>> WrappedDictClass.__name__ # doctest: +SKIP WrappedDict-41e8382d-bee5-430e-ad7d-133c844695cc >>> AnotherWrappedDictClass.__name__ # doctest: +SKIP WrappedDict-e0fc330e-e5eb-42ee-bcec-ffd7bd09ad09 In order to be able to bind values temporarily to a logger, :mod:`structlog.threadlocal` comes with a `context manager `_: :func:`~structlog.threadlocal.tmp_bind`\ : .. testsetup:: ctx from structlog import PrintLogger, wrap_logger from structlog.threadlocal import tmp_bind, wrap_dict WrappedDictClass = wrap_dict(dict) log = wrap_logger(PrintLogger(), context_class=WrappedDictClass) .. doctest:: ctx >>> log.bind(x=42) # doctest: +ELLIPSIS , ...)> >>> log.msg("event!") x=42 event='event!' >>> with tmp_bind(log, x=23, y="foo") as tmp_log: ... tmp_log.msg("another event!") x=23 y='foo' event='another event!' >>> log.msg("one last event!") x=42 event='one last event!' The state before the ``with`` statement is saved and restored once it's left. If you want to detach a logger from thread local data, there's :func:`structlog.threadlocal.as_immutable`. Downsides & Caveats ------------------- The convenience of having a thread local context comes at a price though: .. warning:: - If you can't rule out that your application re-uses threads, you *must* remember to **initialize your thread local context** at the start of each request using :func:`~structlog.BoundLogger.new` (instead of :func:`~structlog.BoundLogger.bind`). Otherwise you may start a new request with the context still filled with data from the request before. - **Don't** stop assigning the results of your ``bind()``\ s and ``new()``\ s! **Do**:: log = log.new(y=23) log = log.bind(x=42) **Don't**:: log.new(y=23) log.bind(x=42) Although the state is saved in a global data structure, you still need the global wrapped logger produce a real bound logger. Otherwise each log call will result in an instantiation of a temporary BoundLogger. See :ref:`configuration` for more details. The general sentiment against thread locals is that they're hard to test. In this case we feel like this is an acceptable trade-off. You can easily write deterministic tests using a call-capturing processor if you use the API properly (cf. warning above). This big red box is also what separates immutable local from mutable global data. .. [*] In the spirit of Python's 'consenting adults', ``structlog`` doesn't enforce the immutability with technical means. However, if you don't meddle with undocumented data, the objects can be safely considered immutable. .. [*] Special care has been taken to detect and support greenlets properly. structlog-18.1.0/docs/standard-library.rst0000644000076500000240000003253313231351223021015 0ustar hynekstaff00000000000000Standard Library Logging ======================== Ideally, ``structlog`` should be able to be used as a drop-in replacement for standard library's :mod:`logging` by wrapping it. In other words, you should be able to replace your call to :func:`logging.getLogger` by a call to :func:`structlog.get_logger` and things should keep working as before (if ``structlog`` is configured right, see :ref:`stdlib-config` below). If you run into incompatibilities, it is a *bug* so please take the time to `report it `_! If you're a heavy :mod:`logging` user, your `help `_ to ensure a better compatibility would be highly appreciated! Just Enough ``logging`` ----------------------- If you want to use ``structlog`` with :mod:`logging`, you still have to have at least fleeting understanding on how the standard library operates because ``structlog`` will *not* do any magic things in the background for you. Most importantly you have *configure* the :mod:`logging` system *additionally* to configuring ``structlog``. Usually it is enough to use:: import logging import sys logging.basicConfig( format="%(message)s", stream=sys.stdout, level=logging.INFO, ) This will send all log messages with the `log level `_ ``logging.INFO`` and above (that means that e.g. :func:`logging.debug` calls are ignored) to standard out without any special formatting by the standard library. If you require more complex behavior, please refer to the standard library's :mod:`logging` documentation. Concrete Bound Logger --------------------- To make ``structlog``'s behavior less magicy, it ships with a standard library-specific wrapper class that has an explicit API instead of improvising: :class:`structlog.stdlib.BoundLogger`. It behaves exactly like the generic :class:`structlog.BoundLogger` except: - it's slightly faster due to less overhead, - has an explicit API that mirrors the log methods of standard library's :class:`logging.Logger`, - hence causing less cryptic error messages if you get method names wrong. Processors ---------- ``structlog`` comes with a few standard library-specific processors: :func:`~structlog.stdlib.render_to_log_kwargs`: Renders the event dictionary into keyword arguments for :func:`logging.log` that attaches everything except the `event` field to the *extra* argument. This is useful if you want to render your log entries entirely within :mod:`logging`. :func:`~structlog.stdlib.filter_by_level`: Checks the log entry's log level against the configuration of standard library's logging. Log entries below the threshold get silently dropped. Put it at the beginning of your processing chain to avoid expensive operations happen in the first place. :func:`~structlog.stdlib.add_logger_name`: Adds the name of the logger to the event dictionary under the key ``logger``. :func:`~structlog.stdlib.add_log_level`: Adds the log level to the event dictionary under the key ``level``. :class:`~structlog.stdlib.PositionalArgumentsFormatter`: This processes and formats positional arguments (if any) passed to log methods in the same way the ``logging`` module would do, e.g. ``logger.info("Hello, %s", name)``. ``structlog`` also comes with :class:`~structlog.stdlib.ProcessorFormatter` which is a :class:`logging.Formatter` that enables you to format non-``structlog`` log entries using ``structlog`` renderers *and* multiplex ``structlog``’s output with different renderers (see below for an example). .. _stdlib-config: Suggested Configurations ------------------------ Depending *where* you'd like to do your formatting, you can take one of three approaches: Rendering Using :mod:`logging`-based Formatters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python import structlog structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.render_to_log_kwargs, ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) Now you have the event dict available within each log record. If you want all your log entries (i.e. also those not from your app/``structlog``) to be formatted as JSON, you can use the `python-json-logger library `_: .. code-block:: python import logging import sys from pythonjsonlogger import jsonlogger handler = logging.StreamHandler(sys.stdout) handler.setFormatter(jsonlogger.JsonFormatter()) root_logger = logging.getLogger() root_logger.addHandler(handler) Now both ``structlog`` and ``logging`` will emit JSON logs: .. code-block:: pycon >>> structlog.get_logger("test").warning("hello") {"message": "hello", "logger": "test", "level": "warning"} >>> logging.getLogger("test").warning("hello") {"message": "hello"} Rendering Using ``structlog``-based Formatters Within :mod:`logging` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``structlog`` comes with a :class:`~structlog.stdlib.ProcessorFormatter` that can be used as a :class:`~logging.Formatter` in any stdlib :mod:`Handler ` object. The :class:`~structlog.stdlib.ProcessorFormatter` has two parts to its API: #. The :meth:`~structlog.stdlib.ProcessorFormatter.wrap_for_formatter` method must be used as the last processor in :func:`structlog.configure`, it converts the the processed event dict to something that the ``ProcessorFormatter`` understands. #. The :class:`~structlog.stdlib.ProcessorFormatter` itself, which can wrap any ``structlog`` renderer to handle the output of both ``structlog`` and standard library events. Thus, the simplest possible configuration looks like the following: .. code-block:: python import logging import structlog structlog.configure( processors=[ structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), ) formatter = structlog.stdlib.ProcessorFormatter( processor=structlog.dev.ConsoleRenderer(), ) handler = logging.StreamHandler() handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.addHandler(handler) root_logger.setLevel(logging.INFO) which will allow both of these to work in other modules: .. code-block:: pycon >>> import logging >>> import structlog >>> logging.getLogger("stdlog").info("woo") woo >>> structlog.get_logger("structlog").info("amazing", events="oh yes") amazing events=oh yes Of course, you probably want timestamps and log levels in your output. The :class:`~structlog.stdlib.ProcessorFormatter` has a ``foreign_pre_chain`` argument which is responsible for adding properties to events from the standard library -- i.e. that do not originate from a ``structlog`` logger -- and which should in general match the ``processors`` argument to :func:`structlog.configure` so you get a consistent output. For example, to add timestamps, log levels, and traceback handling to your logs you should do: .. code-block:: python timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S") shared_processors = [ structlog.stdlib.add_log_level, timestamper, ] structlog.configure( processors=shared_processors + [ structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) formatter = structlog.stdlib.ProcessorFormatter( processor=structlog.dev.ConsoleRenderer(), foreign_pre_chain=shared_processors, ) which (given the same ``logging.*`` calls as in the previous example) will result in: .. code-block:: pycon >>> logging.getLogger("stdlog").info("woo") 2017-03-06 14:59:20 [info ] woo >>> structlog.get_logger("structlog").info("amazing", events="oh yes") 2017-03-06 14:59:20 [info ] amazing events=oh yes This allows you to set up some sophisticated logging configurations. For example, to use the standard library's :func:`~logging.config.dictConfig` to log colored logs to the console and plain logs to a file you could do: .. code-block:: python import logging.config import structlog timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S") pre_chain = [ # Add the log level and a timestamp to the event_dict if the log entry # is not from structlog. structlog.stdlib.add_log_level, timestamper, ] logging.config.dictConfig({ "version": 1, "disable_existing_loggers": False, "formatters": { "plain": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(colors=False), "foreign_pre_chain": pre_chain, }, "colored": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(colors=True), "foreign_pre_chain": pre_chain, }, }, "handlers": { "default": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "colored", }, "file": { "level": "DEBUG", "class": "logging.handlers.WatchedFileHandler", "filename": "test.log", "formatter": "plain", }, }, "loggers": { "": { "handlers": ["default", "file"], "level": "DEBUG", "propagate": True, }, } }) structlog.configure( processors=[ structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), timestamper, structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) This defines two formatters: one plain and one colored. Both are run for each log entry. Log entries that do not originate from ``structlog``, are additionally pre-processed using a cached ``timestamper`` and :func:`~structlog.stdlib.add_log_level`. .. code-block:: pycon >>> logging.getLogger().warning("bar") 2017-03-06 11:49:27 [warning ] bar >>> structlog.get_logger("structlog").warning("foo", x=42) 2017-03-06 11:49:32 [warning ] foo x=42 >>> print(open("test.log").read()) 2017-03-06 11:49:27 [warning ] bar 2017-03-06 11:49:32 [warning ] foo x=42 (sadly, you have to imagine the colors in the first two outputs) If you leave ``foreign_pre_chain`` `None`, formatting will be left to :mod:`logging`. Meaning: you can define a ``format`` for :class:`~structlog.stdlib.ProcessorFormatter` too! Rendering Within ``structlog`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A basic configuration to output structured logs in JSON format looks like this: .. code-block:: python import structlog structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) (if you're still runnning Python 2, replace :class:`~structlog.processors.UnicodeDecoder` through :class:`~structlog.processors.UnicodeEncoder`) To make your program behave like a proper `12 factor app`_ that outputs only JSON to ``stdout``, configure the ``logging`` module like this:: import logging import sys logging.basicConfig( format="%(message)s", stream=sys.stdout, level=logging.INFO, ) In this case *only* your own logs are formatted as JSON: .. code-block:: pycon >>> structlog.get_logger("test").warning("hello") {"event": "hello", "logger": "test", "level": "warning", "timestamp": "2017-03-06T07:39:09.518720Z"} >>> logging.getLogger("test").warning("hello") hello .. _`12 factor app`: https://12factor.net/logs structlog-18.1.0/docs/changelog.rst0000644000076500000240000000003612627571654017517 0ustar hynekstaff00000000000000.. include:: ../CHANGELOG.rst structlog-18.1.0/docs/api.rst0000644000076500000240000001226613231351734016334 0ustar hynekstaff00000000000000.. _api: API Reference ============= .. note:: The examples here use a very simplified configuration using the minimalistic :class:`structlog.processors.KeyValueRenderer` for brewity and to enable doctests. The output is going to be different (nicer!) with default configuration. .. testsetup:: * import structlog structlog.configure( processors=[structlog.processors.KeyValueRenderer()], ) .. testcleanup:: * import structlog structlog.reset_defaults() .. module:: structlog :mod:`structlog` Package ------------------------ .. autofunction:: get_logger .. autofunction:: getLogger .. autofunction:: wrap_logger .. autofunction:: configure .. autofunction:: configure_once .. autofunction:: reset_defaults .. autofunction:: is_configured .. autofunction:: get_config .. autoclass:: BoundLogger :members: new, bind, unbind .. autoclass:: PrintLogger :members: msg, err, debug, info, warning, error, critical, log, failure .. autoclass:: PrintLoggerFactory .. autoclass:: ReturnLogger :members: msg, err, debug, info, warning, error, critical, log, failure .. autoclass:: ReturnLoggerFactory .. autoexception:: DropEvent .. autoclass:: BoundLoggerBase :members: new, bind, unbind, _logger, _process_event, _proxy_to_logger :mod:`dev` Module ----------------- .. automodule:: structlog.dev .. autoclass:: ConsoleRenderer :members: get_default_level_styles :mod:`threadlocal` Module ------------------------- .. automodule:: structlog.threadlocal .. autofunction:: wrap_dict .. autofunction:: tmp_bind(logger, **tmp_values) >>> from structlog import wrap_logger, PrintLogger >>> from structlog.threadlocal import tmp_bind, wrap_dict >>> logger = wrap_logger(PrintLogger(), context_class=wrap_dict(dict)) >>> with tmp_bind(logger, x=5) as tmp_logger: ... logger = logger.bind(y=3) ... tmp_logger.msg("event") x=5 y=3 event='event' >>> logger.msg("event") event='event' .. autofunction:: as_immutable .. _procs: :mod:`processors` Module ------------------------ .. automodule:: structlog.processors .. autoclass:: JSONRenderer .. doctest:: >>> from structlog.processors import JSONRenderer >>> JSONRenderer(sort_keys=True)(None, None, {"a": 42, "b": [1, 2, 3]}) '{"a": 42, "b": [1, 2, 3]}' Bound objects are attempted to be serialize using a ``__structlog__`` method. If none is defined, ``repr()`` is used: .. doctest:: >>> class C1(object): ... def __structlog__(self): ... return ["C1!"] ... def __repr__(self): ... return "__structlog__ took precedence" >>> class C2(object): ... def __repr__(self): ... return "No __structlog__, so this is used." >>> from structlog.processors import JSONRenderer >>> JSONRenderer(sort_keys=True)(None, None, {"c1": C1(), "c2": C2()}) '{"c1": ["C1!"], "c2": "No __structlog__, so this is used."}' Please note that additionally to strings, you can also return any type the standard library JSON module knows about -- like in this example a list. .. autoclass:: KeyValueRenderer .. doctest:: >>> from structlog.processors import KeyValueRenderer >>> KeyValueRenderer(sort_keys=True)(None, None, {"a": 42, "b": [1, 2, 3]}) 'a=42 b=[1, 2, 3]' >>> KeyValueRenderer(key_order=["b", "a"])(None, None, ... {"a": 42, "b": [1, 2, 3]}) 'b=[1, 2, 3] a=42' .. autoclass:: UnicodeDecoder .. autoclass:: UnicodeEncoder .. autofunction:: format_exc_info .. doctest:: >>> from structlog.processors import format_exc_info >>> try: ... raise ValueError ... except ValueError: ... format_exc_info(None, None, {"exc_info": True}) # doctest: +ELLIPSIS {'exception': 'Traceback (most recent call last):... .. autoclass:: StackInfoRenderer .. autoclass:: ExceptionPrettyPrinter .. autoclass:: TimeStamper(fmt=None, utc=True) .. doctest:: >>> from structlog.processors import TimeStamper >>> TimeStamper()(None, None, {}) # doctest: +SKIP {'timestamp': 1378994017} >>> TimeStamper(fmt="iso")(None, None, {}) # doctest: +SKIP {'timestamp': '2013-09-12T13:54:26.996778Z'} >>> TimeStamper(fmt="%Y", key="year")(None, None, {}) # doctest: +SKIP {'year': '2013'} :mod:`stdlib` Module -------------------- .. automodule:: structlog.stdlib .. autoclass:: BoundLogger :members: bind, unbind, new, debug, info, warning, warn, error, critical, exception, log .. autoclass:: LoggerFactory :members: __call__ .. autofunction:: render_to_log_kwargs .. autofunction:: filter_by_level .. autofunction:: add_log_level .. autofunction:: add_logger_name .. autoclass:: PositionalArgumentsFormatter .. autoclass:: ProcessorFormatter :members: wrap_for_formatter :mod:`twisted` Module --------------------- .. automodule:: structlog.twisted .. autoclass:: BoundLogger :members: bind, unbind, new, msg, err .. autoclass:: LoggerFactory :members: __call__ .. autoclass:: EventAdapter .. autoclass:: JSONRenderer .. autofunction:: plainJSONStdOutLogger .. autofunction:: JSONLogObserverWrapper .. autoclass:: PlainFileLogObserver structlog-18.1.0/docs/performance.rst0000644000076500000240000000422513231353635020062 0ustar hynekstaff00000000000000Performance =========== ``structlog``'s default configuration tries to be as unsurprising and not confusing to new developers as possible. Some of the choices made come with an avoidable performance price tag -- although its impact is debatable. Here are a few hints how to get most out of ``structlog`` in production: #. Use plain `dict`\ s as context classes. Python is full of them and they are highly optimized:: configure(context_class=dict) If you don't use automated parsing (you should!) and need predictable order of your keys for some reason, use the `key_order` argument of :class:`~structlog.processors.KeyValueRenderer`. #. Use a specific wrapper class instead of the generic one. ``structlog`` comes with ones for the :doc:`standard-library` and for :doc:`twisted`:: configure(wrapper_class=structlog.stdlib.BoundLogger) :doc:`Writing own wrapper classes ` is straightforward too. #. Avoid (frequently) calling log methods on loggers you get back from :func:`structlog.wrap_logger` and :func:`structlog.get_logger`. Since those functions are usually called in module scope and thus before you are able to configure them, they return a proxy that assembles the correct logger on demand. Create a local logger if you expect to log frequently without binding:: logger = structlog.get_logger() def f(): log = logger.bind() for i in range(1000000000): log.info("iterated", i=i) #. Set the `cache_logger_on_first_use` option to `True` so the aforementioned on-demand loggers will be assembled only once and cached for future uses:: configure(cache_logger_on_first_use=True) This has the only drawback is that later calls on :func:`~structlog.configure` don't have any effect on already cached loggers -- that shouldn't matter outside of testing though. #. Use a faster JSON serializer than the standard library. Possible alternatives are among others simplejson_ or RapidJSON_ (Python 3 only):: structlog.processors.JSONRenderer(serializer=rapidjson.dumps) .. _simplejson: https://simplejson.readthedocs.io/ .. _RapidJSON: https://pypi.org/project/python-rapidjson/ structlog-18.1.0/docs/getting-started.rst0000644000076500000240000001703113231346655020671 0ustar hynekstaff00000000000000.. _getting-started: Getting Started =============== .. _install: Installation ------------ ``structlog`` can be easily installed using:: $ pip install structlog If you'd like colorful output in development (you know you do!), install using:: $ pip install structlog colorama Your First Log Entry -------------------- A lot of effort went into making ``structlog`` accessible without reading pages of documentation. And indeed, the simplest possible usage looks like this: .. doctest:: >>> import structlog >>> log = structlog.get_logger() >>> log.msg("greeted", whom="world", more_than_a_string=[1, 2, 3]) # doctest: +SKIP 2016-09-17 10:13.45 greeted more_than_a_string=[1, 2, 3] whom='world' Here, ``structlog`` takes full advantage of its hopefully useful default settings: - Output is sent to `standard out`_ instead of exploding into the user's face or doing nothing. - All keywords are formatted using :class:`structlog.dev.ConsoleRenderer`. That in turn uses `repr()`_ to serialize all values to strings. Thus, it's easy to add support for logging of your own objects\ [*]_. - If you have `colorama `_ installed, it's rendered in nice :doc:`colors `. It should be noted that even in most complex logging setups the example would still look just like that thanks to :ref:`configuration`. .. note:: For brewity and to enable doctests, all further examples in ``structlog``'s documentation use the more simplistic :class:`structlog.processors.KeyValueRenderer()` without timestamps. There you go, structured logging! However, this alone wouldn't warrant its own package. After all, there's even a recipe_ on structured logging for the standard library. So let's go a step further. Building a Context ------------------ Imagine a hypothetical web application that wants to log out all relevant data with just the API from above: .. literalinclude:: code_examples/getting-started/imaginary_web.py :language: python The calls themselves are nice and straight to the point, however you're repeating yourself all over the place. At this point, you'll be tempted to write a closure like :: def log_closure(event): log.msg(event, user_agent=user_agent, peer_ip=peer_ip) inside of the view. Problem solved? Not quite. What if the parameters are introduced step by step? Do you really want to have a logging closure in each of your views? Let's have a look at a better approach: .. literalinclude:: code_examples/getting-started/imaginary_web_better.py :language: python Suddenly your logger becomes your closure! For ``structlog``, a log entry is just a dictionary called *event dict[ionary]*: - You can pre-build a part of the dictionary step by step. These pre-saved values are called the *context*. - As soon as an *event* happens -- which is a dictionary too -- it is merged together with the *context* to an *event dict* and logged out. - If you don't like the concept of pre-building a context: just don't! Convenient key-value-based logging is great to have on its own. - To keep as much order of the keys as possible, an :class:`collections.OrderedDict` is used for the context by default for Pythons that do not have ordered dictionaries by default (notably all versions of CPython before 3.6). - The recommended way of binding values is the one in these examples: creating new loggers with a new context. If you're okay with giving up immutable local state for convenience, you can also use :ref:`thread/greenlet local storage ` for the context. Manipulating Log Entries in Flight ---------------------------------- Now that your log events are dictionaries, it's also much easier to manipulate them than if it were plain strings. To fascilitate that, ``structlog`` has the concept of :doc:`processor chains `. A processor is a callable like a function that receives the event dictionary along with two other arguments and returns a new event dictionary that may or may not differ from the one it got passed. The next processor in the chain receives that returned dictionary instead of the original one. Let's assume you wanted to add a timestamp to every event dict. The processor would look like this: .. doctest:: >>> import datetime >>> def timestamper(_, __, event_dict): ... event_dict["time"] = datetime.datetime.now().isoformat() ... return event_dict Plain Python, plain dictionaries. No you have to tell ``structlog`` about your processor by :doc:`configuring ` it: .. doctest:: >>> structlog.configure(processors=[timestamper, structlog.processors.KeyValueRenderer()]) >>> structlog.get_logger().msg("hi") # doctest: +SKIP event='hi' time='2018-01-21T09:37:36.976816' Rendering --------- Finally you want to have control over the actual format of your log entries. As you may have noticed in the previous section, renderers are just processors too. It's also important to note, that they do not necessarily have to render your event dictionary to a string. It depends on the *logger* that is wrapped by ``structlog`` what kind of input it should get. However, in most cases it's gonna be strings. So assuming you want to follow :doc:`best practices ` and render your event dictionary to JSON that is picked up by a log aggregation system like ELK or Graylog, ``structlog`` comes with batteries included -- you just have to tell it to use its :class:`~structlog.processors.JSONRenderer`: .. doctest:: >>> structlog.configure(processors=[structlog.processors.JSONRenderer()]) >>> structlog.get_logger().msg("hi") {"event": "hi"} .. _standard-library-lite: ``structlog`` and Standard Library's ``logging`` ------------------------------------------------ ``structlog``'s primary application isn't printing though. Instead, it's intended to wrap your *existing* loggers and **add** *structure* and *incremental context building* to them. For that, ``structlog`` is *completely* agnostic of your underlying logger -- you can use it with any logger you like. The most prominent example of such an 'existing logger' is without doubt the logging module in the standard library. To make this common case as simple as possible, ``structlog`` comes with some tools to help you: .. doctest:: >>> import logging >>> logging.basicConfig() >>> from structlog.stdlib import LoggerFactory >>> structlog.configure(logger_factory=LoggerFactory()) # doctest: +SKIP >>> log = structlog.get_logger() >>> log.warning("it works!", difficulty="easy") # doctest: +SKIP WARNING:structlog...:difficulty='easy' event='it works!' In other words, you tell ``structlog`` that you would like to use the standard library logger factory and keep calling :func:`~structlog.get_logger` like before. Since ``structlog`` is mainly used together with standard library's logging, there's :doc:`more ` goodness to make it as fast and convenient as possible. Liked what you saw? ------------------- Now you're all set for the rest of the user's guide. If you want to see more code, make sure to check out the :ref:`examples`! .. [*] In production, you're more likely to use :class:`~structlog.processors.JSONRenderer` that can also be customized using a ``__structlog__`` method so you don't have to change your repr methods to something they weren't originally intended for. .. _`standard out`: https://en.wikipedia.org/wiki/Standard_out#Standard_output_.28stdout.29 .. _`repr()`: https://docs.python.org/2/reference/datamodel.html#object.__repr__ .. _recipe: https://docs.python.org/2/howto/logging-cookbook.html structlog-18.1.0/.readthedocs.yml0000644000076500000240000000011613230600023017141 0ustar hynekstaff00000000000000--- python: version: 3 pip_install: true extra_requirements: - docs structlog-18.1.0/setup.py0000644000076500000240000000661013233027243015604 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. import codecs import os import re from setuptools import find_packages, setup ############################################################################### NAME = "structlog" KEYWORDS = ["logging", "structured", "structure", "log"] CLASSIFIERS = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", ] INSTALL_REQUIRES = ["six"] EXTRAS_REQUIRE = { "dev": ["colorama"], "tests": [ "coverage", "freezegun>=0.2.8", "pretend", "pytest>=3.3.0", "python-rapidjson; python_version>='3.6'", "simplejson", ], "docs": [ "sphinx<1.6.0", "twisted", ] } ############################################################################### HERE = os.path.abspath(os.path.dirname(__file__)) def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: return f.read() try: PACKAGES except NameError: PACKAGES = find_packages(where="src") try: META_PATH except NameError: META_PATH = os.path.join(HERE, "src", NAME, "__init__.py") finally: META_FILE = read(META_PATH) def find_meta(meta): """ Extract __*meta*__ from META_FILE. """ meta_match = re.search( r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M ) if meta_match: return meta_match.group(1) raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) VERSION = find_meta("version") LONG = ( read("README.rst") + "\n\n" + "Release Information\n" + "===================\n\n" + re.search("(\d+.\d.\d \(.*?\)\n.*?)\n\n\n----\n\n\n", read("CHANGELOG.rst"), re.S).group(1) + "\n\n`Full changelog " + "`_.\n\n" + read("AUTHORS.rst") ) if __name__ == "__main__": setup( name=NAME, description=find_meta("description"), license=find_meta("license"), url=find_meta("uri"), version=VERSION, author=find_meta("author"), author_email=find_meta("email"), maintainer=find_meta("author"), maintainer_email=find_meta("email"), long_description=LONG, keywords=KEYWORDS, packages=PACKAGES, package_dir={"": "src"}, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, zip_safe=False, ) structlog-18.1.0/.github/0000755000076500000240000000000013233032005015417 5ustar hynekstaff00000000000000structlog-18.1.0/.github/CONTRIBUTING.rst0000644000076500000240000001320413230622330020063 0ustar hynekstaff00000000000000How To Contribute ================= First off, thank you for considering contributing to ``structlog``! It's people like *you* who make it is such a great tool for everyone. This document is mainly to help you to get started by codifying tribal knowledge and expectations and make it more accessible to everyone. But don't be afraid to open half-finished PRs and ask questions if something is unclear! Workflow -------- - No contribution is too small! Please submit as many fixes for typos and grammar bloopers as you can! - Try to limit each pull request to *one* change only. - *Always* add tests and docs for your code. This is a hard rule; patches with missing tests or documentation can't be merged. - Make sure your changes pass our CI_. You won't get any feedback until it's green unless you ask for it. - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. - Don’t break `backward compatibility`_. Code ---- - Obey `PEP 8`_ and `PEP 257`_. We use the ``"""``\ -on-separate-lines style for docstrings: .. code-block:: python def func(x): """ Do something. :param str x: A very important parameter. :rtype: str """ - If you add or change public APIs, tag the docstring using ``.. versionadded:: 16.0.0 WHAT`` or ``.. versionchanged:: 17.1.0 WHAT``. - Prefer double quotes (``"``) over single quotes (``'``) unless the string contains double quotes itself. Tests ----- - Write your asserts as ``expected == actual`` to line them up nicely: .. code-block:: python x = f() assert 42 == x.some_attribute assert "foo" == x._a_private_attribute - To run the test suite, all you need is a recent tox_. It will ensure the test suite runs with all dependencies against all Python versions just as it will on Travis CI. If you lack some Python versions, you can can make it a non-failure using ``tox --skip-missing-interpreters`` (in that case you may want to look into pyenv_ that makes it very easy to install many different Python versions in parallel). - Write `good test docstrings`_. Documentation ------------- - Use `semantic newlines`_ in reStructuredText_ files (files ending in ``.rst``): .. code-block:: rst This is a sentence. This is another sentence. - If you start a new section, add two blank lines before and one blank line after the header except if two headers follow immediately after each other: .. code-block:: rst Last line of previous section. Header of New Top Section ------------------------- Header of New Section ^^^^^^^^^^^^^^^^^^^^^ First line of new section. - If your change is noteworthy, add an entry to the changelog_. Use `semantic newlines`_, and add a link to your pull request: .. code-block:: rst - Added ``structlog.func()`` that does foo. It's pretty cool. [`#1 `_] - ``structlog.func()`` now doesn't crash the Large Hadron Collider anymore. That was a nasty bug! [`#2 `_] Local Development Environment ----------------------------- You can (and should) run our test suite using tox_ however you’ll probably want a more traditional environment too. We highly recommend to develop using the latest Python 3 release because you're more likely to catch certain bugs earlier. First create a `virtual environment `_. It’s out of scope for this document to list all the ways to manage virtual environments in Python but if you don’t have already a pet way, take some time to look at tools like `pew `_, `virtualfish `_, and `virtualenvwrapper `_. Next get an up to date checkout of the ``structlog`` repository: .. code-block:: bash git checkout git@github.com:hynek/structlog.git Change into the newly created directory and **after activating your virtual environment** install an editable version of ``structlog`` along with its test and docs dependencies: .. code-block:: bash cd structlog pip install -e .[tests,docs] If you run the virtual environment’s Python and try to ``import structlog`` it should work! At this point .. code-block:: bash python -m pytest should work and pass and .. code-block:: bash cd docs make html should build docs in ``docs/_build/html``. **** Again, this list is mainly to help you to get started by codifying tribal knowledge and expectations. If something is unclear, feel free to ask for help! Please note that this project is released with a Contributor `Code of Conduct`_. By participating in this project you agree to abide by its terms. Please report any harm to `Hynek Schlawack`_ in any way you find appropriate. Thank you for considering contributing to ``structlog``! .. _`Hynek Schlawack`: https://hynek.me/about/ .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ .. _`PEP 257`: https://www.python.org/dev/peps/pep-0257/ .. _`good test docstrings`: https://jml.io/pages/test-docstrings.html .. _`Code of Conduct`: https://github.com/hynek/structlog/blob/master/.github/CODE_OF_CONDUCT.rst .. _changelog: https://github.com/hynek/structlog/blob/master/CHANGELOG.rst .. _`backward compatibility`: https://structlog.readthedocs.io/en/latest/backward-compatibility.html .. _tox: https://tox.readthedocs.io/ .. _pyenv: https://github.com/pyenv/pyenv .. _reStructuredText: http://sphinx-doc.org/rest.html .. _semantic newlines: http://rhodesmill.org/brandon/2012/one-sentence-per-line/ .. _CI: https://travis-ci.org/hynek/structlog/ structlog-18.1.0/.github/CODE_OF_CONDUCT.rst0000644000076500000240000000626213230606171020444 0ustar hynekstaff00000000000000Contributor Covenant Code of Conduct ==================================== Our Pledge ---------- In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. Our Standards ------------- Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting Our Responsibilities -------------------- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Scope ----- This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. Enforcement ----------- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hs@ox.cx. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Attribution ----------- This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.4, available at . structlog-18.1.0/tox.ini0000644000076500000240000000252713233027203015404 0ustar hynekstaff00000000000000[tox] envlist = isort,{py27,py34,py35,py36,pypy,pypy3}-{threads,greenlets},{py27,py36}-{colorama,oldtwisted},flake8,docs,readme,manifest,coverage-report [testenv] extras = tests deps = greenlets: greenlet threads,greenlets,colorama: twisted oldtwisted: twisted < 17 colorama: colorama setenv = PYTHONHASHSEED = 0 commands = coverage run --parallel -m pytest {posargs} [testenv:flake8] basepython = python3.6 extras = tests deps = flake8 flake8-isort commands = flake8 src tests setup.py conftest.py docs/conf.py [testenv:isort] basepython = python3.6 extras = tests # Needs a full install so isort can determine own/foreign imports. deps = isort commands = isort --recursive setup.py conftest.py src tests [testenv:docs] basepython = python3.6 extras = docs passenv = TERM setenv = PYTHONHASHSEED = 0 commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html [testenv:readme] basepython = python3.6 deps = readme_renderer skip_install = true commands = python setup.py check -r -s [testenv:manifest] basepython = python3.6 skip_install = true deps = check-manifest commands = check-manifest [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine coverage report structlog-18.1.0/AUTHORS.rst0000644000076500000240000000115513100107646015747 0ustar hynekstaff00000000000000Authors ======= ``structlog`` is written and maintained by `Hynek Schlawack `_. It’s inspired by previous work done by `Jean-Paul Calderone `_ and `David Reid `_. The development is kindly supported by `Variomedia AG `_. A full list of contributors can be found on GitHub’s `overview `_. Some of them disapprove of the addition of thread local context data. :) The ``structlog`` logo has been contributed by `Russell Keith-Magee `_. structlog-18.1.0/setup.cfg0000644000076500000240000000054713233032005015706 0ustar hynekstaff00000000000000[bdist_wheel] universal = 1 [metadata] license_file = LICENSE [tool:pytest] minversion = 3.0 strict = true addopts = -ra testpaths = tests filterwarnings = once::Warning [isort] atomic = true lines_after_imports = 2 lines_between_types = 1 multi_line_output = 5 not_skip = __init__.py known_first_party = structlog [egg_info] tag_build = tag_date = 0 structlog-18.1.0/README.rst0000644000076500000240000001246613231335613015570 0ustar hynekstaff00000000000000.. image:: http://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: http://www.structlog.org/ ``structlog``: Structured Logging for Python ============================================ .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://structlog.readthedocs.io/en/stable/?badge=stable :alt: Documentation Status .. image:: https://travis-ci.org/hynek/structlog.svg?branch=master :target: https://travis-ci.org/hynek/structlog .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://www.irccloud.com/invite-svg?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 :target: https://www.irccloud.com/invite?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 .. -begin-short- ``structlog`` makes logging in Python less painful and more powerful by adding structure to your log entries. It's up to you whether you want ``structlog`` to take care about the **output** of your log entries or whether you prefer to **forward** them to an existing logging system like the standard library's ``logging`` module. .. -end-short- .. -begin-spiel- Easier Logging -------------- You can stop writing prose and start thinking in terms of an event that happens in the context of key/value pairs: .. code-block:: pycon >>> from structlog import get_logger >>> log = get_logger() >>> log.info("key_value_logging", out_of_the_box=True, effort=0) 2016-04-20 16:20.13 key_value_logging effort=0 out_of_the_box=True Each log entry is a meaningful dictionary instead of an opaque string now! Data Binding ------------ Since log entries are dictionaries, you can start binding and re-binding key/value pairs to your loggers to ensure they are present in every following logging call: .. code-block:: pycon >>> log = log.bind(user="anonymous", some_key=23) >>> log = log.bind(user="hynek", another_key=42) >>> log.info("user.logged_in", happy=True) 2016-04-20 16:20.13 user.logged_in another_key=42 happy=True some_key=23 user='hynek' Powerful Pipelines ------------------ Each log entry goes through a `processor pipeline `_ that is just a chain of functions that receive a dictionary and return a new dictionary that gets fed into the next function. That allows for simple but powerful data manipulation: .. code-block:: python def timestamper(logger, log_method, event_dict): """Add a timestamp to each log entry.""" event_dict["timestamp"] = time.time() return event_dict There are `plenty of processors `_ for most common tasks coming with ``structlog``: - Collectors of `call stack information `_ ("How did this log entry happen?"), - …and `exceptions `_ ("What happened‽"). - Unicode encoders/decoders. - Flexible `timestamping `_. Formatting ---------- ``structlog`` is completely flexible about *how* the resulting log entry is emitted. Since each log entry is a dictionary, it can be formatted to **any** format: - A colorful key/value format for `local development `_, - `JSON `_ for easy parsing, - or some standard format you have parsers for like nginx or Apache httpd. Internally, formatters are processors whose return value (usually a string) is passed into loggers that are responsible for the output of your message. ``structlog`` comes with multiple useful formatters out of-the-box. Output ------ ``structlog`` is also very flexible with the final output of your log entries: - A **built-in** lightweight printer like in the examples above. Easy to use and fast. - Use the **standard library**'s or **Twisted**'s logging modules for compatibility. In this case ``structlog`` works like a wrapper that formats a string and passes them off into existing systems that won't ever know that ``structlog`` even exists. Or the other way round: ``structlog`` comes with a ``logging`` formatter that allows for processing third party log records. - Don't format it to a string at all! ``structlog`` passes you a dictionary and you can do with it whatever you want. Reported uses cases are sending them out via network or saving them in a database. .. -end-spiel- Project Information ------------------- .. -begin-meta- ``structlog`` is dual-licensed under `Apache License, version 2 `_ and `MIT `_, available from `PyPI `_, the source code can be found on `GitHub `_, the documentation at http://www.structlog.org/. ``structlog`` targets Python 2.7, 3.4 and newer, and PyPy. If you need any help, visit us on ``#structlog`` on `Freenode `_! structlog-18.1.0/CHANGELOG.rst0000644000076500000240000003237513233030541016115 0ustar hynekstaff00000000000000Changelog ========= Versions are year-based with a strict backward compatibility policy. The third digit is only for regressions. 18.1.0 (2018-01-27) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - The meaning of the ``structlog[dev]`` installation target will change from "colorful output" to "dependencies to develop ``structlog``" in 19.1.0. The main reason behind this decision is that it's impossible to have a ``structlog`` in your normal dependencies and additionally a ``structlog[dev]`` for developement (``pip`` will report an error). Changes: ^^^^^^^^ - Empty strings are valid events now. `#110 `_ - Do not encapsulate Twisted failures twice with newer versions of Twisted. `#144 `_ - ``structlog.dev.ConsoleRenderer`` now accepts a *force_colors* argument to output colored logs even if the destination is not a tty. Use this option if your logs are stored in files that are intended to be streamed to the console. - ``structlog.dev.ConsoleRenderer`` now accepts a *level_styles* argument for overriding the colors for individual levels, as well as to add new levels. See the docs for ``ConsoleRenderer.get_default_level_styles()`` for usage. `#139 `_ - ``structlog.stdlib.BoundLogger.exception()`` now uses the ``exc_info`` argument if it has been passed instead of setting it unconditionally to ``True``. `#149 `_ - Default configuration now uses plain ``dict``\ s on Python 3.6+ and PyPy since they are ordered by default. - Added ``structlog.is_configured()`` to check whether or not ``structlog`` has been configured. - Added ``structlog.get_config()`` to introspect current configuration. ---- 17.2.0 (2017-05-15) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - ``structlog.stdlib.ProcessorFormatter`` now accepts *keep_exc_info* and *keep_stack_info* arguments to control what to do with this information on log records. Most likely you want them both to be ``False`` therefore it's the default. `#109 `_ - ``structlog.stdlib.add_logger_name()`` now works in ``structlog.stdlib.ProcessorFormatter``'s ``foreign_pre_chain``. `#112 `_ - Clear log record args in ``structlog.stdlib.ProcessorFormatter`` after rendering. This fix is for you if you tried to use it and got ``TypeError: not all arguments converted during string formatting`` exceptions. `#116 `_ `#117 `_ ---- 17.1.0 (2017-04-24) ------------------- The main features of this release are massive improvements in standard library's ``logging`` integration. Have a look at the updated `standard library chapter `_ on how to use them! Special thanks go to `Fabian Büchler `_, `Gilbert Gilb's `_, `Iva Kaneva `_, `insolite `_, and `sky-code `_, that made them possible. Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - The default renderer now is ``structlog.dev.ConsoleRenderer`` if you don't configure ``structlog``. Colors are used if available and human-friendly timestamps are prepended. This is in line with our backward `compatibility policy `_ that explicitly excludes default settings. Changes: ^^^^^^^^ - Added ``structlog.stdlib.render_to_log_kwargs()``. This allows you to use ``logging``-based formatters to take care of rendering your entries. `#98 `_ - Added ``structlog.stdlib.ProcessorFormatter`` which does the opposite: This allows you to run ``structlog`` processors on arbitrary ``logging.LogRecords``. `#79 `_ `#105 `_ - UNIX epoch timestamps from ``structlog.processors.TimeStamper`` are more precise now. - Added *repr_native_str* to ``structlog.processors.KeyValueRenderer`` and ``structlog.dev.ConsoleRenderer``. This allows for human-readable non-ASCII output on Python 2 (``repr()`` on Python 2 behaves like ``ascii()`` on Python 3 in that regard). As per compatibility policy, it's on (original behavior) in ``KeyValueRenderer`` and off (humand-friendly behavior) in ``ConsoleRenderer``. `#94 `_ - Added *colors* argument to ``structlog.dev.ConsoleRenderer`` and made it the default renderer. `#78 `_ - Fixed bug with Python 3 and ``structlog.stdlib.BoundLogger.log()``. Error log level was not reproductible and was logged as exception one time out of two. `#92 `_ - Positional arguments are now removed even if they are empty. `#82 `_ ---- 16.1.0 (2016-05-24) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Python 3.3 and 2.6 aren't supported anymore. They may work by chance but any effort to keep them working has ceased. The last Python 2.6 release was on October 29, 2013 and isn't supported by the CPython core team anymore. Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. Changes: ^^^^^^^^ - Add a ``drop_missing`` argument to ``KeyValueRenderer``. If ``key_order`` is used and a key is missing a value, it's not rendered at all instead of being rendered as ``None``. `#67 `_ - Exceptions without a ``__traceback__`` are now also rendered on Python 3. - Don't cache loggers in lazy proxies returned from ``get_logger()``. This lead to in-place mutation of them if used before configuration which in turn lead to the problem that configuration was applied only partially to them later. `#72 `_ ---- 16.0.0 (2016-01-28) ------------------- Changes: ^^^^^^^^ - ``structlog.processors.ExceptionPrettyPrinter`` and ``structlog.processors.format_exc_info`` now support passing of Exceptions on Python 3. - Clean up the context when exiting ``structlog.threadlocal.tmp_bind`` in case of exceptions. `#64 `_ - Be more more lenient about missing ``__name__``\ s. `#62 `_ - Add ``structlog.dev.ConsoleRenderer`` that renders the event dictionary aligned and with colors. - Use `six `_ for compatibility. - Add ``structlog.processors.UnicodeDecoder`` that will decode all byte string values in an event dictionary to Unicode. - Add ``serializer`` parameter to ``structlog.processors.JSONRenderer`` which allows for using different (possibly faster) JSON encoders than the standard library. ---- 15.3.0 (2015-09-25) ------------------- Changes: ^^^^^^^^ - Tolerate frames without a ``__name__``, better. `#58 `_ - Officially support Python 3.5. - Add ``structlog.ReturnLogger.failure`` and ``structlog.PrintLogger.failure`` as preparation for the new Twisted logging system. ---- 15.2.0 (2015-06-10) ------------------- Changes: ^^^^^^^^ - Allow empty lists of processors. This is a valid use case since `#26 `_ has been merged. Before, supplying an empty list resulted in the defaults being used. - Prevent Twisted's ``log.err`` from quoting strings rendered by ``structlog.twisted.JSONRenderer``. - Better support of ``logging.Logger.exception`` within ``structlog``. `#52 `_ - Add option to specify target key in ``structlog.processors.TimeStamper`` processor. `#51 `_ ---- 15.1.0 (2015-02-24) ------------------- Changes: ^^^^^^^^ - Tolerate frames without a ``__name__``. ---- 15.0.0 (2015-01-23) ------------------- Changes: ^^^^^^^^ - Add ``structlog.stdlib.add_log_level`` and ``structlog.stdlib.add_logger_name`` processors. `#44 `_ - Add ``structlog.stdlib.BoundLogger.log``. `#42 `_ - Pass positional arguments to stdlib wrapped loggers that use string formatting. `#19 `_ - ``structlog`` is now dually licensed under the `Apache License, Version 2 `_ and the `MIT `_ license. Therefore it is now legal to use structlog with `GPLv2 `_-licensed projects. `#28 `_ - Add ``structlog.stdlib.BoundLogger.exception``. `#22 `_ ---- 0.4.2 (2014-07-26) ------------------ Changes: ^^^^^^^^ - Fixed a memory leak in greenlet code that emulates thread locals. It shouldn't matter in practice unless you use multiple wrapped dicts within one program that is rather unlikely. `#8 `_ - ``structlog.PrintLogger`` now is thread-safe. - Test Twisted-related code on Python 3 (with some caveats). - Drop support for Python 3.2. There is no justification to add complexity for a Python version that nobody uses. If you are one of the `0.350% `_ that use Python 3.2, please stick to the 0.4 branch; critical bugs will still be fixed. - Officially support Python 3.4. - Allow final processor to return a dictionary. See the adapting chapter. `#26`_ - ``from structlog import *`` works now (but you still shouldn't use it). ---- 0.4.1 (2013-12-19) ------------------ Changes: ^^^^^^^^ - Don't cache proxied methods in ``structlog.threadlocal._ThreadLocalDictWrapper``. This doesn't affect regular users. - Various doc fixes. ---- 0.4.0 (2013-11-10) ------------------ Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Changes: ^^^^^^^^ - Add ``structlog.processors.StackInfoRenderer`` for adding stack information to log entries without involving exceptions. Also added it to default processor chain. `#6 `_ - Allow optional positional arguments for ``structlog.get_logger`` that are passed to logger factories. The standard library factory uses this for explicit logger naming. `#12 `_ - Add ``structlog.processors.ExceptionPrettyPrinter`` for development and testing when multiline log entries aren't just acceptable but even helpful. - Allow the standard library name guesser to ignore certain frame names. This is useful together with frameworks. - Add meta data (e.g. function names, line numbers) extraction for wrapped stdlib loggers. `#5 `_ ---- 0.3.2 (2013-09-27) ------------------ Changes: ^^^^^^^^ - Fix stdlib's name guessing. ---- 0.3.1 (2013-09-26) ------------------ Changes: ^^^^^^^^ - Add forgotten ``structlog.processors.TimeStamper`` to API documentation. ---- 0.3.0 (2013-09-23) ------------------ Changes: ^^^^^^^^ - Greatly enhanced and polished the documentation and added a new theme based on Write The Docs, requests, and Flask. - Add Python Standard Library-specific BoundLogger that has an explicit API instead of intercepting unknown method calls. See ``structlog.stdlib.BoundLogger``. - ``structlog.ReturnLogger`` now allows arbitrary positional and keyword arguments. - Add Twisted-specific BoundLogger that has an explicit API instead of intercepting unknown method calls. See ``structlog.twisted.BoundLogger``. - Allow logger proxies that are returned by ``structlog.get_logger`` and ``structlog.wrap_logger`` to cache the BoundLogger they assemble according to configuration on first use. See the chapter on performance and the ``cache_logger_on_first_use`` argument of ``structlog.configure`` and ``structlog.wrap_logger``. - Extract a common base class for loggers that does nothing except keeping the context state. This makes writing custom loggers much easier and more straight-forward. See ``structlog.BoundLoggerBase``. ---- 0.2.0 (2013-09-17) ------------------ Changes: ^^^^^^^^ - Promote to stable, thus henceforth a strict backward compatibility policy is put into effect. - Add ``key_order`` option to ``structlog.processors.KeyValueRenderer`` for more predictable log entries with any ``dict`` class. - ``structlog.PrintLogger`` now uses proper I/O routines and is thus viable not only for examples but also for production. - Enhance Twisted support by offering JSONification of non-structlog log entries. - Allow for custom serialization in ``structlog.twisted.JSONRenderer`` without abusing ``__repr__``. ---- 0.1.0 (2013-09-16) ------------------ Initial release. structlog-18.1.0/src/0000755000076500000240000000000013233032005014646 5ustar hynekstaff00000000000000structlog-18.1.0/src/structlog/0000755000076500000240000000000013233032005016674 5ustar hynekstaff00000000000000structlog-18.1.0/src/structlog/_base.py0000644000076500000240000001340213233027243020327 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Logger wrapper and helper class. """ from __future__ import absolute_import, division, print_function from six import string_types from structlog.exceptions import DropEvent class BoundLoggerBase(object): """ Immutable context carrier. Doesn't do any actual logging; examples for useful subclasses are: - the generic :class:`BoundLogger` that can wrap anything, - :class:`structlog.twisted.BoundLogger`, - and :class:`structlog.stdlib.BoundLogger`. See also :doc:`custom-wrappers`. """ _logger = None """ Wrapped logger. .. note:: Despite underscore available **read-only** to custom wrapper classes. See also :doc:`custom-wrappers`. """ def __init__(self, logger, processors, context): self._logger = logger self._processors = processors self._context = context def __repr__(self): return '<{0}(context={1!r}, processors={2!r})>'.format( self.__class__.__name__, self._context, self._processors, ) def __eq__(self, other): try: if self._context == other._context: return True else: return False except AttributeError: return False def __ne__(self, other): return not self.__eq__(other) def bind(self, **new_values): """ Return a new logger with *new_values* added to the existing ones. :rtype: `self.__class__` """ return self.__class__( self._logger, self._processors, self._context.__class__(self._context, **new_values) ) def unbind(self, *keys): """ Return a new logger with *keys* removed from the context. :raises KeyError: If the key is not part of the context. :rtype: `self.__class__` """ bl = self.bind() for key in keys: del bl._context[key] return bl def new(self, **new_values): """ Clear context and binds *initial_values* using :func:`bind`. Only necessary with dict implementations that keep global state like those wrapped by :func:`structlog.threadlocal.wrap_dict` when threads are re-used. :rtype: `self.__class__` """ self._context.clear() return self.bind(**new_values) # Helper methods for sub-classing concrete BoundLoggers. def _process_event(self, method_name, event, event_kw): """ Combines creates an `event_dict` and runs the chain. Call it to combine your *event* and *context* into an event_dict and process using the processor chain. :param str method_name: The name of the logger method. Is passed into the processors. :param event: The event -- usually the first positional argument to a logger. :param event_kw: Additional event keywords. For example if someone calls ``log.msg("foo", bar=42)``, *event* would to be ``"foo"`` and *event_kw* ``{"bar": 42}``. :raises: :class:`structlog.DropEvent` if log entry should be dropped. :raises: :class:`ValueError` if the final processor doesn't return a string, tuple, or a dict. :rtype: `tuple` of `(*args, **kw)` .. note:: Despite underscore available to custom wrapper classes. See also :doc:`custom-wrappers`. .. versionchanged:: 14.0.0 Allow final processor to return a `dict`. """ event_dict = self._context.copy() event_dict.update(**event_kw) if event is not None: event_dict["event"] = event for proc in self._processors: event_dict = proc(self._logger, method_name, event_dict) if isinstance(event_dict, string_types): return (event_dict,), {} elif isinstance(event_dict, tuple): # In this case we assume that the last processor returned a tuple # of ``(args, kwargs)`` and pass it right through. return event_dict elif isinstance(event_dict, dict): return (), event_dict else: raise ValueError( "Last processor didn't return an approriate value. Allowed " "return values are a dict, a tuple of (args, kwargs), or a " "string." ) def _proxy_to_logger(self, method_name, event=None, **event_kw): """ Run processor chain on event & call *method_name* on wrapped logger. DRY convenience method that runs :func:`_process_event`, takes care of handling :exc:`structlog.DropEvent`, and finally calls *method_name* on :attr:`_logger` with the result. :param str method_name: The name of the method that's going to get called. Technically it should be identical to the method the user called because it also get passed into processors. :param event: The event -- usually the first positional argument to a logger. :param event_kw: Additional event keywords. For example if someone calls ``log.msg("foo", bar=42)``, *event* would to be ``"foo"`` and *event_kw* ``{"bar": 42}``. .. note:: Despite underscore available to custom wrapper classes. See also :doc:`custom-wrappers`. """ try: args, kw = self._process_event(method_name, event, event_kw) return getattr(self._logger, method_name)(*args, **kw) except DropEvent: return structlog-18.1.0/src/structlog/threadlocal.py0000644000076500000240000001100013233027243021530 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Primitives to keep context global but thread (and greenlet) local. """ from __future__ import absolute_import, division, print_function import contextlib import uuid from structlog._config import BoundLoggerLazyProxy try: from greenlet import getcurrent except ImportError: from threading import local as ThreadLocal else: from weakref import WeakKeyDictionary class ThreadLocal(object): """ threading.local() replacement for greenlets. """ def __init__(self): self.__dict__["_weakdict"] = WeakKeyDictionary() def __getattr__(self, name): key = getcurrent() try: return self._weakdict[key][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, val): key = getcurrent() self._weakdict.setdefault(key, {})[name] = val def __delattr__(self, name): key = getcurrent() try: del self._weakdict[key][name] except KeyError: raise AttributeError(name) def wrap_dict(dict_class): """ Wrap a dict-like class and return the resulting class. The wrapped class and used to keep global in the current thread. :param type dict_class: Class used for keeping context. :rtype: `type` """ Wrapped = type('WrappedDict-' + str(uuid.uuid4()), (_ThreadLocalDictWrapper,), {}) Wrapped._tl = ThreadLocal() Wrapped._dict_class = dict_class return Wrapped def as_immutable(logger): """ Extract the context from a thread local logger into an immutable logger. :param structlog.BoundLogger logger: A logger with *possibly* thread local state. :rtype: :class:`~structlog.BoundLogger` with an immutable context. """ if isinstance(logger, BoundLoggerLazyProxy): logger = logger.bind() try: ctx = logger._context._tl.dict_.__class__(logger._context._dict) bl = logger.__class__( logger._logger, processors=logger._processors, context={}, ) bl._context = ctx return bl except AttributeError: return logger @contextlib.contextmanager def tmp_bind(logger, **tmp_values): """ Bind *tmp_values* to *logger* & memorize current state. Rewind afterwards. """ saved = as_immutable(logger)._context try: yield logger.bind(**tmp_values) finally: logger._context.clear() logger._context.update(saved) class _ThreadLocalDictWrapper(object): """ Wrap a dict-like class and keep the state *global* but *thread-local*. Attempts to re-initialize only updates the wrapped dictionary. Useful for short-lived threaded applications like requests in web app. Use :func:`wrap` to instantiate and use :func:`structlog._loggers.BoundLogger.new` to clear the context. """ def __init__(self, *args, **kw): """ We cheat. A context dict gets never recreated. """ if args and isinstance(args[0], self.__class__): # our state is global, no need to look at args[0] if it's of our # class self._dict.update(**kw) else: self._dict.update(*args, **kw) @property def _dict(self): """ Return or create and return the current context. """ try: return self.__class__._tl.dict_ except AttributeError: self.__class__._tl.dict_ = self.__class__._dict_class() return self.__class__._tl.dict_ def __repr__(self): return '<{0}({1!r})>'.format(self.__class__.__name__, self._dict) def __eq__(self, other): # Same class == same dictionary return self.__class__ == other.__class__ def __ne__(self, other): return not self.__eq__(other) # Proxy methods necessary for structlog. # Dunder methods don't trigger __getattr__ so we need to proxy by hand. def __iter__(self): return self._dict.__iter__() def __setitem__(self, key, value): self._dict[key] = value def __delitem__(self, key): self._dict.__delitem__(key) def __len__(self): return self._dict.__len__() def __getattr__(self, name): method = getattr(self._dict, name) return method structlog-18.1.0/src/structlog/_generic.py0000644000076500000240000000222713233027243021034 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Generic bound logger that can wrap anything. """ from __future__ import absolute_import, division, print_function from functools import partial from structlog._base import BoundLoggerBase class BoundLogger(BoundLoggerBase): """ A generic BoundLogger that can wrap anything. Every unknown method will be passed to the wrapped logger. If that's too much magic for you, try :class:`structlog.stdlib.BoundLogger` or :class:`structlog.twisted.BoundLogger` which also take advantage of knowing the wrapped class which generally results in better performance. Not intended to be instantiated by yourself. See :func:`~structlog.wrap_logger` and :func:`~structlog.get_logger`. """ def __getattr__(self, method_name): """ If not done so yet, wrap the desired logger method & cache the result. """ wrapped = partial(self._proxy_to_logger, method_name) setattr(self, method_name, wrapped) return wrapped structlog-18.1.0/src/structlog/_loggers.py0000644000076500000240000000607313233027243021065 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Logger wrapper and helper class. """ from __future__ import absolute_import, division, print_function import sys import threading from structlog._utils import until_not_interrupted class PrintLoggerFactory(object): """ Produce :class:`PrintLogger`\ s. To be used with :func:`structlog.configure`\ 's `logger_factory`. :param file file: File to print to. (default: stdout) Positional arguments are silently ignored. .. versionadded:: 0.4.0 """ def __init__(self, file=None): self._file = file def __call__(self, *args): return PrintLogger(self._file) WRITE_LOCKS = {} class PrintLogger(object): """ Print events into a file. :param file file: File to print to. (default: stdout) >>> from structlog import PrintLogger >>> PrintLogger().msg("hello") hello Useful if you follow :doc:`current logging best practices `. Also very useful for testing and examples since logging is finicky in doctests. """ def __init__(self, file=None): self._file = file or sys.stdout self._write = self._file.write self._flush = self._file.flush lock = WRITE_LOCKS.get(self._file) if lock is None: lock = threading.Lock() WRITE_LOCKS[self._file] = lock self._lock = lock def __repr__(self): return ''.format(self._file) def msg(self, message): """ Print *message*. """ with self._lock: until_not_interrupted(self._write, message + '\n') until_not_interrupted(self._flush) log = debug = info = warn = warning = msg failure = err = error = critical = exception = msg class ReturnLoggerFactory(object): """ Produce and cache :class:`ReturnLogger`\ s. To be used with :func:`structlog.configure`\ 's `logger_factory`. Positional arguments are silently ignored. .. versionadded:: 0.4.0 """ def __init__(self): self._logger = ReturnLogger() def __call__(self, *args): return self._logger class ReturnLogger(object): """ Return the arguments that it's called with. >>> from structlog import ReturnLogger >>> ReturnLogger().msg("hello") 'hello' >>> ReturnLogger().msg("hello", when="again") (('hello',), {'when': 'again'}) Useful for testing. .. versionchanged:: 0.3.0 Allow for arbitrary arguments and keyword arguments to be passed in. """ def msg(self, *args, **kw): """ Return tuple of ``args, kw`` or just ``args[0]`` if only one arg passed """ # Slightly convoluted for backwards compatibility. if len(args) == 1 and not kw: return args[0] else: return args, kw log = debug = info = warn = warning = msg failure = err = error = critical = exception = msg structlog-18.1.0/src/structlog/_frames.py0000644000076500000240000000320513233027243020672 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. from __future__ import absolute_import, division, print_function import sys import traceback from six.moves import cStringIO as StringIO def _format_exception(exc_info): """ Prettyprint an `exc_info` tuple. Shamelessly stolen from stdlib's logging module. """ sio = StringIO() traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], None, sio) s = sio.getvalue() sio.close() if s[-1:] == "\n": s = s[:-1] return s def _find_first_app_frame_and_name(additional_ignores=None): """ Remove all intra-structlog calls and return the relevant app frame. :param additional_ignores: Additional names with which the first frame must not start. :type additional_ignores: `list` of `str` or `None` :rtype: tuple of (frame, name) """ ignores = ["structlog"] + (additional_ignores or []) f = sys._getframe() name = f.f_globals.get("__name__") or "?" while any(name.startswith(i) for i in ignores): if f.f_back is None: name = "?" break f = f.f_back name = f.f_globals.get("__name__") or "?" return f, name def _format_stack(frame): """ Pretty-print the stack of `frame` like logging would. """ sio = StringIO() sio.write('Stack (most recent call last):\n') traceback.print_stack(frame, file=sio) sinfo = sio.getvalue() if sinfo[-1] == '\n': sinfo = sinfo[:-1] sio.close() return sinfo structlog-18.1.0/src/structlog/__init__.py0000644000076500000240000000277413233030541021022 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Structured logging for Python. """ from __future__ import absolute_import, division, print_function from structlog import dev, processors, stdlib, threadlocal from structlog._base import BoundLoggerBase from structlog._config import ( configure, configure_once, get_config, get_logger, getLogger, is_configured, reset_defaults, wrap_logger ) from structlog._generic import BoundLogger from structlog._loggers import ( PrintLogger, PrintLoggerFactory, ReturnLogger, ReturnLoggerFactory ) from structlog.exceptions import DropEvent try: from structlog import twisted except ImportError: # pragma: nocover twisted = None __version__ = "18.1.0" __title__ = "structlog" __description__ = "Structured Logging for Python" __uri__ = "http://www.structlog.org/" __author__ = "Hynek Schlawack" __email__ = "hs@ox.cx" __license__ = "MIT or Apache License, Version 2.0" __copyright__ = "Copyright (c) 2013 {0}".format(__author__) __all__ = [ "BoundLogger", "BoundLoggerBase", "DropEvent", "PrintLogger", "PrintLoggerFactory", "ReturnLogger", "ReturnLoggerFactory", "configure", "configure_once", "dev", "getLogger", "get_config", "get_logger", "is_configured", "processors", "reset_defaults", "stdlib", "threadlocal", "twisted", "wrap_logger", ] structlog-18.1.0/src/structlog/twisted.py0000644000076500000240000002205613233027243020746 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Processors and tools specific to the `Twisted `_ networking engine. See also :doc:`structlog's Twisted support `. """ from __future__ import absolute_import, division, print_function import json import sys from six import PY2, string_types from twisted.python import log from twisted.python.failure import Failure from twisted.python.log import ILogObserver, textFromEventDict from zope.interface import implementer from ._base import BoundLoggerBase from ._config import _BUILTIN_DEFAULT_PROCESSORS from ._utils import until_not_interrupted from .processors import JSONRenderer as GenericJSONRenderer class BoundLogger(BoundLoggerBase): """ Twisted-specific version of :class:`structlog.BoundLogger`. Works exactly like the generic one except that it takes advantage of knowing the logging methods in advance. Use it like:: configure( wrapper_class=structlog.twisted.BoundLogger, ) """ def msg(self, event=None, **kw): """ Process event and call ``log.msg()`` with the result. """ return self._proxy_to_logger("msg", event, **kw) def err(self, event=None, **kw): """ Process event and call ``log.err()`` with the result. """ return self._proxy_to_logger("err", event, **kw) class LoggerFactory(object): """ Build a Twisted logger when an *instance* is called. >>> from structlog import configure >>> from structlog.twisted import LoggerFactory >>> configure(logger_factory=LoggerFactory()) """ def __call__(self, *args): """ Positional arguments are silently ignored. :rvalue: A new Twisted logger. .. versionchanged:: 0.4.0 Added support for optional positional arguments. """ return log _FAIL_TYPES = (BaseException, Failure) def _extractStuffAndWhy(eventDict): """ Removes all possible *_why*s and *_stuff*s, analyzes exc_info and returns a tuple of `(_stuff, _why, eventDict)`. **Modifies** *eventDict*! """ _stuff = eventDict.pop('_stuff', None) _why = eventDict.pop('_why', None) event = eventDict.pop('event', None) if ( isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES) ): raise ValueError('Both _stuff and event contain an Exception/Failure.') # `log.err('event', _why='alsoEvent')` is ambiguous. if _why and isinstance(event, string_types): raise ValueError('Both `_why` and `event` supplied.') # Two failures are ambiguous too. if not isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES): _why = _why or 'error' _stuff = event if isinstance(event, string_types): _why = event if not _stuff and sys.exc_info() != (None, None, None): _stuff = Failure() # Either we used the error ourselves or the user supplied one for # formatting. Avoid log.err() to dump another traceback into the log. if isinstance(_stuff, BaseException) and not isinstance(_stuff, Failure): _stuff = Failure(_stuff) if PY2: sys.exc_clear() return _stuff, _why, eventDict class ReprWrapper(object): """ Wrap a string and return it as the __repr__. This is needed for log.err() that calls repr() on _stuff: >>> repr("foo") "'foo'" >>> repr(ReprWrapper("foo")) 'foo' Note the extra quotes in the unwrapped example. """ def __init__(self, string): self.string = string def __eq__(self, other): """ Check for equality, actually just for tests. """ return isinstance(other, self.__class__) \ and self.string == other.string def __repr__(self): return self.string class JSONRenderer(GenericJSONRenderer): """ Behaves like :class:`structlog.processors.JSONRenderer` except that it formats tracebacks and failures itself if called with `err()`. .. note:: This ultimately means that the messages get logged out using `msg()`, and *not* `err()` which renders failures in separate lines. Therefore it will break your tests that contain assertions using `flushLoggedErrors `_. *Not* an adapter like :class:`EventAdapter` but a real formatter. Nor does it require to be adapted using it. Use together with a :class:`JSONLogObserverWrapper`-wrapped Twisted logger like :func:`plainJSONStdOutLogger` for pure-JSON logs. """ def __call__(self, logger, name, eventDict): _stuff, _why, eventDict = _extractStuffAndWhy(eventDict) if name == 'err': eventDict['event'] = _why if isinstance(_stuff, Failure): eventDict['exception'] = _stuff.getTraceback(detail='verbose') _stuff.cleanFailure() else: eventDict['event'] = _why return ((ReprWrapper( GenericJSONRenderer.__call__(self, logger, name, eventDict) ),), {'_structlog': True}) @implementer(ILogObserver) class PlainFileLogObserver(object): """ Write only the the plain message without timestamps or anything else. Great to just print JSON to stdout where you catch it with something like runit. :param file file: File to print to. .. versionadded:: 0.2.0 """ def __init__(self, file): self._write = file.write self._flush = file.flush def __call__(self, eventDict): until_not_interrupted(self._write, textFromEventDict(eventDict) + '\n') until_not_interrupted(self._flush) @implementer(ILogObserver) class JSONLogObserverWrapper(object): """ Wrap a log *observer* and render non-:class:`JSONRenderer` entries to JSON. :param ILogObserver observer: Twisted log observer to wrap. For example :class:`PlainFileObserver` or Twisted's stock `FileLogObserver `_ .. versionadded:: 0.2.0 """ def __init__(self, observer): self._observer = observer def __call__(self, eventDict): if '_structlog' not in eventDict: eventDict['message'] = (json.dumps({ 'event': textFromEventDict(eventDict), 'system': eventDict.get('system'), }),) eventDict['_structlog'] = True return self._observer(eventDict) def plainJSONStdOutLogger(): """ Return a logger that writes only the message to stdout. Transforms non-:class:`~structlog.twisted.JSONRenderer` messages to JSON. Ideal for JSONifying log entries from Twisted plugins and libraries that are outside of your control:: $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web {"event": "Log opened.", "system": "-"} {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"} {"event": "reactor class: twisted...EPollReactor.", "system": "-"} {"event": "Site starting on 8080", "system": "-"} {"event": "Starting factory ", ...} ... Composes :class:`PlainFileLogObserver` and :class:`JSONLogObserverWrapper` to a usable logger. .. versionadded:: 0.2.0 """ return JSONLogObserverWrapper(PlainFileLogObserver(sys.stdout)) class EventAdapter(object): """ Adapt an ``event_dict`` to Twisted logging system. Particularly, make a wrapped `twisted.python.log.err `_ behave as expected. :param callable dictRenderer: Renderer that is used for the actual log message. Please note that structlog comes with a dedicated :class:`JSONRenderer`. **Must** be the last processor in the chain and requires a `dictRenderer` for the actual formatting as an constructor argument in order to be able to fully support the original behaviors of ``log.msg()`` and ``log.err()``. """ def __init__(self, dictRenderer=None): """ :param dictRenderer: A processor used to format the log message. """ self._dictRenderer = dictRenderer or _BUILTIN_DEFAULT_PROCESSORS[-1] def __call__(self, logger, name, eventDict): if name == 'err': # This aspires to handle the following cases correctly: # - log.err(failure, _why='event', **kw) # - log.err('event', **kw) # - log.err(_stuff=failure, _why='event', **kw) _stuff, _why, eventDict = _extractStuffAndWhy(eventDict) eventDict['event'] = _why return ((), { '_stuff': _stuff, '_why': self._dictRenderer(logger, name, eventDict), }) else: return self._dictRenderer(logger, name, eventDict) structlog-18.1.0/src/structlog/exceptions.py0000644000076500000240000000064613233027243021445 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Exceptions factored out to avoid import loops. """ class DropEvent(BaseException): """ If raised by an processor, the event gets silently dropped. Derives from BaseException because it's technically not an error. """ structlog-18.1.0/src/structlog/processors.py0000644000076500000240000002474113233027243021470 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Processors useful regardless of the logging framework. """ from __future__ import absolute_import, division, print_function import datetime import json import operator import sys import time import six from structlog._frames import ( _find_first_app_frame_and_name, _format_exception, _format_stack ) class KeyValueRenderer(object): """ Render `event_dict` as a list of ``Key=repr(Value)`` pairs. :param bool sort_keys: Whether to sort keys when formatting. :param list key_order: List of keys that should be rendered in this exact order. Missing keys will be rendered as ``None``, extra keys depending on *sort_keys* and the dict class. :param bool drop_missing: When ``True``, extra keys in *key_order* will be dropped rather than rendered as ``None``. :param bool repr_native_str: When ``True``, :func:`repr()` is also applied to native strings (i.e. unicode on Python 3 and bytes on Python 2). Setting this to ``False`` is useful if you want to have human-readable non-ASCII output on Python 2. .. versionadded:: 0.2.0 *key_order* .. versionadded:: 16.1.0 *drop_missing* .. versionadded:: 17.1.0 *repr_native_str* """ def __init__(self, sort_keys=False, key_order=None, drop_missing=False, repr_native_str=True): # Use an optimized version for each case. if key_order and sort_keys: def ordered_items(event_dict): items = [] for key in key_order: value = event_dict.pop(key, None) if value is not None or not drop_missing: items.append((key, value)) items += sorted(event_dict.items()) return items elif key_order: def ordered_items(event_dict): items = [] for key in key_order: value = event_dict.pop(key, None) if value is not None or not drop_missing: items.append((key, value)) items += event_dict.items() return items elif sort_keys: def ordered_items(event_dict): return sorted(event_dict.items()) else: ordered_items = operator.methodcaller("items") self._ordered_items = ordered_items if repr_native_str is True: self._repr = repr else: def _repr(inst): if isinstance(inst, str): return inst else: return repr(inst) self._repr = _repr def __call__(self, _, __, event_dict): return " ".join(k + "=" + self._repr(v) for k, v in self._ordered_items(event_dict)) class UnicodeEncoder(object): """ Encode unicode values in `event_dict`. :param str encoding: Encoding to encode to (default: ``"utf-8"``). :param str errors: How to cope with encoding errors (default ``"backslashreplace"``). Useful if you're running Python 2 as otherwise ``u"abc"`` will be rendered as ``'u"abc"'``. Just put it in the processor chain before the renderer. """ def __init__(self, encoding="utf-8", errors="backslashreplace"): self._encoding = encoding self._errors = errors def __call__(self, logger, name, event_dict): for key, value in event_dict.items(): if isinstance(value, six.text_type): event_dict[key] = value.encode(self._encoding, self._errors) return event_dict class UnicodeDecoder(object): """ Decode byte string values in `event_dict`. :param str encoding: Encoding to decode from (default: ``"utf-8"``). :param str errors: How to cope with encoding errors (default: ``"replace"``). Useful if you're running Python 3 as otherwise ``b"abc"`` will be rendered as ``'b"abc"'``. Just put it in the processor chain before the renderer. .. versionadded:: 15.4.0 """ def __init__(self, encoding="utf-8", errors="replace"): self._encoding = encoding self._errors = errors def __call__(self, logger, name, event_dict): for key, value in event_dict.items(): if isinstance(value, bytes): event_dict[key] = value.decode(self._encoding, self._errors) return event_dict class JSONRenderer(object): """ Render the `event_dict` using ``serializer(event_dict, **json_kw)``. :param dict json_kw: Are passed unmodified to *serializer*. :param callable serializer: A :func:`json.dumps`-compatible callable that will be used to format the string. This can be used to use alternative JSON encoders like `simplejson `_ or `RapidJSON `_ (faster but Python 3-only) (default: :func:`json.dumps`). .. versionadded:: 0.2.0 Support for ``__structlog__`` serialization method. .. versionadded:: 15.4.0 ``serializer`` parameter. """ def __init__(self, serializer=json.dumps, **dumps_kw): self._dumps_kw = dumps_kw self._dumps = serializer def __call__(self, logger, name, event_dict): return self._dumps(event_dict, default=_json_fallback_handler, **self._dumps_kw) def _json_fallback_handler(obj): """ Serialize custom datatypes and pass the rest to __structlog__ & repr(). """ # circular imports :( from structlog.threadlocal import _ThreadLocalDictWrapper if isinstance(obj, _ThreadLocalDictWrapper): return obj._dict else: try: return obj.__structlog__() except AttributeError: return repr(obj) def format_exc_info(logger, name, event_dict): """ Replace an `exc_info` field by an `exception` string field: If *event_dict* contains the key ``exc_info``, there are two possible behaviors: - If the value is a tuple, render it into the key ``exception``. - If the value is an Exception *and* you're running Python 3, render it into the key ``exception``. - If the value true but no tuple, obtain exc_info ourselves and render that. If there is no ``exc_info`` key, the *event_dict* is not touched. This behavior is analogue to the one of the stdlib's logging. """ exc_info = event_dict.pop("exc_info", None) if exc_info: event_dict["exception"] = _format_exception( _figure_out_exc_info(exc_info) ) return event_dict class TimeStamper(object): """ Add a timestamp to `event_dict`. .. note:: You should let OS tools take care of timestamping. See also :doc:`logging-best-practices`. :param str fmt: strftime format string, or ``"iso"`` for `ISO 8601 `_, or `None` for a `UNIX timestamp `_. :param bool utc: Whether timestamp should be in UTC or local time. :param str key: Target key in `event_dict` for added timestamps. """ def __new__(cls, fmt=None, utc=True, key="timestamp"): if fmt is None and not utc: raise ValueError("UNIX timestamps are always UTC.") now_method = getattr(datetime.datetime, "utcnow" if utc else "now") if fmt is None: def stamper(self, _, __, event_dict): event_dict[key] = time.time() return event_dict elif fmt.upper() == "ISO": if utc: def stamper(self, _, __, event_dict): event_dict[key] = now_method().isoformat() + "Z" return event_dict else: def stamper(self, _, __, event_dict): event_dict[key] = now_method().isoformat() return event_dict else: def stamper(self, _, __, event_dict): event_dict[key] = now_method().strftime(fmt) return event_dict return type("TimeStamper", (object,), {"__call__": stamper})() def _figure_out_exc_info(v): """ Depending on the Python version will try to do the smartest thing possible to transform *v* into an ``exc_info`` tuple. :rtype: tuple """ if six.PY3 and isinstance(v, BaseException): return (v.__class__, v, getattr(v, "__traceback__")) elif isinstance(v, tuple): return v elif v: return sys.exc_info() return v class ExceptionPrettyPrinter(object): """ Pretty print exceptions and remove them from the `event_dict`. :param file file: Target file for output (default: ``sys.stdout``). This processor is mostly for development and testing so you can read exceptions properly formatted. It behaves like :func:`format_exc_info` except it removes the exception data from the event dictionary after printing it. It's tolerant to having `format_exc_info` in front of itself in the processor chain but doesn't require it. In other words, it handles both `exception` as well as `exc_info` keys. .. versionadded:: 0.4.0 .. versionchanged:: 16.0.0 Added support for passing exceptions as ``exc_info`` on Python 3. """ def __init__(self, file=None): if file is not None: self._file = file else: self._file = sys.stdout def __call__(self, logger, name, event_dict): exc = event_dict.pop("exception", None) if exc is None: exc_info = _figure_out_exc_info(event_dict.pop("exc_info", None)) if exc_info: exc = _format_exception(exc_info) if exc: print(exc, file=self._file) return event_dict class StackInfoRenderer(object): """ Add stack information with key `stack` if `stack_info` is true. Useful when you want to attach a stack dump to a log entry without involving an exception. It works analogously to the `stack_info` argument of the Python 3 standard library logging but works on both 2 and 3. .. versionadded:: 0.4.0 """ def __call__(self, logger, name, event_dict): if event_dict.pop("stack_info", None): event_dict["stack"] = _format_stack( _find_first_app_frame_and_name()[0] ) return event_dict structlog-18.1.0/src/structlog/dev.py0000644000076500000240000001655613233027243020051 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Helpers that make development with ``structlog`` more pleasant. """ from __future__ import absolute_import, division, print_function from six import StringIO try: import colorama except ImportError: colorama = None __all__ = [ "ConsoleRenderer", ] _MISSING = ( "{who} requires the {package} package installed. " ) _EVENT_WIDTH = 30 # pad the event name to so many characters def _pad(s, l): """ Pads *s* to length *l*. """ missing = l - len(s) return s + " " * (missing if missing > 0 else 0) if colorama is not None: _has_colorama = True RESET_ALL = colorama.Style.RESET_ALL BRIGHT = colorama.Style.BRIGHT DIM = colorama.Style.DIM RED = colorama.Fore.RED BLUE = colorama.Fore.BLUE CYAN = colorama.Fore.CYAN MAGENTA = colorama.Fore.MAGENTA YELLOW = colorama.Fore.YELLOW GREEN = colorama.Fore.GREEN RED_BACK = colorama.Back.RED else: _has_colorama = False RESET_ALL = BRIGHT = DIM = RED = BLUE = CYAN = MAGENTA = YELLOW = GREEN = \ RED_BACK = "" class _ColorfulStyles(object): reset = RESET_ALL bright = BRIGHT level_critical = RED level_exception = RED level_error = RED level_warn = YELLOW level_info = GREEN level_debug = GREEN level_notset = RED_BACK timestamp = DIM logger_name = BLUE kv_key = CYAN kv_value = MAGENTA class _PlainStyles(object): reset = "" bright = "" level_critical = "" level_exception = "" level_error = "" level_warn = "" level_info = "" level_debug = "" level_notset = "" timestamp = "" logger_name = "" kv_key = "" kv_value = "" class ConsoleRenderer(object): """ Render `event_dict` nicely aligned, possibly in colors, and ordered. :param int pad_event: Pad the event to this many characters. :param bool colors: Use colors for a nicer output. :param bool force_colors: Force colors even for non-tty destinations. Use this option if your logs are stored in a file that is meant to be streamed to the console. :param bool repr_native_str: When ``True``, :func:`repr()` is also applied to native strings (i.e. unicode on Python 3 and bytes on Python 2). Setting this to ``False`` is useful if you want to have human-readable non-ASCII output on Python 2. The `event` key is *never* :func:`repr()` -ed. :param dict level_styles: When present, use these styles for colors. This must be a dict from level names (strings) to colorama styles. The default can be obtained by calling :meth:`ConsoleRenderer.get_default_level_styles` Requires the colorama_ package if *colors* is ``True``. .. _colorama: https://pypi.org/project/colorama/ .. versionadded:: 16.0 .. versionadded:: 16.1 *colors* .. versionadded:: 17.1 *repr_native_str* .. versionadded:: 18.1 *force_colors* .. versionadded:: 18.1 *level_styles* """ def __init__(self, pad_event=_EVENT_WIDTH, colors=True, force_colors=False, repr_native_str=False, level_styles=None): if colors is True: if colorama is None: raise SystemError( _MISSING.format( who=self.__class__.__name__ + " with `colors=True`", package="colorama" ) ) if force_colors: colorama.deinit() colorama.init(strip=False) else: colorama.init() styles = _ColorfulStyles else: styles = _PlainStyles self._styles = styles self._pad_event = pad_event if level_styles is None: self._level_to_color = self.get_default_level_styles(colors) else: self._level_to_color = level_styles for key in self._level_to_color.keys(): self._level_to_color[key] += styles.bright self._longest_level = len(max( self._level_to_color.keys(), key=lambda e: len(e) )) if repr_native_str is True: self._repr = repr else: def _repr(inst): if isinstance(inst, str): return inst else: return repr(inst) self._repr = _repr def __call__(self, _, __, event_dict): sio = StringIO() ts = event_dict.pop("timestamp", None) if ts is not None: sio.write( # can be a number if timestamp is UNIXy self._styles.timestamp + str(ts) + self._styles.reset + " " ) level = event_dict.pop("level", None) if level is not None: sio.write( "[" + self._level_to_color[level] + _pad(level, self._longest_level) + self._styles.reset + "] " ) event = event_dict.pop("event") if event_dict: event = _pad(event, self._pad_event) + self._styles.reset + " " else: event += self._styles.reset sio.write(self._styles.bright + event) logger_name = event_dict.pop("logger", None) if logger_name is not None: sio.write( "[" + self._styles.logger_name + self._styles.bright + logger_name + self._styles.reset + "] " ) stack = event_dict.pop("stack", None) exc = event_dict.pop("exception", None) sio.write( " ".join( self._styles.kv_key + key + self._styles.reset + "=" + self._styles.kv_value + self._repr(event_dict[key]) + self._styles.reset for key in sorted(event_dict.keys()) ) ) if stack is not None: sio.write("\n" + stack) if exc is not None: sio.write("\n\n" + "=" * 79 + "\n") if exc is not None: sio.write("\n" + exc) return sio.getvalue() @staticmethod def get_default_level_styles(colors=True): """ Get the default styles for log levels This is intended to be used with :class:`ConsoleRenderer`'s ``level_styles`` parameter. For example, if you are adding custom levels in your home-grown :func:`~structlog.stdlib.add_log_level` you could do:: my_styles = ConsoleRenderer.get_default_level_styles() my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"] renderer = ConsoleRenderer(level_styles=my_styles) :param bool colors: Whether to use colorful styles. This must match the `colors` parameter to :class:`ConsoleRenderer`. Default: True. """ if colors: styles = _ColorfulStyles else: styles = _PlainStyles return { "critical": styles.level_critical, "exception": styles.level_exception, "error": styles.level_error, "warn": styles.level_warn, "warning": styles.level_warn, "info": styles.level_info, "debug": styles.level_debug, "notset": styles.level_notset, } structlog-18.1.0/src/structlog/stdlib.py0000644000076500000240000003762513233027243020554 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Processors and helpers specific to the :mod:`logging` module from the `Python standard library `_. See also :doc:`structlog's standard library support `. """ from __future__ import absolute_import, division, print_function import logging from six import PY3 from structlog._base import BoundLoggerBase from structlog._frames import _find_first_app_frame_and_name, _format_stack from structlog.exceptions import DropEvent class _FixedFindCallerLogger(logging.Logger): """ Change the behavior of findCaller to cope with structlog's extra frames. """ def findCaller(self, stack_info=False): """ Finds the first caller frame outside of structlog so that the caller info is populated for wrapping stdlib. This logger gets set as the default one when using LoggerFactory. """ f, name = _find_first_app_frame_and_name(["logging"]) if PY3: if stack_info: sinfo = _format_stack(f) else: sinfo = None return f.f_code.co_filename, f.f_lineno, f.f_code.co_name, sinfo else: return f.f_code.co_filename, f.f_lineno, f.f_code.co_name class BoundLogger(BoundLoggerBase): """ Python Standard Library version of :class:`structlog.BoundLogger`. Works exactly like the generic one except that it takes advantage of knowing the logging methods in advance. Use it like:: structlog.configure( wrapper_class=structlog.stdlib.BoundLogger, ) """ def debug(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.debug` with the result. """ return self._proxy_to_logger("debug", event, *args, **kw) def info(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.info` with the result. """ return self._proxy_to_logger("info", event, *args, **kw) def warning(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.warning` with the result. """ return self._proxy_to_logger("warning", event, *args, **kw) warn = warning def error(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.error` with the result. """ return self._proxy_to_logger("error", event, *args, **kw) def critical(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.critical` with the result. """ return self._proxy_to_logger("critical", event, *args, **kw) def exception(self, event=None, *args, **kw): """ Process event and call :meth:`logging.Logger.error` with the result, after setting ``exc_info`` to `True`. """ kw.setdefault("exc_info", True) return self.error(event, *args, **kw) def log(self, level, event, *args, **kw): """ Process event and call the appropriate logging method depending on `level`. """ return self._proxy_to_logger(_LEVEL_TO_NAME[level], event, *args, **kw) fatal = critical def _proxy_to_logger(self, method_name, event, *event_args, **event_kw): """ Propagate a method call to the wrapped logger. This is the same as the superclass implementation, except that it also preserves positional arguments in the `event_dict` so that the stdblib's support for format strings can be used. """ if event_args: event_kw["positional_args"] = event_args return super(BoundLogger, self)._proxy_to_logger(method_name, event=event, **event_kw) # # Pass-through methods to mimick the stdlib's logger interface. # def setLevel(self, level): """ Calls :meth:`logging.Logger.setLevel` with unmodified arguments. """ self._logger.setLevel(level) def findCaller(self, stack_info=False): """ Calls :meth:`logging.Logger.findCaller` with unmodified arguments. """ return self._logger.findCaller(stack_info=stack_info) def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None): """ Calls :meth:`logging.Logger.makeRecord` with unmodified arguments. """ return self._logger.makeRecord(name, level, fn, lno, msg, args, exc_info, func=func, extra=extra) def handle(self, record): """ Calls :meth:`logging.Logger.handle` with unmodified arguments. """ self._logger.handle(record) def addHandler(self, hdlr): """ Calls :meth:`logging.Logger.addHandler` with unmodified arguments. """ self._logger.addHandler(hdlr) def removeHandler(self, hdlr): """ Calls :meth:`logging.Logger.removeHandler` with unmodified arguments. """ self._logger.removeHandler(hdlr) def hasHandlers(self): """ Calls :meth:`logging.Logger.hasHandlers` with unmodified arguments. Exists only in Python 3. """ return self._logger.hasHandlers() def callHandlers(self, record): """ Calls :meth:`logging.Logger.callHandlers` with unmodified arguments. """ self._logger.callHandlers(record) def getEffectiveLevel(self): """ Calls :meth:`logging.Logger.getEffectiveLevel` with unmodified arguments. """ return self._logger.getEffectiveLevel() def isEnabledFor(self, level): """ Calls :meth:`logging.Logger.isEnabledFor` with unmodified arguments. """ return self._logger.isEnabledFor(level) def getChild(self, suffix): """ Calls :meth:`logging.Logger.getChild` with unmodified arguments. """ return self._logger.getChild(suffix) class LoggerFactory(object): """ Build a standard library logger when an *instance* is called. Sets a custom logger using :func:`logging.setLoggerClass` so variables in log format are expanded properly. >>> from structlog import configure >>> from structlog.stdlib import LoggerFactory >>> configure(logger_factory=LoggerFactory()) :param ignore_frame_names: When guessing the name of a logger, skip frames whose names *start* with one of these. For example, in pyramid applications you'll want to set it to ``["venusian", "pyramid.config"]``. :type ignore_frame_names: ``list`` of ``str`` """ def __init__(self, ignore_frame_names=None): self._ignore = ignore_frame_names logging.setLoggerClass(_FixedFindCallerLogger) def __call__(self, *args): """ Deduce the caller's module name and create a stdlib logger. If an optional argument is passed, it will be used as the logger name instead of guesswork. This optional argument would be passed from the :func:`structlog.get_logger` call. For example ``structlog.get_logger("foo")`` would cause this method to be called with ``"foo"`` as its first positional argument. :rtype: logging.Logger .. versionchanged:: 0.4.0 Added support for optional positional arguments. Using the first one for naming the constructed logger. """ if args: return logging.getLogger(args[0]) # We skip all frames that originate from within structlog or one of the # configured names. _, name = _find_first_app_frame_and_name(self._ignore) return logging.getLogger(name) class PositionalArgumentsFormatter(object): """ Apply stdlib-like string formatting to the `event` key. If the `positional_args` key in the event dict is set, it must contain a tuple that is used for formatting (using the `%s` string formatting operator) of the value from the `event` key. This works in the same way as the stdlib handles arguments to the various log methods: if the tuple contains only a single `dict` argument it is used for keyword placeholders in the `event` string, otherwise it will be used for positional placeholders. `positional_args` is populated by :class:`structlog.stdlib.BoundLogger` or can be set manually. The `remove_positional_args` flag can be set to `False` to keep the `positional_args` key in the event dict; by default it will be removed from the event dict after formatting a message. """ def __init__(self, remove_positional_args=True): self.remove_positional_args = remove_positional_args def __call__(self, _, __, event_dict): args = event_dict.get("positional_args") # Mimick the formatting behaviour of the stdlib's logging # module, which accepts both positional arguments and a single # dict argument. The "single dict" check is the same one as the # stdlib's logging module performs in LogRecord.__init__(). if args: if len(args) == 1 and isinstance(args[0], dict) and args[0]: args = args[0] event_dict["event"] = event_dict["event"] % args if self.remove_positional_args and args is not None: del event_dict["positional_args"] return event_dict # Adapted from the stdlib CRITICAL = 50 FATAL = CRITICAL ERROR = 40 WARNING = 30 WARN = WARNING INFO = 20 DEBUG = 10 NOTSET = 0 _NAME_TO_LEVEL = { "critical": CRITICAL, "exception": ERROR, "error": ERROR, "warn": WARNING, "warning": WARNING, "info": INFO, "debug": DEBUG, "notset": NOTSET, } _LEVEL_TO_NAME = dict( (v, k) for k, v in _NAME_TO_LEVEL.items() if k not in ("warn", "exception", "notset", ) ) def filter_by_level(logger, name, event_dict): """ Check whether logging is configured to accept messages from this log level. Should be the first processor if stdlib's filtering by level is used so possibly expensive processors like exception formatters are avoided in the first place. >>> import logging >>> from structlog.stdlib import filter_by_level >>> logging.basicConfig(level=logging.WARN) >>> logger = logging.getLogger() >>> filter_by_level(logger, 'warn', {}) {} >>> filter_by_level(logger, 'debug', {}) Traceback (most recent call last): ... DropEvent """ if logger.isEnabledFor(_NAME_TO_LEVEL[name]): return event_dict else: raise DropEvent def add_log_level(logger, method_name, event_dict): """ Add the log level to the event dict. """ if method_name == 'warn': # The stdlib has an alias method_name = 'warning' event_dict['level'] = method_name return event_dict def add_logger_name(logger, method_name, event_dict): """ Add the logger name to the event dict. """ record = event_dict.get("_record") if record is None: event_dict["logger"] = logger.name else: event_dict["logger"] = record.name return event_dict def render_to_log_kwargs(wrapped_logger, method_name, event_dict): """ Render `event_dict` into keyword arguments for :func:`logging.log`. The `event` field is translated into `msg` and the rest of the `event_dict` is added as `extra`. This allows you to defer formatting to :mod:`logging`. .. versionadded:: 17.1.0 """ return { "msg": event_dict.pop("event"), "extra": event_dict, } class ProcessorFormatter(logging.Formatter): """ Call ``structlog`` processors on :class:`logging.LogRecord`\ s. This :class:`logging.Formatter` allows to configure :mod:`logging` to call *processor* on ``structlog``-borne log entries (origin is determined solely on the fact whether the ``msg`` field on the :class:`logging.LogRecord` is a dict or not). This allows for two interesting use cases: #. You can format non-``structlog`` log entries. #. You can multiplex log records into multiple :class:`logging.Handler`\ s. Please refer to :doc:`standard-library` for examples. :param callable processor: A ``structlog`` processor. :param foreign_pre_chain: If not `None`, it is used as an iterable of processors that is applied to non-``structlog`` log entries before *processor*. If `None`, formatting is left to :mod:`logging`. (default: `None`) :param bool keep_exc_info: ``exc_info`` on :class:`logging.LogRecord`\ s is added to the ``event_dict`` and removed afterwards. Set this to ``True`` to keep it on the :class:`logging.LogRecord`. (default: False) :param bool keep_stack_info: Same as *keep_exc_info* except for Python 3's ``stack_info``. :rtype: str .. versionadded:: 17.1.0 .. versionadded:: 17.2.0 *keep_exc_info* and *keep_stack_info* """ def __init__(self, processor, foreign_pre_chain=None, keep_exc_info=False, keep_stack_info=False, *args, **kwargs): fmt = kwargs.pop("fmt", "%(message)s") super(ProcessorFormatter, self).__init__(*args, fmt=fmt, **kwargs) self.processor = processor self.foreign_pre_chain = foreign_pre_chain self.keep_exc_info = keep_exc_info # The and clause saves us checking for PY3 in the formatter. self.keep_stack_info = keep_stack_info and PY3 def format(self, record): """ Extract ``structlog``'s `event_dict` from ``record.msg`` and format it. """ # Make a shallow copy of the record to let other handlers/formatters # process the original one record = logging.makeLogRecord(record.__dict__) if isinstance(record.msg, dict): # Both attached by wrap_for_formatter logger = record._logger meth_name = record._name # We need to copy because it's possible that the same record gets # processed by multiple logging formatters. LogRecord.getMessage # would transform our dict into a str. ed = record.msg.copy() else: logger = None meth_name = record.levelname.lower() ed = {"event": record.getMessage(), "_record": record} record.args = () # Add stack-related attributes to event_dict and unset them # on the record copy so that the base implementation wouldn't # append stacktraces to the output. if record.exc_info: ed["exc_info"] = record.exc_info if PY3 and record.stack_info: ed["stack_info"] = record.stack_info if not self.keep_exc_info: record.exc_text = None record.exc_info = None if not self.keep_stack_info: record.stack_info = None # Non-structlog allows to run through a chain to prepare it for the # final processor (e.g. adding timestamps and log levels). for proc in self.foreign_pre_chain or (): ed = proc(None, meth_name, ed) del ed["_record"] record.msg = self.processor(logger, meth_name, ed) return super(ProcessorFormatter, self).format(record) @staticmethod def wrap_for_formatter(logger, name, event_dict): """ Wrap *logger*, *name*, and *event_dict*. The result is later unpacked by :class:`ProcessorFormatter` when formatting log entries. Use this static method as the renderer (i.e. final processor) if you want to use :class:`ProcessorFormatter` in your :mod:`logging` configuration. """ return (event_dict,), {"extra": {"_logger": logger, "_name": name}} structlog-18.1.0/src/structlog/_config.py0000644000076500000240000002543313233027243020671 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Global state department. Don't reload this module or everything breaks. """ from __future__ import absolute_import, division, print_function import platform import sys import warnings from collections import OrderedDict from ._generic import BoundLogger from ._loggers import PrintLoggerFactory from .dev import ConsoleRenderer, _has_colorama from .processors import StackInfoRenderer, TimeStamper, format_exc_info _BUILTIN_DEFAULT_PROCESSORS = [ StackInfoRenderer(), format_exc_info, TimeStamper(fmt="%Y-%m-%d %H:%M.%S", utc=False), ConsoleRenderer(colors=_has_colorama), ] if ( sys.version_info[:2] >= (3, 6) or platform.python_implementation() == "PyPy" ): # Python 3.6+ and PyPy have ordered dicts. _BUILTIN_DEFAULT_CONTEXT_CLASS = dict else: _BUILTIN_DEFAULT_CONTEXT_CLASS = OrderedDict _BUILTIN_DEFAULT_WRAPPER_CLASS = BoundLogger _BUILTIN_DEFAULT_LOGGER_FACTORY = PrintLoggerFactory() _BUILTIN_CACHE_LOGGER_ON_FIRST_USE = False class _Configuration(object): """ Global defaults. """ is_configured = False default_processors = _BUILTIN_DEFAULT_PROCESSORS[:] default_context_class = _BUILTIN_DEFAULT_CONTEXT_CLASS default_wrapper_class = _BUILTIN_DEFAULT_WRAPPER_CLASS logger_factory = _BUILTIN_DEFAULT_LOGGER_FACTORY cache_logger_on_first_use = _BUILTIN_CACHE_LOGGER_ON_FIRST_USE _CONFIG = _Configuration() """ Global defaults used when arguments to :func:`wrap_logger` are omitted. """ def is_configured(): """ Return whether ``structlog`` has been configured. If ``False``, ``structlog`` is running with builtin defaults. :rtype: bool .. versionadded: 18.1 """ return _CONFIG.is_configured def get_config(): """ Get a dictionary with the current configuration. .. note:: Changes to the returned dictionary do *not* affect ``structlog``. :rtype: dict .. versionadded: 18.1 """ return { "processors": _CONFIG.default_processors, "context_class": _CONFIG.default_context_class, "wrapper_class": _CONFIG.default_wrapper_class, "logger_factory": _CONFIG.logger_factory, "cache_logger_on_first_use": _CONFIG.cache_logger_on_first_use, } def get_logger(*args, **initial_values): """ Convenience function that returns a logger according to configuration. >>> from structlog import get_logger >>> log = get_logger(y=23) >>> log.msg("hello", x=42) y=23 x=42 event='hello' :param args: *Optional* positional arguments that are passed unmodified to the logger factory. Therefore it depends on the factory what they mean. :param initial_values: Values that are used to pre-populate your contexts. :rtype: A proxy that creates a correctly configured bound logger when necessary. See :ref:`configuration` for details. If you prefer CamelCase, there's an alias for your reading pleasure: :func:`structlog.getLogger`. .. versionadded:: 0.4.0 `args` """ return wrap_logger(None, logger_factory_args=args, **initial_values) getLogger = get_logger """ CamelCase alias for :func:`structlog.get_logger`. This function is supposed to be in every source file -- we don't want it to stick out like a sore thumb in frameworks like Twisted or Zope. """ def wrap_logger(logger, processors=None, wrapper_class=None, context_class=None, cache_logger_on_first_use=None, logger_factory_args=None, **initial_values): """ Create a new bound logger for an arbitrary *logger*. Default values for *processors*, *wrapper_class*, and *context_class* can be set using :func:`configure`. If you set an attribute here, :func:`configure` calls have *no* effect for the *respective* attribute. In other words: selective overwriting of the defaults while keeping some *is* possible. :param initial_values: Values that are used to pre-populate your contexts. :param tuple logger_factory_args: Values that are passed unmodified as ``*logger_factory_args`` to the logger factory if not `None`. :rtype: A proxy that creates a correctly configured bound logger when necessary. See :func:`configure` for the meaning of the rest of the arguments. .. versionadded:: 0.4.0 `logger_factory_args` """ return BoundLoggerLazyProxy( logger, wrapper_class=wrapper_class, processors=processors, context_class=context_class, cache_logger_on_first_use=cache_logger_on_first_use, initial_values=initial_values, logger_factory_args=logger_factory_args, ) def configure(processors=None, wrapper_class=None, context_class=None, logger_factory=None, cache_logger_on_first_use=None): """ Configures the **global** defaults. They are used if :func:`wrap_logger` has been called without arguments. Can be called several times, keeping an argument at `None` leaves is unchanged from the current setting. After calling for the first time, :func:`is_configured` starts returning ``True``. Use :func:`reset_defaults` to undo your changes. :param list processors: List of processors. :param type wrapper_class: Class to use for wrapping loggers instead of :class:`structlog.BoundLogger`. See :doc:`standard-library`, :doc:`twisted`, and :doc:`custom-wrappers`. :param type context_class: Class to be used for internal context keeping. :param callable logger_factory: Factory to be called to create a new logger that shall be wrapped. :param bool cache_logger_on_first_use: `wrap_logger` doesn't return an actual wrapped logger but a proxy that assembles one when it's first used. If this option is set to `True`, this assembled logger is cached. See :doc:`performance`. .. versionadded:: 0.3.0 `cache_logger_on_first_use` """ _CONFIG.is_configured = True if processors is not None: _CONFIG.default_processors = processors if wrapper_class is not None: _CONFIG.default_wrapper_class = wrapper_class if context_class is not None: _CONFIG.default_context_class = context_class if logger_factory is not None: _CONFIG.logger_factory = logger_factory if cache_logger_on_first_use is not None: _CONFIG.cache_logger_on_first_use = cache_logger_on_first_use def configure_once(*args, **kw): """ Configures iff structlog isn't configured yet. It does *not* matter whether is was configured using :func:`configure` or :func:`configure_once` before. Raises a :class:`RuntimeWarning` if repeated configuration is attempted. """ if not _CONFIG.is_configured: configure(*args, **kw) else: warnings.warn('Repeated configuration attempted.', RuntimeWarning) def reset_defaults(): """ Resets global default values to builtin defaults. :func:`is_configured` starts returning ``False`` afterwards. """ _CONFIG.is_configured = False _CONFIG.default_processors = _BUILTIN_DEFAULT_PROCESSORS[:] _CONFIG.default_wrapper_class = _BUILTIN_DEFAULT_WRAPPER_CLASS _CONFIG.default_context_class = _BUILTIN_DEFAULT_CONTEXT_CLASS _CONFIG.logger_factory = _BUILTIN_DEFAULT_LOGGER_FACTORY _CONFIG.cache_logger_on_first_use = _BUILTIN_CACHE_LOGGER_ON_FIRST_USE class BoundLoggerLazyProxy(object): """ Instantiates a BoundLogger on first usage. Takes both configuration and instantiation parameters into account. The only points where a BoundLogger changes state are bind(), unbind(), and new() and that return the actual BoundLogger. If and only if configuration says so, that actual BoundLogger is cached on first usage. .. versionchanged:: 0.4.0 Added support for `logger_factory_args`. """ def __init__(self, logger, wrapper_class=None, processors=None, context_class=None, cache_logger_on_first_use=None, initial_values=None, logger_factory_args=None): self._logger = logger self._wrapper_class = wrapper_class self._processors = processors self._context_class = context_class self._cache_logger_on_first_use = cache_logger_on_first_use self._initial_values = initial_values or {} self._logger_factory_args = logger_factory_args or () def __repr__(self): return ( ''.format(self) ) def bind(self, **new_values): """ Assemble a new BoundLogger from arguments and configuration. """ if self._context_class: ctx = self._context_class(self._initial_values) else: ctx = _CONFIG.default_context_class(self._initial_values) cls = self._wrapper_class or _CONFIG.default_wrapper_class _logger = self._logger if not _logger: _logger = _CONFIG.logger_factory(*self._logger_factory_args) if self._processors is None: procs = _CONFIG.default_processors else: procs = self._processors logger = cls( _logger, processors=procs, context=ctx, ) def finalized_bind(**new_values): """ Use cached assembled logger to bind potentially new values. """ if new_values: return logger.bind(**new_values) else: return logger if ( self._cache_logger_on_first_use is True or (self._cache_logger_on_first_use is None and _CONFIG.cache_logger_on_first_use is True) ): self.bind = finalized_bind return finalized_bind(**new_values) def unbind(self, *keys): """ Same as bind, except unbind *keys* first. In our case that could be only initial values. """ return self.bind().unbind(*keys) def new(self, **new_values): """ Clear context, then bind. """ if self._context_class: self._context_class().clear() else: _CONFIG.default_context_class().clear() bl = self.bind(**new_values) return bl def __getattr__(self, name): """ If a logging method if called on a lazy proxy, we have to create an ephemeral BoundLogger first. """ bl = self.bind() return getattr(bl, name) structlog-18.1.0/src/structlog/_utils.py0000644000076500000240000000137113233027243020557 0ustar hynekstaff00000000000000# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Generic utilities. """ from __future__ import absolute_import, division, print_function import errno def until_not_interrupted(f, *args, **kw): """ Retry until *f* succeeds or an exception that isn't caused by EINTR occurs. :param callable f: A callable like a function. :param *args: Positional arguments for *f*. :param **kw: Keyword arguments for *f*. """ while True: try: return f(*args, **kw) except (IOError, OSError) as e: if e.args[0] == errno.EINTR: continue raise structlog-18.1.0/src/structlog.egg-info/0000755000076500000240000000000013233032005020366 5ustar hynekstaff00000000000000structlog-18.1.0/src/structlog.egg-info/PKG-INFO0000644000076500000240000002445413233032005021474 0ustar hynekstaff00000000000000Metadata-Version: 1.1 Name: structlog Version: 18.1.0 Summary: Structured Logging for Python Home-page: http://www.structlog.org/ Author: Hynek Schlawack Author-email: hs@ox.cx License: MIT or Apache License, Version 2.0 Description-Content-Type: UNKNOWN Description: .. image:: http://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: http://www.structlog.org/ ``structlog``: Structured Logging for Python ============================================ .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://structlog.readthedocs.io/en/stable/?badge=stable :alt: Documentation Status .. image:: https://travis-ci.org/hynek/structlog.svg?branch=master :target: https://travis-ci.org/hynek/structlog .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://www.irccloud.com/invite-svg?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 :target: https://www.irccloud.com/invite?channel=%23structlog&hostname=irc.freenode.net&port=6697&ssl=1 .. -begin-short- ``structlog`` makes logging in Python less painful and more powerful by adding structure to your log entries. It's up to you whether you want ``structlog`` to take care about the **output** of your log entries or whether you prefer to **forward** them to an existing logging system like the standard library's ``logging`` module. .. -end-short- .. -begin-spiel- Easier Logging -------------- You can stop writing prose and start thinking in terms of an event that happens in the context of key/value pairs: .. code-block:: pycon >>> from structlog import get_logger >>> log = get_logger() >>> log.info("key_value_logging", out_of_the_box=True, effort=0) 2016-04-20 16:20.13 key_value_logging effort=0 out_of_the_box=True Each log entry is a meaningful dictionary instead of an opaque string now! Data Binding ------------ Since log entries are dictionaries, you can start binding and re-binding key/value pairs to your loggers to ensure they are present in every following logging call: .. code-block:: pycon >>> log = log.bind(user="anonymous", some_key=23) >>> log = log.bind(user="hynek", another_key=42) >>> log.info("user.logged_in", happy=True) 2016-04-20 16:20.13 user.logged_in another_key=42 happy=True some_key=23 user='hynek' Powerful Pipelines ------------------ Each log entry goes through a `processor pipeline `_ that is just a chain of functions that receive a dictionary and return a new dictionary that gets fed into the next function. That allows for simple but powerful data manipulation: .. code-block:: python def timestamper(logger, log_method, event_dict): """Add a timestamp to each log entry.""" event_dict["timestamp"] = time.time() return event_dict There are `plenty of processors `_ for most common tasks coming with ``structlog``: - Collectors of `call stack information `_ ("How did this log entry happen?"), - …and `exceptions `_ ("What happened‽"). - Unicode encoders/decoders. - Flexible `timestamping `_. Formatting ---------- ``structlog`` is completely flexible about *how* the resulting log entry is emitted. Since each log entry is a dictionary, it can be formatted to **any** format: - A colorful key/value format for `local development `_, - `JSON `_ for easy parsing, - or some standard format you have parsers for like nginx or Apache httpd. Internally, formatters are processors whose return value (usually a string) is passed into loggers that are responsible for the output of your message. ``structlog`` comes with multiple useful formatters out of-the-box. Output ------ ``structlog`` is also very flexible with the final output of your log entries: - A **built-in** lightweight printer like in the examples above. Easy to use and fast. - Use the **standard library**'s or **Twisted**'s logging modules for compatibility. In this case ``structlog`` works like a wrapper that formats a string and passes them off into existing systems that won't ever know that ``structlog`` even exists. Or the other way round: ``structlog`` comes with a ``logging`` formatter that allows for processing third party log records. - Don't format it to a string at all! ``structlog`` passes you a dictionary and you can do with it whatever you want. Reported uses cases are sending them out via network or saving them in a database. .. -end-spiel- Project Information ------------------- .. -begin-meta- ``structlog`` is dual-licensed under `Apache License, version 2 `_ and `MIT `_, available from `PyPI `_, the source code can be found on `GitHub `_, the documentation at http://www.structlog.org/. ``structlog`` targets Python 2.7, 3.4 and newer, and PyPy. If you need any help, visit us on ``#structlog`` on `Freenode `_! Release Information =================== 18.1.0 (2018-01-27) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - The meaning of the ``structlog[dev]`` installation target will change from "colorful output" to "dependencies to develop ``structlog``" in 19.1.0. The main reason behind this decision is that it's impossible to have a ``structlog`` in your normal dependencies and additionally a ``structlog[dev]`` for developement (``pip`` will report an error). Changes: ^^^^^^^^ - Empty strings are valid events now. `#110 `_ - Do not encapsulate Twisted failures twice with newer versions of Twisted. `#144 `_ - ``structlog.dev.ConsoleRenderer`` now accepts a *force_colors* argument to output colored logs even if the destination is not a tty. Use this option if your logs are stored in files that are intended to be streamed to the console. - ``structlog.dev.ConsoleRenderer`` now accepts a *level_styles* argument for overriding the colors for individual levels, as well as to add new levels. See the docs for ``ConsoleRenderer.get_default_level_styles()`` for usage. `#139 `_ - ``structlog.stdlib.BoundLogger.exception()`` now uses the ``exc_info`` argument if it has been passed instead of setting it unconditionally to ``True``. `#149 `_ - Default configuration now uses plain ``dict``\ s on Python 3.6+ and PyPy since they are ordered by default. - Added ``structlog.is_configured()`` to check whether or not ``structlog`` has been configured. - Added ``structlog.get_config()`` to introspect current configuration. `Full changelog `_. Authors ======= ``structlog`` is written and maintained by `Hynek Schlawack `_. It’s inspired by previous work done by `Jean-Paul Calderone `_ and `David Reid `_. The development is kindly supported by `Variomedia AG `_. A full list of contributors can be found on GitHub’s `overview `_. Some of them disapprove of the addition of thread local context data. :) The ``structlog`` logo has been contributed by `Russell Keith-Magee `_. Keywords: logging,structured,structure,log Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules structlog-18.1.0/src/structlog.egg-info/not-zip-safe0000644000076500000240000000000112610174074022627 0ustar hynekstaff00000000000000 structlog-18.1.0/src/structlog.egg-info/SOURCES.txt0000644000076500000240000000366713233032005022266 0ustar hynekstaff00000000000000.coveragerc .readthedocs.yml AUTHORS.rst CHANGELOG.rst LICENSE LICENSE.apache2 LICENSE.mit MANIFEST.in README.rst conftest.py setup.cfg setup.py tox.ini .github/CODE_OF_CONDUCT.rst .github/CONTRIBUTING.rst docs/Makefile docs/api.rst docs/backward-compatibility.rst docs/changelog.rst docs/conf.py docs/configuration.rst docs/contributing.rst docs/custom-wrappers.rst docs/development.rst docs/examples.rst docs/getting-started.rst docs/index.rst docs/license.rst docs/loggers.rst docs/logging-best-practices.rst docs/make.bat docs/performance.rst docs/processors.rst docs/standard-library.rst docs/thread-local.rst docs/twisted.rst docs/why.rst docs/_static/.keep docs/_static/console_renderer.png docs/_static/structlog_logo.png docs/_static/structlog_logo_small.png docs/code_examples/twisted_echo.py docs/code_examples/flask_/some_module.py docs/code_examples/flask_/webapp.py docs/code_examples/getting-started/imaginary_web.py docs/code_examples/getting-started/imaginary_web_better.py docs/code_examples/processors/conditional_dropper.py docs/code_examples/processors/dropper.py docs/code_examples/processors/timestamper.py src/structlog/__init__.py src/structlog/_base.py src/structlog/_config.py src/structlog/_frames.py src/structlog/_generic.py src/structlog/_loggers.py src/structlog/_utils.py src/structlog/dev.py src/structlog/exceptions.py src/structlog/processors.py src/structlog/stdlib.py src/structlog/threadlocal.py src/structlog/twisted.py src/structlog.egg-info/PKG-INFO src/structlog.egg-info/SOURCES.txt src/structlog.egg-info/dependency_links.txt src/structlog.egg-info/not-zip-safe src/structlog.egg-info/requires.txt src/structlog.egg-info/top_level.txt tests/__init__.py tests/additional_frame.py tests/test_base.py tests/test_config.py tests/test_dev.py tests/test_frames.py tests/test_generic.py tests/test_loggers.py tests/test_processors.py tests/test_stdlib.py tests/test_threadlocal.py tests/test_twisted.py tests/test_utils.py tests/utils.pystructlog-18.1.0/src/structlog.egg-info/requires.txt0000644000076500000240000000024713233032005022771 0ustar hynekstaff00000000000000six [dev] colorama [docs] sphinx<1.6.0 twisted [tests] coverage freezegun>=0.2.8 pretend pytest>=3.3.0 simplejson [tests:python_version >= "3.6"] python-rapidjson structlog-18.1.0/src/structlog.egg-info/top_level.txt0000644000076500000240000000001213233032005023111 0ustar hynekstaff00000000000000structlog structlog-18.1.0/src/structlog.egg-info/dependency_links.txt0000644000076500000240000000000113233032005024434 0ustar hynekstaff00000000000000