././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1580211815.679042 structlog-20.1.0/0000755000076500000240000000000000000000000014062 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1580211815.613466 structlog-20.1.0/.github/0000755000076500000240000000000000000000000015422 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1516440697.0 structlog-20.1.0/.github/CODE_OF_CONDUCT.rst0000644000076500000240000000626200000000000020437 0ustar00hynekstaff00000000000000Contributor 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 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571220617.0 structlog-20.1.0/.github/CONTRIBUTING.rst0000644000076500000240000001572200000000000020072 0ustar00hynekstaff00000000000000How 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! Support ------- In case you'd like to help out but don't want to deal with GitHub, there's a great opportunity: help your fellow developers on `StackOverflow `_! The offical tag is ``structlog`` and helping out in support frees us up to improve ``structlog`` instead! 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``. - We use isort_ to sort our imports, and we follow the Black_ code style with a line length of 79 characters. As long as you run our full tox suite before committing, or install our pre-commit_ hooks (ideally you'll do both -- see below "Local Development Environment"), you won't have to spend any time on formatting your code at all. If you don't, CI will catch it for you -- but that seems like a waste of your time! Tests ----- - Write your asserts as ``expected == actual`` to line them up nicely and leave an empty line before them: .. 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 in our 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 as well. 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 .[dev] 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``. To avoid committing code that violates our style guide, we strongly advise you to install pre-commit_ [#f1]_ hooks: .. code-block:: bash $ pre-commit install You can also run them anytime using: .. code-block:: bash $ pre-commit run --all-files .. [#f1] pre-commit should have been installed into your virtualenv automatically when you ran ``pip install -e '.[dev]'`` above. If pre-commit is missing, it may be that you need to re-run ``pip install -e '.[dev]'``. **** 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://www.structlog.org/en/latest/backward-compatibility.html .. _tox: https://tox.readthedocs.io/ .. _pyenv: https://github.com/pyenv/pyenv .. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html .. _semantic newlines: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ .. _CI: https://dev.azure.com/the-hynek/structlog/_build?definitionId=1 .. _black: https://github.com/psf/black .. _pre-commit: https://pre-commit.com/ .. _isort: https://github.com/timothycrosley/isort ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579950575.0 structlog-20.1.0/.github/PULL_REQUEST_TEMPLATE.md0000644000076500000240000000251700000000000021230 0ustar00hynekstaff00000000000000# Pull Request Check List This is just a friendly reminder about the most common mistakes. Please make sure that you tick all boxes. But please read our [contribution guide](https://www.structlog.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles! If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do. - [ ] Added **tests** for changed code. - [ ] Updated **documentation** for changed code. - [ ] New functions/classes have to be added to `docs/api.rst` by hand. - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). Find the appropriate next version in our [``__init__.py``](https://github.com/hynek/structlog/blob/master/src/structlog/__init__.py) file. - [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). - [ ] Changes (and possible deprecations) are documented in the [changelog](https://github.com/hynek/structlog/blob/master/CHANGELOG.rst). If you have *any* questions to *any* of the points above, just **submit and ask**! This checklist is here to *help* you, not to deter you from contributing! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578562927.0 structlog-20.1.0/.pre-commit-config.yaml0000644000076500000240000000154600000000000020351 0ustar00hynekstaff00000000000000--- repos: - repo: https://github.com/psf/black rev: 19.10b0 hooks: - id: black language_version: python3.7 # override until resolved: https://github.com/psf/black/issues/402 files: \.pyi?$ types: [] - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: - id: flake8 language_version: python3.7 exclude: docs/code_examples - repo: https://github.com/asottile/seed-isort-config rev: v1.9.4 hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: - id: isort additional_dependencies: [toml] language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1516437523.0 structlog-20.1.0/.readthedocs.yml0000644000076500000240000000011600000000000017146 0ustar00hynekstaff00000000000000--- python: version: 3 pip_install: true extra_requirements: - docs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1536133777.0 structlog-20.1.0/AUTHORS.rst0000644000076500000240000000115600000000000015744 0ustar00hynekstaff00000000000000Authors ======= ``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 `_. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211222.0 structlog-20.1.0/CHANGELOG.rst0000644000076500000240000004701500000000000016112 0ustar00hynekstaff00000000000000Changelog ========= Versions are year-based with a strict backward compatibility policy. The third digit is only for regressions. 20.1.0 (2020-01-28) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - This is the last version to support Python 2.7 (including PyPy) and 3.5. All following versions will only support Python 3.6 or later. Changes: ^^^^^^^^ - Added a new module ``structlog.contextvars`` that allows to have a global but context-local ``structlog`` context the same way as with ``structlog.threadlocal`` since 19.2.0. `#201 `_, `#236 `_ - Added a new module ``structlog.testing`` for first class testing support. The first entry is the context manager ``capture_logs()`` that allows to make assertions about structured log calls. `#14 `_, `#234 `_ - Added ``structlog.threadlocal.unbind_threadlocal()``. `#239 `_ - The logger created by ``structlog.get_logger()`` is not detected as an abstract method anymore, when attached to an abstract base class. `#229 `_ - ``colorama`` isn't initialized lazily on Windows anymore because it breaks rendering. `#232 `_, `#242 `_ ---- 19.2.0 (2019-10-16) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Python 3.4 is not supported anymore. It has been unsupported by the Python core team for a while now and its PyPI downloads are negligible. It's very unlikely that ``structlog`` will break under 3.4 anytime soon, but we don't test it anymore. Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - Full Python 3.8 support for ``structlog.stdlib``. - Added more pass-through properties to ``structlog.stdlib.BoundLogger``. To makes it easier to use it as a drop-in replacement for ``logging.Logger``. `#198 `_ - ``structlog.stdlib.ProcessorFormatter`` now takes a logger object as an optional keyword argument. This makes ``ProcessorFormatter`` work properly with ``stuctlog.stdlib.filter_by_level()``. `#219 `_ - ``structlog.dev.ConsoleRenderer`` now uses no colors by default, if ``colorama`` is not available. `#215 `_ - ``structlog.dev.ConsoleRenderer`` now initializes ``colorama`` lazily, to prevent accidental side-effects just by importing ``structlog``. `#210 `_ - Added new processor ``structlog.dev.set_exc_info()`` that will set ``exc_info=True`` if the method's name is `exception` and ``exc_info`` isn't set at all. *This is only necessary when the standard library integration is not used*. It fixes the problem that in the default configuration, ``structlog.get_logger().exception("hi")`` in an ``except`` block would not print the exception without passing ``exc_info=True`` to it explicitly. `#130 `_, `#173 `_, `#200 `_, `#204 `_ - A best effort has been made to make as much of ``structlog`` pickleable as possible to make it friendlier with ``multiprocessing`` and similar libraries. Some classes can only be pickled on Python 3 or using the `dill `_ library though and that is very unlikely to change. So far, the configuration proxy, ``structlog.processor.TimeStamper``, ``structlog.BoundLogger``, ``structlog.PrintLogger`` and ``structlog.dev.ConsoleRenderer`` have been made pickelable. Please report if you need any another class fixed. `#126 `_ - Added a new thread-local API that allows binding values to a thread-local context explicitly without affecting the default behavior of ``bind()``. `#222 `_, `#225 `_ - Added ``pass_foreign_args`` argument to ``structlog.stdlib.ProcessorFormatter``. It allows to pass a foreign log record's ``args`` attribute to the event dictionary under the ``positional_args`` key. `#228 `_ - ``structlog.dev.ConsoleRenderer`` now calls ``str()`` on the event value. `#221 `_ ---- 19.1.0 (2019-02-02) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - As announced in 18.1.0, ``pip install -e .[dev]`` now installs all development dependencies. Sorry for the inconveniences this undoubtedly will cause! Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - ``structlog.ReturnLogger`` and ``structlog.PrintLogger`` now have a ``fatal()`` log method. `#181 `_ - Under certain (rather unclear) circumstances, the frame extraction could throw an ``SystemError: error return without exception set``. A workaround has been added. `#174 `_ - ``structlog`` now tolerates passing through ``dict``\ s to stdlib logging. `#187 `_, `#188 `_, `#189 `_ ---- 18.2.0 (2018-09-05) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - Added ``structlog.stdlib.add_log_level_number()`` processor that adds the level *number* to the event dictionary. Can be used to simplify log filtering. `#151 `_ - ``structlog.processors.JSONRenderer`` now allows for overwriting the *default* argument of its serializer. `#77 `_, `#163 `_ - Added ``try_unbind()`` that works like ``unbind()`` but doesn't raise a ``KeyError`` if one of the keys is missing. `#171 `_ ---- 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1531569151.0 structlog-20.1.0/LICENSE0000644000076500000240000000062700000000000015074 0ustar00hynekstaff00000000000000Licensed under either of - Apache License, Version 2.0 (LICENSE.apache or ) - or MIT license (LICENSE.mit or ) at your option. Any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1536132942.0 structlog-20.1.0/LICENSE.apache20000644000076500000240000002367600000000000016407 0ustar00hynekstaff00000000000000 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450530755.0 structlog-20.1.0/LICENSE.mit0000644000076500000240000000207200000000000015660 0ustar00hynekstaff00000000000000The 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578324884.0 structlog-20.1.0/MANIFEST.in0000644000076500000240000000027700000000000015626 0ustar00hynekstaff00000000000000include LICENSE LICENSE.apache2 LICENSE.mit conftest.py include *.rst include *.ini *.yml *.yaml *.toml graft .github graft tests recursive-exclude tests *.pyc graft docs prune docs/_build ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1580211815.67849 structlog-20.1.0/PKG-INFO0000644000076500000240000002621400000000000015164 0ustar00hynekstaff00000000000000Metadata-Version: 2.1 Name: structlog Version: 20.1.0 Summary: Structured Logging for Python Home-page: https://www.structlog.org/ Author: Hynek Schlawack Author-email: hs@ox.cx Maintainer: Hynek Schlawack Maintainer-email: hs@ox.cx License: MIT or Apache License, Version 2.0 Description: .. image:: https://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: https://www.structlog.org/ ============================================ ``structlog``: Structured Logging for Python ============================================ .. image:: https://img.shields.io/pypi/v/structlog.svg :target: https://pypi.org/project/structlog/ :alt: PyPI .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://www.structlog.org/en/stable/?badge=stable :alt: Documentation Status .. image:: https://dev.azure.com/the-hynek/structlog/_apis/build/status/hynek.structlog?branchName=master :target: https://dev.azure.com/the-hynek/structlog/_build?definitionId=1 :alt: CI Status .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: black .. -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- Once you feel inspired to try it out, check out our friendly `Getting Started tutorial `_ that also contains detailed installation instructions! .. -begin-spiel- If you prefer videos over reading, check out this DjangoCon Europe 2019 talk by `Markus Holtermann `_: "`Logging Rethought 2: The Actions of Frank Taylor Jr. `_". 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- .. -begin-meta- Getting Help ============ Please use the ``structlog`` tag on `StackOverflow `_ to get help. Answering questions of your fellow developers is also great way to help the project! Project Information =================== ``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 https://www.structlog.org/. We collect useful third party extension in `our wiki `_. ``structlog`` targets Python 2.7, 3.5 and newer, and PyPy. Release Information =================== 20.1.0 (2020-01-28) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - This is the last version to support Python 2.7 (including PyPy) and 3.5. All following versions will only support Python 3.6 or later. Changes: ^^^^^^^^ - Added a new module ``structlog.contextvars`` that allows to have a global but context-local ``structlog`` context the same way as with ``structlog.threadlocal`` since 19.2.0. `#201 `_, `#236 `_ - Added a new module ``structlog.testing`` for first class testing support. The first entry is the context manager ``capture_logs()`` that allows to make assertions about structured log calls. `#14 `_, `#234 `_ - Added ``structlog.threadlocal.unbind_threadlocal()``. `#239 `_ - The logger created by ``structlog.get_logger()`` is not detected as an abstract method anymore, when attached to an abstract base class. `#229 `_ - ``colorama`` isn't initialized lazily on Windows anymore because it breaks rendering. `#232 `_, `#242 `_ `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.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst Provides-Extra: tests Provides-Extra: docs Provides-Extra: dev Provides-Extra: azure-pipelines ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571221305.0 structlog-20.1.0/README.rst0000644000076500000240000001422600000000000015556 0ustar00hynekstaff00000000000000.. image:: https://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: https://www.structlog.org/ ============================================ ``structlog``: Structured Logging for Python ============================================ .. image:: https://img.shields.io/pypi/v/structlog.svg :target: https://pypi.org/project/structlog/ :alt: PyPI .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://www.structlog.org/en/stable/?badge=stable :alt: Documentation Status .. image:: https://dev.azure.com/the-hynek/structlog/_apis/build/status/hynek.structlog?branchName=master :target: https://dev.azure.com/the-hynek/structlog/_build?definitionId=1 :alt: CI Status .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: black .. -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- Once you feel inspired to try it out, check out our friendly `Getting Started tutorial `_ that also contains detailed installation instructions! .. -begin-spiel- If you prefer videos over reading, check out this DjangoCon Europe 2019 talk by `Markus Holtermann `_: "`Logging Rethought 2: The Actions of Frank Taylor Jr. `_". 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- .. -begin-meta- Getting Help ============ Please use the ``structlog`` tag on `StackOverflow `_ to get help. Answering questions of your fellow developers is also great way to help the project! Project Information =================== ``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 https://www.structlog.org/. We collect useful third party extension in `our wiki `_. ``structlog`` targets Python 2.7, 3.5 and newer, and PyPy. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580209869.0 structlog-20.1.0/azure-pipelines.yml0000644000076500000240000001022700000000000017723 0ustar00hynekstaff00000000000000 # Don't have two build jobs for each pull request. trigger: - master jobs: - job: 'Test' pool: vmImage: 'ubuntu-latest' strategy: matrix: Lint: tox.env: lint python.version: '3.7' Manifest: tox.env: manifest python.version: '3.7' py27-colorama: tox.env: py27-colorama python.version: '2.7' py27-greenlets: tox.env: py27-greenlets python.version: '2.7' py27-threads: tox.env: py27-threads python.version: '2.7' py35-threads: tox.env: py35-threads python.version: '3.5' py36-threads: tox.env: py36-threads python.version: '3.6' py37-colorama: tox.env: py37-colorama python.version: '3.7' py37-greenlets: tox.env: py37-greenlets python.version: '3.7' py37-threads: tox.env: py37-threads python.version: '3.7' py38-threads: tox.env: py38-threads python.version: '3.8' py38-greenlets: tox.env: py38-greenlets python.version: '3.8' pypy-colorama: tox.env: pypy-colorama python.version: pypy2 pypy-greenlets: tox.env: pypy-greenlets python.version: pypy2 pypy-threads: tox.env: pypy-threads python.version: pypy2 pypy3-threads: tox.env: pypy3-threads python.version: pypy3 Docs: python.version: '3.7' tox.env: docs PyPI-Description: python.version: '3.7' tox.env: pypi-description steps: - task: UsePythonVersion@0 displayName: Get Python for Python tools. inputs: versionSpec: '3.7' addToPath: false name: pyTools - script: $(pyTools.pythonLocation)/bin/pip install --upgrade tox displayName: Install Python-based tools. - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' architecture: 'x64' # condition: not(in(variables['python.version'], '3.8')) displayName: Use cached Python $(python.version) for tests. # - script: | # sudo add-apt-repository ppa:deadsnakes # sudo apt-get update # sudo apt-get install -y --no-install-recommends python$(python.version)-dev python$(python.version)-distutils # condition: in(variables['python.version'], '3.8') # displayName: Install Python $(python.version) from the deadsnakes PPA for tests. - script: $(pyTools.pythonLocation)/bin/tox -e $(tox.env) env: TOX_AP_TEST_EXTRAS: azure-pipelines displayName: run tox -e $(tox.env) - script: | if [ ! -f .coverage.* ]; then echo No coverage data found. exit 0 fi # codecov shells out to "coverage" and avoiding 'sudo pip' allows for # package caching. PATH=$HOME/.local/bin:$PATH case "$(python.version)" in "pypy2") PY=pypy ;; "pypy3") PY=pypy3 ;; *) PY=python$(python.version) ;; esac # Python 3.8 needs an up-to-date pip. if [ "$(python.version)" = "3.8" ]; then curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py $PY get-pip.py --user fi $PY -m pip install --user 'coverage[toml]' codecov coverage combine codecov env: CODECOV_TOKEN: $(codecov.token) displayName: Report Coverage condition: succeeded() # Make sure contributors can use Windows. - job: 'Windows' pool: vmImage: 'windows-latest' strategy: matrix: py38: python.version: '3.8' steps: - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' architecture: 'x64' displayName: Use cached Python $(python.version) for tests. - script: python -m pip install -e .[dev] displayName: Install package in dev mode. - script: python -m pytest displayName: Run tests. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1529577154.0 structlog-20.1.0/codecov.yml0000644000076500000240000000025700000000000016233 0ustar00hynekstaff00000000000000--- comment: false coverage: status: patch: default: target: "100" project: default: target: "100" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578396051.0 structlog-20.1.0/conftest.py0000644000076500000240000000203100000000000016255 0ustar00hynekstaff00000000000000# -*- 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 sys import pytest from six.moves import cStringIO as StringIO from structlog.stdlib import _NAME_TO_LEVEL @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 r"" return {"a": A(), "b": [3, 4], "x": 7, "y": "test", "z": (1, 2)} @pytest.fixture( name="stdlib_log_method", params=[m for m in _NAME_TO_LEVEL if m != "notset"], ) def fixture_stdlib_log_methods(request): return request.param collect_ignore = [] if sys.version_info[:2] < (3, 7): collect_ignore.append("tests/test_contextvars.py") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6218987 structlog-20.1.0/docs/0000755000076500000240000000000000000000000015012 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445195.0 structlog-20.1.0/docs/Makefile0000644000076500000240000001271300000000000016456 0ustar00hynekstaff00000000000000# 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) -n -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." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6529446 structlog-20.1.0/docs/_static/0000755000076500000240000000000000000000000016440 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1376851909.0 structlog-20.1.0/docs/_static/.keep0000644000076500000240000000000000000000000017353 0ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579417995.0 structlog-20.1.0/docs/_static/BoundLogger.svg0000644000076500000240000000526700000000000021402 0ustar00hynekstaff00000000000000BoundLogger{ "user": "Guido", "ip": "8.8.8.8", }Context- censor_ip() - add_timestamp() - format_to_json()Processorslogging.LoggerWrapped Logger ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450552462.0 structlog-20.1.0/docs/_static/console_renderer.png0000644000076500000240000023413600000000000022507 0ustar00hynekstaff00000000000000PNG  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`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1493208772.0 structlog-20.1.0/docs/_static/structlog_logo.png0000644000076500000240000030077200000000000022225 0ustar00hynekstaff00000000000000PNG  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`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445675.0 structlog-20.1.0/docs/api.rst0000644000076500000240000001416600000000000016325 0ustar00hynekstaff00000000000000.. _api: API Reference ============= .. note:: The examples here use a very simplified configuration using the minimalistic `structlog.processors.KeyValueRenderer` for brevity and to enable doctests. The output is going to be different (nicer!) with the default configuration. .. testsetup:: * import structlog structlog.configure( processors=[structlog.processors.KeyValueRenderer()], ) .. testcleanup:: * import structlog structlog.reset_defaults() .. module:: structlog `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, fatal .. autoclass:: PrintLoggerFactory .. autoexception:: DropEvent .. autoclass:: BoundLoggerBase :members: new, bind, unbind, _logger, _process_event, _proxy_to_logger `structlog.dev` Module ---------------------- .. automodule:: structlog.dev .. autoclass:: ConsoleRenderer :members: get_default_level_styles .. autofunction:: set_exc_info `structlog.testing` Module -------------------------- .. automodule:: structlog.testing .. autofunction:: capture_logs .. autoclass:: LogCapture .. autoclass:: ReturnLogger :members: msg, err, debug, info, warning, error, critical, log, failure, fatal .. autoclass:: ReturnLoggerFactory `structlog.threadlocal` Module ------------------------------ .. automodule:: structlog.threadlocal .. autofunction:: merge_threadlocal .. autofunction:: clear_threadlocal .. autofunction:: bind_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 `structlog.contextvars` Module ------------------------------ .. automodule:: structlog.contextvars .. autofunction:: merge_contextvars .. autofunction:: clear_contextvars .. autofunction:: bind_contextvars .. autofunction:: unbind_contextvars .. _procs: `structlog.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. If you choose to pass a *default* parameter as part of *json_kw*, support for ``__structlog__`` is disabled. This can be useful when used together with more elegant serialization methods like :func:`functools.singledispatch`: `Better Python Object Serialization `_. .. 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, key="timestamp") .. 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'} `structlog.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_log_level_number .. autofunction:: add_logger_name .. autoclass:: PositionalArgumentsFormatter .. autoclass:: ProcessorFormatter :members: wrap_for_formatter `structlog.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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450551766.0 structlog-20.1.0/docs/backward-compatibility.rst0000644000076500000240000000143400000000000022173 0ustar00hynekstaff00000000000000Backward 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1449063340.0 structlog-20.1.0/docs/changelog.rst0000644000076500000240000000003600000000000017472 0ustar00hynekstaff00000000000000.. include:: ../CHANGELOG.rst ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1580211815.653611 structlog-20.1.0/docs/code_examples/0000755000076500000240000000000000000000000017622 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6546755 structlog-20.1.0/docs/code_examples/flask_/0000755000076500000240000000000000000000000021061 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/docs/code_examples/flask_/some_module.py0000644000076500000240000000044000000000000023741 0ustar00hynekstaff00000000000000from 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' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250854.0 structlog-20.1.0/docs/code_examples/flask_/webapp.py0000644000076500000240000000172500000000000022716 0ustar00hynekstaff00000000000000import logging import sys import uuid import flask from some_module import some_function import structlog 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() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1580211815.655664 structlog-20.1.0/docs/code_examples/getting-started/0000755000076500000240000000000000000000000022727 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/docs/code_examples/getting-started/imaginary_web.py0000644000076500000240000000077500000000000026127 0ustar00hynekstaff00000000000000from 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" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/docs/code_examples/getting-started/imaginary_web_better.py0000644000076500000240000000076400000000000027472 0ustar00hynekstaff00000000000000from 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" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1580211815.657372 structlog-20.1.0/docs/code_examples/processors/0000755000076500000240000000000000000000000022024 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1516622170.0 structlog-20.1.0/docs/code_examples/processors/conditional_dropper.py0000644000076500000240000000123100000000000026431 0ustar00hynekstaff00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1379329173.0 structlog-20.1.0/docs/code_examples/processors/dropper.py0000644000076500000240000000014400000000000024050 0ustar00hynekstaff00000000000000from structlog import DropEvent def dropper(logger, method_name, event_dict): raise DropEvent ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1462692486.0 structlog-20.1.0/docs/code_examples/processors/timestamper.py0000644000076500000240000000024200000000000024726 0ustar00hynekstaff00000000000000import calendar import time def timestamper(logger, log_method, event_dict): event_dict["timestamp"] = calendar.timegm(time.gmtime()) return event_dict ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/docs/code_examples/twisted_echo.py0000644000076500000240000000175300000000000022663 0ustar00hynekstaff00000000000000import sys import uuid import twisted from twisted.internet import protocol, reactor import structlog 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445997.0 structlog-20.1.0/docs/conf.py0000644000076500000240000002436100000000000016317 0ustar00hynekstaff00000000000000# -*- 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 = "any" nitpick_ignore = [ ("py:class", "callable"), ("py:class", "file object"), ("py:class", "ILogObserver"), ("py:class", "PlainFileObserver"), ] # 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", "show_relbars": True, } 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} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579433121.0 structlog-20.1.0/docs/configuration.rst0000644000076500000240000001657000000000000020424 0ustar00hynekstaff00000000000000Configuration ============= 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's start at the end and introduce the ultimate convenience function that relies purely on configuration: :func:`structlog.get_logger`. 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 that end you'll have to call :func:`structlog.configure` on app initialization. Thus the :ref:`example ` from the previous chapter could 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_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.PrintLogger` is the default ``LoggerFactory`` (see :ref:`logger-factories`). Configuration also applies to :func:`~structlog.wrap_logger` because that's what's used internally: .. 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'} ----- 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 `structlog.get_logger` most likely in module scope, they run at import time before you had a chance to configure ``structlog``. Therefore they return a **lazy proxy** that returns a correct wrapped logger on first ``bind()``/``new()``. Thus, you must never call ``new()`` or ``bind()`` in module or class scope because 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 `performance`. .. _logger-factories: Logger Factories ---------------- To make `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 `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: - `structlog.stdlib.LoggerFactory` - `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 `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 `structlog.get_logger` without configuration gives you a perfectly useful `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** See `Third-Party Extensions `_ in the wiki. **Flask** See `Logging `_. **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 `structlog.configure` gets called more than once, ``structlog`` offers `structlog.configure_once` that raises a warning if ``structlog`` has been configured before (no matter whether using `structlog.configure` or :func:`~structlog.configure_once`) but doesn't change anything. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579434406.0 structlog-20.1.0/docs/contextvars.rst0000644000076500000240000000460100000000000020125 0ustar00hynekstaff00000000000000.. _contextvars: Context Variables ================= .. testsetup:: * import structlog .. testcleanup:: * import structlog structlog.reset_defaults() Historically, ``structlog`` only supported thread-local context binding. With the introduction of :mod:`contextvars` in Python 3.7, there is now a way of having a global context that is local to the current context and even works in concurrent code such as code using :mod:`asyncio`. For that ``structlog`` provides a the `structlog.contextvars` module with a set of functions to bind variables to a context-local context. This context is safe to be used in asynchronous code. The general flow is: - Use `structlog.configure` with `structlog.contextvars.merge_contextvars` as your first processor. - Call `structlog.contextvars.clear_contextvars` at the beginning of your request handler (or whenever you want to reset the context-local context). - Call `structlog.contextvars.bind_contextvars` and `structlog.contextvars.unbind_contextvars` instead of you bound logger's ``bind()`` and ``unbind()`` when you want to bind and unbind key-value pairs to the context-local context. - Use ``structlog`` as normal. Loggers act as the always do, but the `structlog.contextvars.merge_contextvars` processor ensures that any context-local binds get included in all of your log messages. .. doctest:: >>> from structlog.contextvars import ( ... bind_contextvars, ... clear_contextvars, ... merge_contextvars, ... unbind_contextvars, ... ) >>> from structlog import configure >>> configure( ... processors=[ ... merge_contextvars, ... structlog.processors.KeyValueRenderer(), ... ] ... ) >>> log = structlog.get_logger() >>> # At the top of your request handler (or, ideally, some general >>> # middleware), clear the threadlocal context and bind some common >>> # values: >>> clear_contextvars() >>> bind_contextvars(a=1, b=2) >>> # Then use loggers as per normal >>> # (perhaps by using structlog.get_logger() to create them). >>> log.msg("hello") a=1 b=2 event='hello' >>> # Use unbind_contextvars to remove a variable from the context >>> unbind_contextvars("b") >>> log.msg("world") a=1 event='world' >>> # And when we clear the threadlocal state again, it goes away. >>> clear_contextvars() >>> log.msg("hi there") event='hi there' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1516440842.0 structlog-20.1.0/docs/contributing.rst0000644000076500000240000000007400000000000020254 0ustar00hynekstaff00000000000000.. _contributing: .. include:: ../.github/CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579437120.0 structlog-20.1.0/docs/custom-wrappers.rst0000644000076500000240000000467000000000000020726 0ustar00hynekstaff00000000000000Custom Wrappers =============== .. testsetup:: * import structlog structlog.configure( processors=[structlog.processors.KeyValueRenderer()], ) .. testcleanup:: * import structlog structlog.reset_defaults() ``structlog`` comes with a generic bound logger called `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 `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 `structlog.configure`. And to make it easier for you, it comes with the 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 `structlog.BoundLoggerBase._logger`. - The helper method `structlog.BoundLoggerBase._proxy_to_logger` that is a DRY_ convenience function that runs the processor chain, handles possible `DropEvent`\ s and calls a named function on `_logger`. - You can run the chain by hand through using `structlog.BoundLoggerBase._process_event` . These two methods and one attribute are all you need to write own wrapper classes. .. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579435276.0 structlog-20.1.0/docs/development.rst0000644000076500000240000000310700000000000020067 0ustar00hynekstaff00000000000000Development =========== To make development a more pleasurable experience, ``structlog`` comes with the `structlog.dev` module. The highlight is `structlog.dev.ConsoleRenderer` that offers nicely aligned and colorful (requires the `colorama module `_ installed) 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 `ConsoleRenderer`, therefore if you want nice colorful output on the console, you don't have to do anything except installing colorama. 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, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579435232.0 structlog-20.1.0/docs/examples.rst0000644000076500000240000001002100000000000017354 0ustar00hynekstaff00000000000000Examples ======== 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 `structlog.stdlib.LoggerFactory` is a totally magic-free class that just deduces the name of the caller's module and does a `logging.getLogger` with it. It's used by `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 `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 ---------- :doc:`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 `. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579432780.0 structlog-20.1.0/docs/getting-started.rst0000644000076500000240000002012300000000000020647 0ustar00hynekstaff00000000000000Getting 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 `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 `colors `. It should be noted that even in most complex logging setups the example would still look just like that thanks to `configuration`. Using the defaults, as above, is equivalent to:: import structlog structlog.configure( processors=[ structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, structlog.processors.format_exc_info, structlog.processors.TimeStamper(), structlog.dev.ConsoleRenderer() ], wrapper_class=structlog.BoundLogger, context_class=dict, # or OrderedDict if the runtime's dict is unordered (e.g. Python <3.6) logger_factory=structlog.PrintLoggerFactory(), cache_logger_on_first_use=False ) log = structlog.get_logger() .. note:: For brevity and to enable doctests, all further examples in ``structlog``'s documentation use the more simplistic `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 `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 `thread/greenlet local storage ` or :doc:`context variables ` 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 facilitate 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. Now you have to tell ``structlog`` about your processor by `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 `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"} ``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 `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 and can start reading about :doc:`bound loggers ` -- the heart of ``structlog``. If you want to see more code, make sure to check out the `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 .. _recipe: https://docs.python.org/2/howto/logging-cookbook.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579437528.0 structlog-20.1.0/docs/index.rst0000644000076500000240000000311400000000000016652 0ustar00hynekstaff00000000000000============================= Structured Logging for Python ============================= Release v\ |version| (`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 `why`. - If you can't wait to log your first entry, start at `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 testing thread-local contextvars 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 .. include:: ../README.rst :start-after: -begin-meta- .. toctree:: :maxdepth: 1 backward-compatibility contributing license changelog Indices and tables ================== - `genindex` - `modindex` - `search` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579429434.0 structlog-20.1.0/docs/license.rst0000644000076500000240000000143200000000000017166 0ustar00hynekstaff00000000000000License 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`` project. The full license texts can be also found in the source code repository: - `Apache License 2.0 `_ - `MIT `_ .. _authors: .. include:: ../AUTHORS.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579433024.0 structlog-20.1.0/docs/loggers.rst0000644000076500000240000001231000000000000017203 0ustar00hynekstaff00000000000000Loggers ======= Bound Loggers ------------- The center of ``structlog`` is the immutable log wrapper :class:`~structlog.BoundLogger`. .. image:: _static/BoundLogger.svg What it does is: - Store a *context dictionary* with key-value pairs that should be part of every log entry, - store a list of :doc:`processors ` that are called on every log entry, - and store a *logger* that it's wrapping. This *can* be standard library's `logging.Logger` but absolutely doesn't have to. To manipulate the context dictionary, it offers to: - Recreate itself with (optional) *additional* context data: :func:`~structlog.BoundLogger.bind` and :func:`~structlog.BoundLogger.new`. - Recreate itself with *less* context data: :func:`~structlog.BoundLogger.unbind`. In any case, the original bound logger or its context are never mutated. Finally, if you call *any other* method on :class:`~structlog.BoundLogger`, it will: #. Make a copy of the context -- now it becomes the *event dictionary*, #. Add the keyword arguments of the method call to the event dict. #. Add a new key ``event`` with the value of the first positional argument of the method call to the event dict. #. Run the processors on the event dict. Each processor receives the result of its predecessor. #. Finally it takes the result of the final processor and calls the method with the same name that got called on the bound logger on ther wrapped logger\ [1]_. For flexibility, the final processor can return either a string that is passed directly as a positional parameter, or a tuple ``(args, kwargs)`` that are passed as ``wrapped_logger.log_method(*args, **kwargs)``. .. [1] Since this is slightly magicy, ``structlog`` comes with concrete loggers for the `standard-library` and :doc:`twisted` that offer you explicit APIs for the supported logging methods but behave identically like the generic BoundLogger otherwise. Of course, you are free to implement your own bound loggers too. Creation -------- You won't be instantiating it yourself though. In practice you will configure ``structlog`` as explained in the `next chapter ` and then just call `structlog.get_logger`. In some rare cases you may not want to do that. For that times there is the `structlog.wrap_logger` function that can be used to wrap a logger without any global state (i.e. configuration): .. _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 :doc:`filter, mutate, and format ` the log entry before it gets passed to the wrapped logger. Default is ``[``:class:`~structlog.processors.StackInfoRenderer`, :func:`~structlog.processors.format_exc_info`, :class:`~structlog.processors.TimeStamper`, :class:`~structlog.dev.ConsoleRenderer`\ ``]``. **context_class** The class to save your context in. Particularly useful for `thread local context storage `. On Python versions that have ordered dictionaries (Python 3.6+, PyPy) the default is a plain `dict`. For everything else it's `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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571221290.0 structlog-20.1.0/docs/logging-best-practices.rst0000644000076500000240000000560600000000000022107 0ustar00hynekstaff00000000000000====================== Logging Best Practices ====================== Servers ======= 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 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 -- of 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 your timestamp field. Graylog ^^^^^^^ Graylog_ goes one step further. It not only supports everything those above do (and then some); you can also directly log 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`: https://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/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1376850699.0 structlog-20.1.0/docs/make.bat0000644000076500000240000001175600000000000016431 0ustar00hynekstaff00000000000000@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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579429382.0 structlog-20.1.0/docs/performance.rst0000644000076500000240000000422500000000000020050 0ustar00hynekstaff00000000000000Performance =========== ``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/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579437643.0 structlog-20.1.0/docs/processors.rst0000644000076500000240000001030400000000000017744 0ustar00hynekstaff00000000000000Processors ========== 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 `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 a processor built in: `TimeStamper`. Filtering --------- If a processor raises `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 `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 `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. We collect those packages in our `GitHub Wiki `_ and encourage you to add your package too! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579436337.0 structlog-20.1.0/docs/standard-library.rst0000644000076500000240000003246300000000000021016 0ustar00hynekstaff00000000000000Standard Library Logging ======================== Ideally, ``structlog`` should be able to be used as a drop-in replacement for standard library's `logging` by wrapping it. In other words, you should be able to replace your call to `logging.getLogger` by a call to `structlog.get_logger` and things should keep working as before (if ``structlog`` is configured right, see `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 `logging` user, your `help `_ to ensure a better compatibility would be highly appreciated! Just Enough ``logging`` ----------------------- If you want to use ``structlog`` with `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 to *configure* the `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. `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 `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: `structlog.stdlib.BoundLogger`. It behaves exactly like the generic `structlog.BoundLogger` except: - it's slightly faster due to less overhead, - has an explicit API that mirrors the log methods of standard library's `logging.Logger`, - hence causing less cryptic error messages if you get method names wrong. Processors ---------- ``structlog`` comes with a few standard library-specific processors: `render_to_log_kwargs`: Renders the event dictionary into keyword arguments for `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 `logging`. `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 from happening in the first place. `add_logger_name`: Adds the name of the logger to the event dictionary under the key ``logger``. `add_log_level`: Adds the log level to the event dictionary under the key ``level``. `add_log_level_number`: Adds the log level number to the event dictionary under the key ``level_number``. Log level numbers map to the log level names. The Python stdlib uses them for filtering logic. This adds the same numbers so users can leverage similar filtering. Compare:: level in ("warning", "error", "critical") level_number >= 30 The mapping of names to numbers is in ``structlog.stdlib._NAME_TO_LEVEL``. `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 `ProcessorFormatter` which is a `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 `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 `logging` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``structlog`` comes with a `ProcessorFormatter` that can be used as a `logging.Formatter` in any stdlib `Handler ` object. The `ProcessorFormatter` has two parts to its API: #. The `structlog.stdlib.ProcessorFormatter.wrap_for_formatter` method must be used as the last processor in `structlog.configure`, it converts the the processed event dict to something that the ``ProcessorFormatter`` understands. #. The `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 `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 `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 `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 `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`` as `None`, formatting will be left to `logging`. Meaning: you can define a ``format`` for `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 `UnicodeDecoder` through `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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579433184.0 structlog-20.1.0/docs/testing.rst0000644000076500000240000000271600000000000017227 0ustar00hynekstaff00000000000000Testing ------- ``structlog`` comes with tools for testing the logging behavior of your application. If you need functionality similar to `unittest.TestCase.assertLogs`, or you want to capture all logs for some other reason, you can use the `structlog.testing.capture_logs` context manager: .. doctest:: >>> from structlog import get_logger >>> from structlog.testing import capture_logs >>> with capture_logs() as cap_logs: ... get_logger().bind(x="y").info("hello") >>> cap_logs [{'x': 'y', 'event': 'hello', 'log_level': 'info'}] You can build your own helpers using `structlog.testing.LogCapture`. For example a `pytest `_ fixture to capture log output could look like this:: @pytest.fixture(name="log_output") def fixture_log_output(): return LogCapture() @pytest.fixture(autouse=True) def fixture_configure_structlog(log_output): structlog.configure( processors=[log_output] ) def test_my_stuff(log_output): do_something() assert log_output.entries == [...] ---- Additionally ``structlog`` also ships with a logger that just returns whatever it gets passed into it: `structlog.testing.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'}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579437643.0 structlog-20.1.0/docs/thread-local.rst0000644000076500000240000001730200000000000020106 0ustar00hynekstaff00000000000000Thread 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 `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 ships with the `structlog.threadlocal` module and a couple of mechanisms to help here. The ``merge_threadlocal`` Processor ----------------------------------- ``structlog`` provides a simple set of functions that allow explicitly binding certain fields to a global (thread-local) context and merge them later using a processor into the event dict. The general flow of using these functions is: - Use `structlog.configure` with `structlog.threadlocal.merge_threadlocal` as your first processor. - Call `structlog.threadlocal.clear_threadlocal` at the beginning of your request handler (or whenever you want to reset the thread-local context). - Call `structlog.threadlocal.bind_threadlocal` as an alternative to your bound logger's ``bind()`` when you want to bind a particular variable to the thread-local context. - Use ``structlog`` as normal. Loggers act as they always do, but the `structlog.threadlocal.merge_threadlocal` processor ensures that any thread-local binds get included in all of your log messages. .. doctest:: >>> from structlog.threadlocal import ( ... bind_threadlocal, ... clear_threadlocal, ... merge_threadlocal, ... ) >>> from structlog import configure >>> configure( ... processors=[ ... merge_threadlocal, ... structlog.processors.KeyValueRenderer(), ... ] ... ) >>> log = structlog.get_logger() >>> # At the top of your request handler (or, ideally, some general >>> # middleware), clear the threadlocal context and bind some common >>> # values: >>> clear_threadlocal() >>> bind_threadlocal(a=1) >>> # Then use loggers as per normal >>> # (perhaps by using structlog.get_logger() to create them). >>> log.msg("hi") a=1 event='hi' >>> # And when we clear the threadlocal state again, it goes away. >>> clear_threadlocal() >>> log.msg("hi there") event='hi there' Thread-local Contexts --------------------- ``structlog`` also provides thread-local context storage in a form that you may already know from `Flask `_ and that makes the *entire context* global to your thread or greenlet. This makes its behavior more difficult to reason about which is why we generally recomment to use the `merge_threadlocal` route. 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: `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. `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, `structlog.threadlocal` comes with a `context manager `_: `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 `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 `configuration` for more details. The general sentiment against threadn-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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579436973.0 structlog-20.1.0/docs/twisted.rst0000644000076500000240000001052400000000000017231 0ustar00hynekstaff00000000000000Twisted ======= .. warning:: Since ``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: `structlog.twisted.BoundLogger`. It behaves exactly like the generic `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 `structlog.get_logger` called `structlog.getLogger`. Processors ---------- ``structlog`` comes with two Twisted-specific processors: `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: `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: `structlog.twisted.JSONLogObserverWrapper`. What it does is determining whether a log entry has been formatted by `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 `structlog.twisted.PlainFileLogObserver` that writes only the plain message to a file and `structlog.twisted.plainJSONStdOutLogger` that composes it with the aforementioned `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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579432270.0 structlog-20.1.0/docs/why.rst0000644000076500000240000000150300000000000016352 0ustar00hynekstaff00000000000000==== 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/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578325007.0 structlog-20.1.0/pyproject.toml0000644000076500000240000000115000000000000016773 0ustar00hynekstaff00000000000000[build-system] requires = ["setuptools>=40.6.0", "wheel"] build-backend = "setuptools.build_meta" [tool.coverage.run] parallel = true branch = true source = ["structlog"] [tool.coverage.paths] source = ["src", ".tox/*/site-packages"] [tool.coverage.report] show_missing = true [tool.black] line-length = 79 [tool.isort] atomic=true include_trailing_comma=true lines_after_imports=2 lines_between_types=1 multi_line_output=3 not_skip="__init__.py" known_first_party="structlog" known_third_party=["flask", "freezegun", "pretend", "pytest", "setuptools", "six", "some_module", "structlog", "twisted", "zope"] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6792157 structlog-20.1.0/setup.cfg0000644000076500000240000000004600000000000015703 0ustar00hynekstaff00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580209869.0 structlog-20.1.0/setup.py0000644000076500000240000000740400000000000015601 0ustar00hynekstaff00000000000000# 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.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", ] INSTALL_REQUIRES = ["six"] EXTRAS_REQUIRE = { "tests": [ "coverage[toml]", "freezegun>=0.2.8", "pretend", "pytest>=3.3.0", "pytest-asyncio; python_version>='3.7'", "python-rapidjson; python_version>='3.6'", "simplejson", ], "docs": ["sphinx", "twisted"], } EXTRAS_REQUIRE["dev"] = ( EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"] ) EXTRAS_REQUIRE["azure-pipelines"] = EXTRAS_REQUIRE["tests"] + [ "pytest-azurepipelines" ] ############################################################################### 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( r"(\d+.\d.\d \(.*?\)\r?\n.*?)\r?\n\r?\n\r?\n----\r?\n\r?\n\r?\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, long_description_content_type="text/x-rst", keywords=KEYWORDS, packages=PACKAGES, package_dir={"": "src"}, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, zip_safe=False, options={"bdist_wheel": {"universal": "1"}}, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.5853992 structlog-20.1.0/src/0000755000076500000240000000000000000000000014651 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6649997 structlog-20.1.0/src/structlog/0000755000076500000240000000000000000000000016677 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211222.0 structlog-20.1.0/src/structlog/__init__.py0000644000076500000240000000307200000000000021012 0ustar00hynekstaff00000000000000# 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, testing, 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 from structlog.exceptions import DropEvent from structlog.testing import ReturnLogger, ReturnLoggerFactory try: from structlog import twisted except ImportError: # pragma: nocover twisted = None __version__ = "20.1.0" __title__ = "structlog" __description__ = "Structured Logging for Python" __uri__ = "https://www.structlog.org/" __author__ = "Hynek Schlawack" __email__ = "hs@ox.cx" __license__ = "MIT or Apache License, Version 2.0" __copyright__ = "Copyright (c) 2013 " + __author__ __all__ = [ "BoundLogger", "BoundLoggerBase", "DropEvent", "PrintLogger", "PrintLoggerFactory", "ReturnLogger", "ReturnLoggerFactory", "configure", "configure_once", "dev", "getLogger", "get_config", "get_logger", "is_configured", "processors", "reset_defaults", "stdlib", "testing", "threadlocal", "twisted", "wrap_logger", ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579430785.0 structlog-20.1.0/src/structlog/_base.py0000644000076500000240000001400200000000000020317 0ustar00hynekstaff00000000000000# 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 `custom-wrappers`. """ _logger = None """ Wrapped logger. .. note:: Despite underscore available **read-only** to custom wrapper classes. See also `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 try_unbind(self, *keys): """ Like :meth:`unbind`, but best effort: missing keys are ignored. :rtype: ``self.__class__`` .. versionadded:: 18.2.0 """ bl = self.bind() for key in keys: bl._context.pop(key, None) 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 `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: `structlog.DropEvent` if log entry should be dropped. :raises: `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 `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 appropriate 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 `custom-wrappers`. """ try: args, kw = self._process_event(method_name, event, event_kw) return getattr(self._logger, method_name)(*args, **kw) except DropEvent: return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579960287.0 structlog-20.1.0/src/structlog/_config.py0000644000076500000240000002630700000000000020665 0ustar00hynekstaff00000000000000# 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, set_exc_info from .processors import StackInfoRenderer, TimeStamper, format_exc_info """ .. note:: Any changes to these defaults must be reflected in `getting-started`. """ _BUILTIN_DEFAULT_PROCESSORS = [ StackInfoRenderer(), set_exc_info, 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 `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. :returns: A proxy that creates a correctly configured bound logger when necessary. See `configuration` for details. If you prefer CamelCase, there's an alias for your reading pleasure: `structlog.getLogger`. .. versionadded:: 0.4.0 *args* """ return wrap_logger(None, logger_factory_args=args, **initial_values) getLogger = get_logger """ CamelCase alias for `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 `configure`. If you set an attribute here, `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`. :returns: A proxy that creates a correctly configured bound logger when necessary. See `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 `wrap_logger` or `get_logger` are called without arguments. Can be called several times, keeping an argument at `None` leaves it unchanged from the current setting. After calling for the first time, `is_configured` starts returning `True`. Use `reset_defaults` to undo your changes. :param list processors: List of processors. :param type wrapper_class: Class to use for wrapping loggers instead of `structlog.BoundLogger`. See `standard-library`, :doc:`twisted`, and `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 `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 `configure` or `configure_once` before. Raises a `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. `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. """ if name == "__isabstractmethod__": raise AttributeError bl = self.bind() return getattr(bl, name) def __getstate__(self): """ Our __getattr__ magic makes this necessary. """ return self.__dict__ def __setstate__(self, state): """ Our __getattr__ magic makes this necessary. """ for k, v in state.items(): setattr(self, k, v) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1547724155.0 structlog-20.1.0/src/structlog/_frames.py0000644000076500000240000000321400000000000020665 0ustar00hynekstaff00000000000000# 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(tuple(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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575555759.0 structlog-20.1.0/src/structlog/_generic.py0000644000076500000240000000272100000000000021026 0ustar00hynekstaff00000000000000# 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 def __getstate__(self): """ Our __getattr__ magic makes this necessary. """ return self.__dict__ def __setstate__(self, state): """ Our __getattr__ magic makes this necessary. """ for k, v in state.items(): setattr(self, k, v) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445528.0 structlog-20.1.0/src/structlog/_loggers.py0000644000076500000240000000537300000000000021062 0ustar00hynekstaff00000000000000# 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 pickle import PicklingError from structlog._utils import until_not_interrupted class PrintLoggerFactory(object): r""" Produce `PrintLogger`\ s. To be used with `structlog.configure`\ 's ``logger_factory``. :param file: File to print to. (default: stdout) :type file: file object 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 = {} def _get_lock_for_file(file): global WRITE_LOCKS lock = WRITE_LOCKS.get(file) if lock is None: lock = threading.Lock() WRITE_LOCKS[file] = lock return lock class PrintLogger(object): """ Print events into a file. :param file: File to print to. (default: stdout) :type file: file object >>> from structlog import PrintLogger >>> PrintLogger().msg("hello") hello Useful if you follow `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 self._lock = _get_lock_for_file(self._file) def __getstate__(self): """ Our __getattr__ magic makes this necessary. """ if self._file is sys.stdout: return "stdout" elif self._file is sys.stderr: return "stderr" raise PicklingError( "Only PrintLoggers to sys.stdout and sys.stderr can be pickled." ) def __setstate__(self, state): """ Our __getattr__ magic makes this necessary. """ if state == "stdout": self._file = sys.stdout else: self._file = sys.stderr self._write = self._file.write self._flush = self._file.flush self._lock = _get_lock_for_file(self._file) 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 fatal = failure = err = error = critical = exception = msg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1518763915.0 structlog-20.1.0/src/structlog/_utils.py0000644000076500000240000000137100000000000020552 0ustar00hynekstaff00000000000000# 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579415073.0 structlog-20.1.0/src/structlog/contextvars.py0000644000076500000240000000356400000000000021641 0ustar00hynekstaff00000000000000# 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 deal with a concurrency supporting context, as introduced in Python 3.7 as :mod:`contextvars`. .. versionadded:: 20.1.0 See :doc:`contextvars`. """ from __future__ import absolute_import, division, print_function import contextvars _CONTEXT = contextvars.ContextVar("structlog_context") def merge_contextvars(logger, method_name, event_dict): """ A processor that merges in a global (context-local) context. Use this as your first processor in :func:`structlog.configure` to ensure context-local context is included in all log calls. .. versionadded:: 20.1.0 """ ctx = _get_context().copy() ctx.update(event_dict) return ctx def clear_contextvars(): """ Clear the context-local context. The typical use-case for this function is to invoke it early in request- handling code. .. versionadded:: 20.1.0 """ ctx = _get_context() ctx.clear() def bind_contextvars(**kwargs): """ Put keys and values into the context-local context. Use this instead of :func:`~structlog.BoundLogger.bind` when you want some context to be global (context-local). .. versionadded:: 20.1.0 """ _get_context().update(kwargs) def unbind_contextvars(*keys): """ Remove *keys* from the context-local context if they are present. Use this instead of :func:`~structlog.BoundLogger.unbind` when you want to remove keys from a global (context-local) context. .. versionadded:: 20.1.0 """ ctx = _get_context() for key in keys: ctx.pop(key, None) def _get_context(): try: return _CONTEXT.get() except LookupError: _CONTEXT.set({}) return _CONTEXT.get() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580209056.0 structlog-20.1.0/src/structlog/dev.py0000644000076500000240000002230300000000000020027 0ustar00hynekstaff00000000000000# 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 import sys from six import PY2, StringIO, string_types try: import colorama except ImportError: colorama = None __all__ = ["ConsoleRenderer"] _IS_WINDOWS = sys.platform == "win32" _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. If ``event_dict`` contains an ``exception`` key (for example from :func:`~structlog.processors.format_exc_info`), it will be rendered *after* the log line. :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`, `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* `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 `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* .. versionchanged:: 19.2 ``colorama`` now initializes lazily to avoid unwanted initializations as ``ConsoleRenderer`` is used by default. .. versionchanged:: 19.2 Can be pickled now. .. versionchanged:: 20.1 ``colorama`` does not initialize lazily on Windows anymore because it breaks rendering. """ def __init__( self, pad_event=_EVENT_WIDTH, colors=_has_colorama, force_colors=False, repr_native_str=False, level_styles=None, ): self._force_colors = self._init_colorama = False if colors is True: if colorama is None: raise SystemError( _MISSING.format( who=self.__class__.__name__ + " with `colors=True`", package="colorama", ) ) if _IS_WINDOWS: # pragma: no cover _init_colorama(self._force_colors) else: self._init_colorama = True if force_colors: self._force_colors = True 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)) ) self._repr_native_str = repr_native_str def _repr(self, val): """ Determine representation of *val* depending on its type & self._repr_native_str. """ if self._repr_native_str is True: return repr(val) if isinstance(val, str): return val else: return repr(val) def __call__(self, _, __, event_dict): # Initialize lazily to prevent import side-effects. if self._init_colorama: _init_colorama(self._force_colors) self._init_colorama = False 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 + "] " ) # force event to str for compatibility with standard library event = event_dict.pop("event") if not PY2 or not isinstance(event, string_types): event = str(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 `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 `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, } def _init_colorama(force): if force: colorama.deinit() colorama.init(strip=False) else: colorama.init() _SENTINEL = object() def set_exc_info(_, method_name, event_dict): """ Set ``event_dict["exc_info"] = True`` if *method_name* is ``"exception"``. Do nothing if the name is different or ``exc_info`` is already set. """ if ( method_name != "exception" or event_dict.get("exc_info", _SENTINEL) is not _SENTINEL ): return event_dict event_dict["exc_info"] = True return event_dict ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1518763915.0 structlog-20.1.0/src/structlog/exceptions.py0000644000076500000240000000064600000000000021440 0ustar00hynekstaff00000000000000# 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. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445724.0 structlog-20.1.0/src/structlog/processors.py0000644000076500000240000002642200000000000021461 0ustar00hynekstaff00000000000000# 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*. If *default* is passed, it will disable support for ``__structlog__``-based serialization. :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. .. versionadded:: 18.2.0 Serializer's *default* parameter can be overwritten now. """ def __init__(self, serializer=json.dumps, **dumps_kw): dumps_kw.setdefault("default", _json_fallback_handler) self._dumps_kw = dumps_kw self._dumps = serializer def __call__(self, logger, name, event_dict): return self._dumps(event_dict, **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 `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. .. versionchanged:: 19.2 Can be pickled now. """ __slots__ = ("_stamper", "fmt", "utc", "key") def __init__(self, fmt=None, utc=True, key="timestamp"): self.fmt, self.utc, self.key = fmt, utc, key self._stamper = _make_stamper(fmt, utc, key) def __call__(self, _, __, event_dict): return self._stamper(event_dict) def __getstate__(self): return {"fmt": self.fmt, "utc": self.utc, "key": self.key} def __setstate__(self, state): self.fmt = state["fmt"] self.utc = state["utc"] self.key = state["key"] self._stamper = _make_stamper(**state) def _make_stamper(fmt, utc, key): """ Create a stamper function. """ if fmt is None and not utc: raise ValueError("UNIX timestamps are always UTC.") now = getattr(datetime.datetime, "utcnow" if utc else "now") if fmt is None: def stamper_unix(event_dict): event_dict[key] = time.time() return event_dict return stamper_unix elif fmt.upper() == "ISO": def stamper_iso_local(event_dict): event_dict[key] = now().isoformat() return event_dict def stamper_iso_utc(event_dict): event_dict[key] = now().isoformat() + "Z" return event_dict if utc: return stamper_iso_utc else: return stamper_iso_local def stamper_fmt(event_dict): event_dict[key] = now().strftime(fmt) return event_dict return stamper_fmt 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, 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: Target file for output (default: ``sys.stdout``). :type file: file object This processor is mostly for development and testing so you can read exceptions properly formatted. It behaves like 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445808.0 structlog-20.1.0/src/structlog/stdlib.py0000644000076500000240000004405000000000000020535 0ustar00hynekstaff00000000000000# 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 `logging.Logger.findCaller` to cope with ``structlog``'s extra frames. """ def findCaller(self, stack_info=False, stacklevel=1): """ 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 `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, ) It also contains a bunch of properties that pass-through to the wrapped `logging.Logger` which should make it work as a drop-in replacement. """ def debug(self, event=None, *args, **kw): """ Process event and call `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 `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 `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 `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 `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 `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 attributes and methods to mimick the stdlib's logger # interface. # @property def name(self): """ Returns :attr:`logging.Logger.name` """ return self._logger.name @property def level(self): """ Returns :attr:`logging.Logger.level` """ return self._logger.level @property def parent(self): """ Returns :attr:`logging.Logger.parent` """ return self._logger.parent @property def propagate(self): """ Returns :attr:`logging.Logger.propagate` """ return self._logger.propagate @property def handlers(self): """ Returns :attr:`logging.Logger.handlers` """ return self._logger.handlers @property def disabled(self): """ Returns :attr:`logging.Logger.disabled` """ return self._logger.disabled 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 `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_log_level_number(logger, method_name, event_dict): """ Add the log level number to the event dict. Log level numbers map to the log level names. The Python stdlib uses them for filtering logic. This adds the same numbers so users can leverage similar filtering. Compare:: level in ("warning", "error", "critical") level_number >= 30 The mapping of names to numbers is in ``structlog.stdlib._NAME_TO_LEVEL``. .. versionadded:: 18.2.0 """ event_dict["level_number"] = _NAME_TO_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 `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 `logging`. .. versionadded:: 17.1.0 """ return {"msg": event_dict.pop("event"), "extra": event_dict} class ProcessorFormatter(logging.Formatter): r""" Call ``structlog`` processors on :`logging.LogRecord`\ s. This `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 `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 `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 `logging.LogRecord`\ s is added to the ``event_dict`` and removed afterwards. Set this to ``True`` to keep it on the `logging.LogRecord`. (default: False) :param bool keep_stack_info: Same as *keep_exc_info* except for Python 3's ``stack_info``. (default: False) :param logger: Logger which we want to push through the ``structlog`` processor chain. This parameter is necessary for some of the processors like `filter_by_level`. (default: None) :param bool pass_foreign_args: If True, pass a foreign log record's ``args`` attribute to the ``event_dict`` under ``positional_args`` key. (default: False) :rtype: str .. versionadded:: 17.1.0 .. versionadded:: 17.2.0 *keep_exc_info* and *keep_stack_info* .. versionadded:: 19.2.0 *logger* .. versionadded:: 19.2.0 *pass_foreign_args* """ def __init__( self, processor, foreign_pre_chain=None, keep_exc_info=False, keep_stack_info=False, logger=None, pass_foreign_args=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 self.logger = logger self.pass_foreign_args = pass_foreign_args 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__) try: # Both attached by wrap_for_formatter logger = self.logger if self.logger is not None else 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() except AttributeError: logger = self.logger meth_name = record.levelname.lower() ed = {"event": record.getMessage(), "_record": record} if self.pass_foreign_args: ed["positional_args"] = record.args 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(logger, 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 `ProcessorFormatter` when formatting log entries. Use this static method as the renderer (i.e. final processor) if you want to use `ProcessorFormatter` in your `logging` configuration. """ return (event_dict,), {"extra": {"_logger": logger, "_name": name}} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579431249.0 structlog-20.1.0/src/structlog/testing.py0000644000076500000240000000473100000000000020733 0ustar00hynekstaff00000000000000# 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 to test your application's logging behavior. .. versionadded:: 20.1.0 See :doc:`testing`. """ from contextlib import contextmanager from ._config import configure, get_config from .exceptions import DropEvent __all__ = ["LogCapture", "capture_logs"] class LogCapture(object): """ Class for capturing log messages in its entries list. Generally you should use `structlog.testing.capture_logs`, but you can use this class if you want to capture logs with other patterns. .. versionadded:: 20.1.0 """ def __init__(self): self.entries = [] def __call__(self, _, method_name, event_dict): event_dict["log_level"] = method_name self.entries.append(event_dict) raise DropEvent @contextmanager def capture_logs(): """ Context manager that appends all logging statements to its yielded list while it is active. Attention: this is **not** thread-safe! .. versionadded:: 20.1.0 """ cap = LogCapture() old_processors = get_config()["processors"] try: configure(processors=[cap]) yield cap.entries finally: configure(processors=old_processors) class ReturnLoggerFactory(object): r""" Produce and cache `ReturnLogger`\ s. To be used with `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'}) .. 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 fatal = failure = err = error = critical = exception = msg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579951497.0 structlog-20.1.0/src/structlog/threadlocal.py0000644000076500000240000001407600000000000021543 0ustar00hynekstaff00000000000000# 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. See `thread-local`. """ from __future__ import absolute_import, division, print_function import contextlib import threading 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 _CONTEXT = threading.local() def merge_threadlocal(logger, method_name, event_dict): """ A processor that merges in a global (thread-local) context. Use this as your first processor in :func:`structlog.configure` to ensure thread-local context is included in all log calls. .. versionadded:: 19.2.0 .. versionchanged:: 20.1.0 This function used to be called ``merge_threalocal_context`` and that name is still kept around for backward compatability. """ context = _get_context().copy() context.update(event_dict) return context # Alias that shouldn't be used anymore. merge_threadlocal_context = merge_threadlocal def clear_threadlocal(): """ Clear the thread-local context. The typical use-case for this function is to invoke it early in request-handling code. .. versionadded:: 19.2.0 """ _CONTEXT.context = {} def bind_threadlocal(**kwargs): """ Put keys and values into the thread-local context. Use this instead of :func:`~structlog.BoundLogger.bind` when you want some context to be global (thread-local). .. versionadded:: 19.2.0 """ _get_context().update(kwargs) def unbind_threadlocal(*keys): """ Tries to remove bound *keys* from threadlocal logging context if present. .. versionadded:: 20.1.0 """ context = _get_context() for key in keys: context.pop(key, None) def _get_context(): try: return _CONTEXT.context except AttributeError: _CONTEXT.context = {} return _CONTEXT.context ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579445955.0 structlog-20.1.0/src/structlog/twisted.py0000644000076500000240000002236300000000000020742 0ustar00hynekstaff00000000000000# 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 `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 ``twisted.python.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, just for tests. """ return ( isinstance(other, self.__class__) and self.string == other.string ) def __repr__(self): return self.string class JSONRenderer(GenericJSONRenderer): """ Behaves like `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 `EventAdapter` but a real formatter. Also does *not* require to be adapted using it. Use together with a `JSONLogObserverWrapper`-wrapped Twisted logger like `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 to print to. :type file: file object .. 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-`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-`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 `PlainFileLogObserver` and `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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1580211815.6683328 structlog-20.1.0/src/structlog.egg-info/0000755000076500000240000000000000000000000020371 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211815.0 structlog-20.1.0/src/structlog.egg-info/PKG-INFO0000644000076500000240000002621400000000000021473 0ustar00hynekstaff00000000000000Metadata-Version: 2.1 Name: structlog Version: 20.1.0 Summary: Structured Logging for Python Home-page: https://www.structlog.org/ Author: Hynek Schlawack Author-email: hs@ox.cx Maintainer: Hynek Schlawack Maintainer-email: hs@ox.cx License: MIT or Apache License, Version 2.0 Description: .. image:: https://www.structlog.org/en/latest/_static/structlog_logo_small.png :alt: structlog Logo :width: 256px :target: https://www.structlog.org/ ============================================ ``structlog``: Structured Logging for Python ============================================ .. image:: https://img.shields.io/pypi/v/structlog.svg :target: https://pypi.org/project/structlog/ :alt: PyPI .. image:: https://readthedocs.org/projects/structlog/badge/?version=stable :target: https://www.structlog.org/en/stable/?badge=stable :alt: Documentation Status .. image:: https://dev.azure.com/the-hynek/structlog/_apis/build/status/hynek.structlog?branchName=master :target: https://dev.azure.com/the-hynek/structlog/_build?definitionId=1 :alt: CI Status .. image:: https://codecov.io/github/hynek/structlog/branch/master/graph/badge.svg :target: https://codecov.io/github/hynek/structlog :alt: Test Coverage .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: black .. -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- Once you feel inspired to try it out, check out our friendly `Getting Started tutorial `_ that also contains detailed installation instructions! .. -begin-spiel- If you prefer videos over reading, check out this DjangoCon Europe 2019 talk by `Markus Holtermann `_: "`Logging Rethought 2: The Actions of Frank Taylor Jr. `_". 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- .. -begin-meta- Getting Help ============ Please use the ``structlog`` tag on `StackOverflow `_ to get help. Answering questions of your fellow developers is also great way to help the project! Project Information =================== ``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 https://www.structlog.org/. We collect useful third party extension in `our wiki `_. ``structlog`` targets Python 2.7, 3.5 and newer, and PyPy. Release Information =================== 20.1.0 (2020-01-28) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - This is the last version to support Python 2.7 (including PyPy) and 3.5. All following versions will only support Python 3.6 or later. Changes: ^^^^^^^^ - Added a new module ``structlog.contextvars`` that allows to have a global but context-local ``structlog`` context the same way as with ``structlog.threadlocal`` since 19.2.0. `#201 `_, `#236 `_ - Added a new module ``structlog.testing`` for first class testing support. The first entry is the context manager ``capture_logs()`` that allows to make assertions about structured log calls. `#14 `_, `#234 `_ - Added ``structlog.threadlocal.unbind_threadlocal()``. `#239 `_ - The logger created by ``structlog.get_logger()`` is not detected as an abstract method anymore, when attached to an abstract base class. `#229 `_ - ``colorama`` isn't initialized lazily on Windows anymore because it breaks rendering. `#232 `_, `#242 `_ `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.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst Provides-Extra: tests Provides-Extra: docs Provides-Extra: dev Provides-Extra: azure-pipelines ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211815.0 structlog-20.1.0/src/structlog.egg-info/SOURCES.txt0000644000076500000240000000426200000000000022261 0ustar00hynekstaff00000000000000.pre-commit-config.yaml .readthedocs.yml AUTHORS.rst CHANGELOG.rst LICENSE LICENSE.apache2 LICENSE.mit MANIFEST.in README.rst azure-pipelines.yml codecov.yml conftest.py pyproject.toml setup.py tox.ini .github/CODE_OF_CONDUCT.rst .github/CONTRIBUTING.rst .github/PULL_REQUEST_TEMPLATE.md docs/Makefile docs/api.rst docs/backward-compatibility.rst docs/changelog.rst docs/conf.py docs/configuration.rst docs/contextvars.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/testing.rst docs/thread-local.rst docs/twisted.rst docs/why.rst docs/_static/.keep docs/_static/BoundLogger.svg 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/contextvars.py src/structlog/dev.py src/structlog/exceptions.py src/structlog/processors.py src/structlog/stdlib.py src/structlog/testing.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_contextvars.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_testing.py tests/test_threadlocal.py tests/test_twisted.py tests/test_utils.py tests/utils.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211815.0 structlog-20.1.0/src/structlog.egg-info/dependency_links.txt0000644000076500000240000000000100000000000024437 0ustar00hynekstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1445001276.0 structlog-20.1.0/src/structlog.egg-info/not-zip-safe0000644000076500000240000000000100000000000022617 0ustar00hynekstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211815.0 structlog-20.1.0/src/structlog.egg-info/requires.txt0000644000076500000240000000114700000000000022774 0ustar00hynekstaff00000000000000six [azure-pipelines] coverage[toml] freezegun>=0.2.8 pretend pytest>=3.3.0 simplejson pytest-azurepipelines [azure-pipelines:python_version >= "3.6"] python-rapidjson [azure-pipelines:python_version >= "3.7"] pytest-asyncio [dev] coverage[toml] freezegun>=0.2.8 pretend pytest>=3.3.0 simplejson sphinx twisted pre-commit [dev:python_version >= "3.6"] python-rapidjson [dev:python_version >= "3.7"] pytest-asyncio [docs] sphinx twisted [tests] coverage[toml] freezegun>=0.2.8 pretend pytest>=3.3.0 simplejson [tests:python_version >= "3.6"] python-rapidjson [tests:python_version >= "3.7"] pytest-asyncio ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580211815.0 structlog-20.1.0/src/structlog.egg-info/top_level.txt0000644000076500000240000000001200000000000023114 0ustar00hynekstaff00000000000000structlog ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1580211815.67758 structlog-20.1.0/tests/0000755000076500000240000000000000000000000015224 5ustar00hynekstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1518763915.0 structlog-20.1.0/tests/__init__.py0000644000076500000240000000026500000000000017340 0ustar00hynekstaff00000000000000# 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1518763915.0 structlog-20.1.0/tests/additional_frame.py0000644000076500000240000000077500000000000021071 0ustar00hynekstaff00000000000000# 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578395772.0 structlog-20.1.0/tests/test_base.py0000644000076500000240000001354100000000000017553 0ustar00hynekstaff00000000000000# 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.exceptions import DropEvent from structlog.processors import KeyValueRenderer from structlog.testing import ReturnLogger 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): """ unbind() removes keys from context. """ b = build_bl().bind(x=42, y=23).unbind("x", "y") assert {} == b._context def test_unbind_fail(self): """ unbind() raises KeyError if the key is missing. """ with pytest.raises(KeyError): build_bl().bind(x=42, y=23).unbind("x", "z") def test_try_unbind(self): """ try_unbind() removes keys from context. """ b = build_bl().bind(x=42, y=23).try_unbind("x", "y") assert {} == b._context def test_try_unbind_fail(self): """ try_unbind() does nothing if the key is missing. """ b = build_bl().bind(x=42, y=23).try_unbind("x", "z") assert {"y": 23} == 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579960287.0 structlog-20.1.0/tests/test_config.py0000644000076500000240000002572500000000000020115 0ustar00hynekstaff00000000000000# 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 abc import pickle import platform import sys import warnings from collections import OrderedDict import pytest from pretend import call, call_recorder, stub from six import PY3, add_metaclass 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_lazy_logger_is_not_detected_as_abstract_method(): """ If someone defines an attribute on an ABC with a logger, that logger is not detected as an abstract method. See https://github.com/hynek/structlog/issues/229 """ @add_metaclass(abc.ABCMeta) class Foo(object): log = structlog.get_logger() Foo() 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() @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL)) def test_pickle(self, proto): """ Can be pickled and unpickled. """ bllp = BoundLoggerLazyProxy(None) assert repr(bllp) == repr(pickle.loads(pickle.dumps(bllp, proto))) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579415073.0 structlog-20.1.0/tests/test_contextvars.py0000644000076500000240000001005000000000000021211 0ustar00hynekstaff00000000000000# 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 pytest from structlog.contextvars import ( bind_contextvars, clear_contextvars, merge_contextvars, unbind_contextvars, ) # All test coroutines will be treated as marked. pytestmark = pytest.mark.asyncio class TestNewContextvars(object): async def test_bind(self, event_loop): """ Binding a variable causes it to be included in the result of merge_contextvars. """ async def coro(): bind_contextvars(a=1) return merge_contextvars(None, None, {"b": 2}) assert {"a": 1, "b": 2} == await event_loop.create_task(coro()) async def test_multiple_binds(self, event_loop): """ Multiple calls to bind_contextvars accumulate values instead of replacing them. But they override redefined ones. """ async def coro(): bind_contextvars(a=1, c=3) bind_contextvars(c=333, d=4) return merge_contextvars(None, None, {"b": 2}) assert { "a": 1, "b": 2, "c": 333, "d": 4, } == await event_loop.create_task(coro()) async def test_nested_async_bind(self, event_loop): """ Context is passed correctly between "nested" concurrent operations. """ async def coro(): bind_contextvars(a=1) await event_loop.create_task(nested_coro()) return merge_contextvars(None, None, {"b": 2}) async def nested_coro(): bind_contextvars(c=3) assert {"a": 1, "b": 2, "c": 3} == await event_loop.create_task(coro()) async def test_merge_works_without_bind(self, event_loop): """ merge_contextvars returns values as normal even when there has been no previous calls to bind_contextvars. """ async def coro(): return merge_contextvars(None, None, {"b": 2}) assert {"b": 2} == await event_loop.create_task(coro()) async def test_merge_overrides_bind(self, event_loop): """ Variables included in merge_contextvars override previously bound variables. """ async def coro(): bind_contextvars(a=1) return merge_contextvars(None, None, {"a": 111, "b": 2}) assert {"a": 111, "b": 2} == await event_loop.create_task(coro()) async def test_clear(self, event_loop): """ The context-local context can be cleared, causing any previously bound variables to not be included in merge_contextvars's result. """ async def coro(): bind_contextvars(a=1) clear_contextvars() return merge_contextvars(None, None, {"b": 2}) assert {"b": 2} == await event_loop.create_task(coro()) async def test_clear_without_bind(self, event_loop): """ The context-local context can be cleared, causing any previously bound variables to not be included in merge_contextvars's result. """ async def coro(): clear_contextvars() return merge_contextvars(None, None, {}) assert {} == await event_loop.create_task(coro()) async def test_undbind(self, event_loop): """ Unbinding a previously bound variable causes it to be removed from the result of merge_contextvars. """ async def coro(): bind_contextvars(a=1) unbind_contextvars("a") return merge_contextvars(None, None, {"b": 2}) assert {"b": 2} == await event_loop.create_task(coro()) async def test_undbind_not_bound(self, event_loop): """ Unbinding a not bound variable causes doesn't raise an exception. """ async def coro(): unbind_contextvars("a") return merge_contextvars(None, None, {"b": 2}) assert {"b": 2} == await event_loop.create_task(coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/tests/test_dev.py0000644000076500000240000002452600000000000017424 0ustar00hynekstaff00000000000000# -*- 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 pickle 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(colors=True) 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_event_stringified(self, cr, styles, unpadded): """ Event is cast to string. """ not_a_string = Exception("test") rv = cr(None, None, {"event": not_a_string}) assert unpadded == rv @pytest.mark.skipif(not six.PY2, reason="Problem only exists on Python 2.") @pytest.mark.parametrize("s", [u"\xc3\xa4".encode("utf-8"), u"ä", "ä"]) def test_event_py2_only_stringify_non_strings(self, cr, s, styles): """ If event is a string type already, leave it be on Python 2. Running str() on unicode strings with non-ascii characters raises an error. """ rv = cr(None, None, {"event": s}) assert styles.bright + s + styles.reset == 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"} ) # fmt: off 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 # fmt: on 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"} ) # fmt: off 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 # fmt: on def test_logger_name(self, cr, styles, padded): """ Logger names are appended after the event. """ rv = cr(None, None, {"event": "test", "logger": "some_module"}) # fmt: off assert ( padded + "[" + dev.BLUE + styles.bright + "some_module" + styles.reset + "] " ) == rv # fmt: on 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"}) # fmt: off 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 # fmt: on 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"} ) # fmt: off assert ( styles.bright + dev._pad("test", 42) + styles.reset + " " + styles.kv_key + "foo" + styles.reset + "=" + styles.kv_value + "bar" + styles.reset ) == rv # fmt: on 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, }, ) # fmt: off 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 # fmt: on 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"} ) # fmt: off 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 # fmt: on 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 @pytest.mark.parametrize("repr_native_str", [True, False]) @pytest.mark.parametrize("force_colors", [True, False]) @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL)) def test_pickle(self, repr_native_str, force_colors, proto): """ ConsoleRenderer can be pickled and unpickled. """ r = dev.ConsoleRenderer( repr_native_str=repr_native_str, force_colors=force_colors ) assert r(None, None, {"event": "foo"}) == pickle.loads( pickle.dumps(r, proto) )(None, None, {"event": "foo"}) class TestSetExcInfo(object): def test_wrong_name(self): """ Do nothing if name is not exception. """ assert {} == dev.set_exc_info(None, "foo", {}) @pytest.mark.parametrize("ei", [False, None, ()]) def test_already_set(self, ei): """ Do nothing if exc_info is already set. """ assert {"exc_info": ei} == dev.set_exc_info( None, "foo", {"exc_info": ei} ) def test_set_it(self): """ Set exc_info to True if its not set and if the method name is exception. """ assert {"exc_info": True} == dev.set_exc_info(None, "exception", {}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/tests/test_frames.py0000644000076500000240000001141100000000000020110 0ustar00hynekstaff00000000000000# 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578395764.0 structlog-20.1.0/tests/test_generic.py0000644000076500000240000000355400000000000020260 0ustar00hynekstaff00000000000000# 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 pickle import pytest import six from structlog._config import _CONFIG from structlog._generic import BoundLogger from structlog.testing 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") @pytest.mark.skipif(six.PY2, reason="Needs Py3 or dill.") @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL)) def test_pickle(self, proto): """ Can be pickled and unpickled. Works only on Python 3: TypeError: can't pickle instancemethod objects """ b = BoundLogger( ReturnLogger(), _CONFIG.default_processors, _CONFIG.default_context_class(), ).bind(x=1) assert b.info("hi") == pickle.loads(pickle.dumps(b, proto)).info("hi") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578395772.0 structlog-20.1.0/tests/test_loggers.py0000644000076500000240000000660500000000000020306 0ustar00hynekstaff00000000000000# 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 pickle import sys import pytest from six.moves import cStringIO as StringIO from structlog._loggers import WRITE_LOCKS, PrintLogger, PrintLoggerFactory from .utils import stdlib_log_methods 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(" b=[3, 4] x=7 y='test' z=(1, 2)" == rv def test_order_complete(self, event_dict): """ Orders keys according to key_order. """ rv = KeyValueRenderer(key_order=["y", "b", "a", "z", "x"])( None, None, event_dict ) assert r"y='test' b=[3, 4] a= z=(1, 2) x=7" == rv def test_order_missing(self, event_dict): """ Missing keys get rendered as None. """ rv = KeyValueRenderer(key_order=["c", "y", "b", "a", "z", "x"])( None, None, event_dict ) assert r"c=None y='test' b=[3, 4] a= z=(1, 2) x=7" == rv def test_order_missing_dropped(self, event_dict): """ Missing keys get dropped """ rv = KeyValueRenderer( key_order=["c", "y", "b", "a", "z", "x"], drop_missing=True )(None, None, event_dict) assert r"y='test' b=[3, 4] a= z=(1, 2) x=7" == rv def test_order_extra(self, event_dict): """ Extra keys get sorted if sort_keys=True. """ event_dict["B"] = "B" event_dict["A"] = "A" rv = KeyValueRenderer( key_order=["c", "y", "b", "a", "z", "x"], sort_keys=True )(None, None, event_dict) assert ( r"c=None y='test' b=[3, 4] a= z=(1, 2) x=7 A='A' B='B'" ) == rv 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" rv = KeyValueRenderer( key_order=["c", "y", "b", "a", "z", "x"], sort_keys=True, drop_missing=True, )(None, None, event_dict) assert r"y='test' b=[3, 4] a= z=(1, 2) x=7 A='A' B='B'" == rv 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. """ rv = JSONRenderer(sort_keys=True)(None, None, event_dict) assert ( r'{"a": "", "b": [3, 4], "x": 7, ' r'"y": "test", "z": ' r"[1, 2]}" ) == rv 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) def test_custom_fallback(self): """ A custom fallback handler can be used. """ jr = JSONRenderer(default=lambda x: repr(x)[::-1]) d = {"date": datetime.date(1980, 3, 25)} assert '{"date": ")52 ,3 ,0891(etad.emitetad"}' == jr(None, None, d) @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"] @freeze_time("1980-03-25 16:00:00") @pytest.mark.parametrize("fmt", [None, "%Y"]) @pytest.mark.parametrize("utc", [True, False]) @pytest.mark.parametrize("key", [None, "other-key"]) @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL)) def test_pickle(self, fmt, utc, key, proto): """ TimeStamper is serializable. """ # UNIX timestamps must be UTC. if fmt is None and not utc: pytest.skip() ts = TimeStamper() assert ts(None, None, {}) == pickle.loads(pickle.dumps(ts, proto))( None, None, {} ) 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/tests/test_stdlib.py0000644000076500000240000005662000000000000020127 0ustar00hynekstaff00000000000000# 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 ( _NAME_TO_LEVEL, CRITICAL, WARN, BoundLogger, LoggerFactory, PositionalArgumentsFormatter, ProcessorFormatter, _FixedFindCallerLogger, add_log_level, add_log_level_number, 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( "attribute_name", ["name", "level", "parent", "propagate", "handlers", "disabled"], ) def test_stdlib_passthrough_attributes(self, attribute_name): """ stdlib logger attributes are also available in stdlib BoundLogger. """ stdlib_logger = logging.getLogger("Test") stdlib_logger_attribute = getattr(stdlib_logger, attribute_name) bl = BoundLogger(stdlib_logger, [], {}) bound_logger_attribute = getattr(bl, attribute_name) assert bound_logger_attribute == stdlib_logger_attribute @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 TestAddLogLevelNumber(object): @pytest.mark.parametrize("level, number", _NAME_TO_LEVEL.items()) def test_log_level_number_added(self, level, number): """ The log level number is added to the event dict. """ event_dict = add_log_level_number(None, level, {}) assert number == event_dict["level_number"] 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, logger=None, pass_foreign_args=False): """ 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]", "logger": logger, "pass_foreign_args": pass_foreign_args, } }, "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, configure_for_pf, capsys): """ 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_pass_foreign_args_true_sets_positional_args_key( self, configure_for_pf, capsys ): """ If `pass_foreign_args` is `True` we set the `positional_args` key in the `event_dict` before clearing args. """ test_processor = call_recorder(lambda l, m, event_dict: event_dict) configure_logging((test_processor,), pass_foreign_args=True) configure( processors=[ProcessorFormatter.wrap_for_formatter], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) positional_args = {"foo": "bar"} logging.getLogger().info("okay %(foo)s", positional_args) event_dict = test_processor.calls[0].args[2] assert "positional_args" in event_dict assert positional_args == event_dict["positional_args"] def test_log_dict(self, configure_for_pf, capsys): """ Test that dicts can be logged with std library loggers. """ configure_logging(None) logging.getLogger().warning({"foo": "bar"}) assert ( "", "{'foo': 'bar'} [in test_log_dict]\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_chain_can_pass_dictionaries_without_excepting( self, configure_for_pf, capsys ): """ If a foreign logger passes a dictionary to a logging function, check we correctly identify that it did not come from structlog. """ configure_logging(None) configure( processors=[ProcessorFormatter.wrap_for_formatter], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) logging.getLogger().warning({"foo": "bar"}) assert ( "", "{'foo': 'bar'} [in " "test_foreign_chain_can_pass_dictionaries_without_excepting]\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() def test_foreign_pre_chain_filter_by_level(self, configure_for_pf, capsys): """ foreign_pre_chain works with filter_by_level processor. """ logger = logging.getLogger() configure_logging((filter_by_level,), logger=logger) configure( processors=[ProcessorFormatter.wrap_for_formatter], logger_factory=LoggerFactory(), wrapper_class=BoundLogger, ) logger.warning("foo") assert ( "", "foo [in test_foreign_pre_chain_filter_by_level]\n", ) == capsys.readouterr() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578395984.0 structlog-20.1.0/tests/test_testing.py0000644000076500000240000000572200000000000020320 0ustar00hynekstaff00000000000000# -*- 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 structlog import get_config, get_logger, reset_defaults, testing from structlog.testing import ReturnLogger, ReturnLoggerFactory class TestCaptureLogs(object): @classmethod def teardown_class(cls): reset_defaults() def test_captures_logs(self): """ Log entries are captured and retain their structure. """ with testing.capture_logs() as logs: get_logger().bind(x="y").info("hello", answer=42) get_logger().bind(a="b").info("goodbye", foo={"bar": "baz"}) assert [ {"event": "hello", "log_level": "info", "x": "y", "answer": 42}, { "a": "b", "event": "goodbye", "log_level": "info", "foo": {"bar": "baz"}, }, ] == logs def get_active_procs(self): return get_config()["processors"] def test_restores_processors_on_success(self): """ Processors are patched within the contextmanger and restored on exit. """ orig_procs = self.get_active_procs() with testing.capture_logs(): assert orig_procs is not self.get_active_procs() assert orig_procs is self.get_active_procs() def test_restores_processors_on_error(self): """ Processors are restored even on errors. """ orig_procs = self.get_active_procs() with pytest.raises(NotImplementedError): with testing.capture_logs(): raise NotImplementedError("from test") assert orig_procs is self.get_active_procs() class TestReturnLogger(object): # @pytest.mark.parametrize("method", stdlib_log_methods) def test_stdlib_methods_support(self, stdlib_log_method): """ ReturnLogger implements methods of stdlib loggers. """ v = getattr(ReturnLogger(), stdlib_log_method)("hello") assert "hello" == v def test_return_logger(self): """ Return logger returns exactly what's sent in. """ obj = ["hello"] assert obj is ReturnLogger().msg(obj) class TestReturnLoggerFactory(object): def test_builds_returnloggers(self): f = ReturnLoggerFactory() assert isinstance(f(), ReturnLogger) def test_caches(self): """ There's no need to have several loggers so we return the same one on each call. """ f = ReturnLoggerFactory() assert f() is f() def test_ignores_args(self): """ ReturnLogger doesn't take positional arguments. If any are passed to the factory, they are not passed to the logger. """ ReturnLoggerFactory()(1, 2, 3) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579952027.0 structlog-20.1.0/tests/test_threadlocal.py0000644000076500000240000002055100000000000021122 0ustar00hynekstaff00000000000000# 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 threading from collections import OrderedDict import pytest from structlog._base import BoundLoggerBase from structlog._config import wrap_logger from structlog.testing import ReturnLogger from structlog.threadlocal import ( as_immutable, bind_threadlocal, clear_threadlocal, merge_threadlocal, merge_threadlocal_context, tmp_bind, unbind_threadlocal, wrap_dict, ) try: import greenlet except ImportError: greenlet = None @pytest.fixture def D(): """ Returns a dict wrapped in _ThreadLocalDictWrapper. """ return wrap_dict(dict) @pytest.fixture def log(logger): """ Returns a ReturnLogger with a freshly wrapped OrderedDict. """ return wrap_logger(logger, context_class=wrap_dict(OrderedDict)) @pytest.fixture def logger(): """ Returns a simple logger stub with a *msg* method that takes one argument which gets returned. """ return ReturnLogger() class TestTmpBind(object): def test_bind(self, log): """ tmp_bind does not modify the thread-local state. """ log = log.bind(y=23) with tmp_bind(log, x=42, y="foo") as tmp_log: assert ( {"y": "foo", "x": 42} == tmp_log._context._dict == log._context._dict ) assert {"y": 23} == log._context._dict def test_bind_exc(self, log): """ tmp_bind cleans up properly on exceptions. """ log = log.bind(y=23) with pytest.raises(ValueError): with tmp_bind(log, x=42, y="foo") as tmp_log: assert ( {"y": "foo", "x": 42} == tmp_log._context._dict == log._context._dict ) raise ValueError assert {"y": 23} == log._context._dict class TestAsImmutable(object): def test_does_not_affect_global(self, log): """ A logger from as_mutable is independent from thread local state. """ log = log.new(x=42) il = as_immutable(log) assert isinstance(il._context, dict) il = il.bind(y=23) assert {"x": 42, "y": 23} == il._context assert {"x": 42} == log._context._dict def test_converts_proxy(self, log): """ as_immutable converts a BoundLoggerLazyProxy into a concrete bound logger. """ il = as_immutable(log) assert isinstance(il._context, dict) assert isinstance(il, BoundLoggerBase) def test_idempotency(self, log): """ as_immutable on an as_immutable logger works. """ il = as_immutable(log) assert isinstance(as_immutable(il), BoundLoggerBase) class TestThreadLocalDict(object): def test_wrap_returns_distinct_classes(self): """ Each call to wrap_dict returns a distinct new class whose context is independent from others. """ D1 = wrap_dict(dict) D2 = wrap_dict(dict) assert D1 != D2 assert D1 is not D2 D1.x = 42 D2.x = 23 assert D1.x != D2.x @pytest.mark.skipif( greenlet is not None, reason="Don't mix threads and greenlets." ) def test_is_thread_local(self, D): """ The context is *not* shared between threads. """ class TestThread(threading.Thread): def __init__(self, d): self._d = d threading.Thread.__init__(self) def run(self): assert "tl" not in self._d._dict self._d["tl"] = 23 d = wrap_dict(dict)() d["tl"] = 42 t = TestThread(d) t.start() t.join() assert 42 == d._dict["tl"] def test_context_is_global_to_thread(self, D): """ The context is shared between all instances of a wrapped class. """ d1 = D({"a": 42}) d2 = D({"b": 23}) d3 = D() assert {"a": 42, "b": 23} == d1._dict == d2._dict == d3._dict assert d1 == d2 == d3 D_ = wrap_dict(dict) d_ = D_({"a": 42, "b": 23}) assert d1 != d_ def test_init_with_itself_works(self, D): """ Initializing with an instance of the wrapped class will use its values. """ d = D({"a": 42}) assert {"a": 42, "b": 23} == D(d, b=23)._dict def test_iter_works(self, D): """ ___iter__ is proxied to the wrapped class. """ d = D({"a": 42}) assert ["a"] == list(iter(d)) def test_non_dunder_proxy_works(self, D): """ Calls to a non-dunder method get proxied to the wrapped class. """ d = D({"a": 42}) d.clear() assert 0 == len(d) def test_repr(self, D): """ ___repr__ takes the repr of the wrapped class into account. """ r = repr(D({"a": 42})) assert r.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()) class TestNewThreadLocal(object): def test_alias(self): """ We're keeping the old alias around. """ assert merge_threadlocal_context is merge_threadlocal def test_bind_and_merge(self): """ Binding a variable causes it to be included in the result of merge_threadlocal. """ bind_threadlocal(a=1) assert {"a": 1, "b": 2} == merge_threadlocal(None, None, {"b": 2}) def test_clear(self): """ The thread-local context can be cleared, causing any previously bound variables to not be included in merge_threadlocal's result. """ bind_threadlocal(a=1) clear_threadlocal() assert {"b": 2} == merge_threadlocal(None, None, {"b": 2}) def test_merge_works_without_bind(self): """ merge_threadlocal returns values as normal even when there has been no previous calls to bind_threadlocal. """ assert {"b": 2} == merge_threadlocal(None, None, {"b": 2}) def test_multiple_binds(self): """ Multiple calls to bind_threadlocal accumulate values instead of replacing them. """ bind_threadlocal(a=1, b=2) bind_threadlocal(c=3) assert {"a": 1, "b": 2, "c": 3} == merge_threadlocal( None, None, {"b": 2} ) def test_unbind_threadlocal(self): """ Test that unbinding from threadlocal works for keys that exist and does not raise error when they do not exist. """ clear_threadlocal() bind_threadlocal(a=234, b=34) assert {"a": 234, "b": 34} == merge_threadlocal_context(None, None, {}) unbind_threadlocal("a") assert {"b": 34} == merge_threadlocal_context(None, None, {}) unbind_threadlocal("non-existing-key") assert {"b": 34} == merge_threadlocal_context(None, None, {}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/tests/test_twisted.py0000644000076500000240000002443700000000000020332 0ustar00hynekstaff00000000000000# 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 twisted.python.failure import Failure, NoCurrentExceptionError from twisted.python.log import ILogObserver 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, ) 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()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571250861.0 structlog-20.1.0/tests/test_utils.py0000644000076500000240000000203600000000000017776 0ustar00hynekstaff00000000000000# 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] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1578395772.0 structlog-20.1.0/tests/utils.py0000644000076500000240000000105100000000000016733 0ustar00hynekstaff00000000000000# 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 from structlog.stdlib import _NAME_TO_LEVEL py3_only = pytest.mark.skipif(not six.PY3, reason="Python 3-only") py2_only = pytest.mark.skipif(not six.PY2, reason="Python 2-only") stdlib_log_methods = [m for m in _NAME_TO_LEVEL if m != "notset"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579444800.0 structlog-20.1.0/tox.ini0000644000076500000240000000352700000000000015404 0ustar00hynekstaff00000000000000[pytest] strict = true addopts = -ra testpaths = tests filterwarnings = once::Warning [tox] envlist = lint,{py27,py35,py36,py37,py38,pypy,pypy3}-threads,{py27,py37,py38,pypy}-{greenlets,colorama},docs,pypi-description,manifest,coverage-report isolated_build = True [testenv:lint] basepython = python3.7 skip_install = true deps = pre-commit passenv = HOMEPATH # needed on Windows commands = pre-commit run --all-files [testenv] extras = {env:TOX_AP_TEST_EXTRAS:tests} deps = greenlets: greenlet threads,greenlets,colorama: twisted colorama: colorama setenv = PYTHONHASHSEED = 0 commands = python -m pytest {posargs} [testenv:py37-threads] deps = twisted setenv = PYTHONHASHSEED = 0 commands = coverage run -m pytest {posargs} [testenv:py37-greenlets] deps = greenlet twisted setenv = PYTHONHASHSEED = 0 commands = coverage run -m pytest {posargs} [testenv:py27-threads] deps = twisted setenv = PYTHONHASHSEED = 0 commands = coverage run -m pytest {posargs} [testenv:py27-colorama] deps = colorama twisted setenv = PYTHONHASHSEED = 0 commands = coverage run -m pytest {posargs} [testenv:docs] basepython = python3.7 extras = docs passenv = TERM setenv = PYTHONHASHSEED = 0 commands = sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html [testenv:pypi-description] basepython = python3.7 skip_install = true deps = twine pip >= 18.0.0 commands = pip wheel -w {envtmpdir}/build --no-deps . twine check {envtmpdir}/build/* [testenv:manifest] basepython = python3.7 skip_install = true deps = check-manifest commands = check-manifest [testenv:coverage-report] basepython = python3.7 deps = coverage[toml] skip_install = true commands = coverage combine coverage report