├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── docs.yml │ ├── lint.yml │ ├── publish-to-pypi.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.rst ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README-various.md ├── README.md ├── docs ├── .gitignore ├── Makefile ├── _static │ ├── demo-output-json.png │ ├── demo-output-with-beaver.png │ ├── demo_output.png │ ├── demo_output_with_exception.png │ ├── logo-420.png │ ├── logo-big.png │ ├── logo-text-wide.psd │ ├── logo.ai │ ├── logo.png │ └── logo_1000x500.png ├── conf.py ├── contributing.rst ├── index.rst └── make.bat ├── examples ├── demo.py └── demo2.py ├── logzero ├── __init__.py ├── colors.py └── jsonlogger.py ├── requirements_dev.txt ├── requirements_windows.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_json.py ├── test_logzero.py └── test_new_api.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [*.bat] 17 | indent_style = tab 18 | end_of_line = crlf 19 | 20 | [LICENSE] 21 | insert_final_newline = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * logzero version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Note: to test with Python 2.7, a downgrade in pytest is necessary - last supported is 4.6 series https://docs.pytest.org/en/stable/py27-py34-deprecation.html 2 | name: Generate the docs 3 | 4 | on: [push] 5 | 6 | jobs: 7 | generate_docs: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.x 13 | uses: actions/setup-python@v2 14 | with: 15 | # Semantic version range syntax or exact version of a Python version 16 | python-version: '3.x' 17 | - name: Display Python version 18 | run: python -c "import sys; print(sys.version)" 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r requirements_dev.txt 23 | - name: Generate the docs 24 | run: make docs 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Note: to test with Python 2.7, a downgrade in pytest is necessary - last supported is 4.6 series https://docs.pytest.org/en/stable/py27-py34-deprecation.html 2 | name: Lint the code 3 | 4 | on: [push] 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.x 13 | uses: actions/setup-python@v2 14 | with: 15 | # Semantic version range syntax or exact version of a Python version 16 | python-version: '3.x' 17 | - name: Display Python version 18 | run: python -c "import sys; print(sys.version)" 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r requirements_dev.txt 23 | - name: Lint 24 | run: make lint 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Set up Python 3.x 14 | uses: actions/setup-python@v2 15 | with: 16 | # Semantic version range syntax or exact version of a Python version 17 | python-version: '3.x' 18 | 19 | - name: Install dependencies 20 | run: pip install wheel 21 | - name: Build package 22 | run: python setup.py sdist bdist_wheel 23 | 24 | - name: Publish a Python distribution to PyPI 25 | uses: pypa/gh-action-pypi-publish@master 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_API_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run the tests and demo.py 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-pytest: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.6, 3.7, 3.8, 3.9] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | 19 | - name: Install dependencies 20 | run: | 21 | pip install --upgrade pip 22 | pip install -e . 23 | pip install -r requirements_dev.txt 24 | 25 | - name: Run the tests 26 | run: make test 27 | 28 | - name: Run demo.py 29 | run: python examples/demo.py 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .venv* 6 | venv* 7 | /test.py 8 | _tests/ 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # pyenv python configuration file 66 | .python-version 67 | 68 | # various 69 | *.internal.* 70 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/metachris/logzero/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | and "help wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | logzero could always use more documentation, whether as part of the 42 | official logzero docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/metachris/logzero/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `logzero` for local development. 61 | 62 | 1. Fork the `logzero` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/logzero.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv logzero 70 | $ cd logzero/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 80 | 81 | $ flake8 logzero tests 82 | $ python setup.py test or py.test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub:: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. Put 102 | your new functionality into a function with a docstring, and add the 103 | feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.4, 3.5, 3.6 and for PyPy. Check 105 | https://travis-ci.org/metachris/logzero/pull_requests 106 | and make sure that the tests pass for all supported Python versions. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | $ py.test tests.test_logzero 114 | 115 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 1.7.0 (2021-03-17) 5 | ------------------ 6 | - Export loglevels directly (you can use eg. `logzero.DEBUG` instead of `logging.DEBUG`) 7 | - `setup_default_logger` use `backupCount` 8 | - Update dependencies 9 | - PRs: (386)[https://github.com/metachris/logzero/pull/386] 10 | 11 | 12 | 1.6.3 (2020-11-15) 13 | ------------------ 14 | 15 | - JSON logging with UTF-8 enabled by default ([PR 357](https://github.com/metachris/logzero/pull/357)) 16 | 17 | 18 | 1.6.0 (1.6.2) (2020-10-29) 19 | -------------------------- 20 | 21 | - JSON logging support ([PR 344][]) 22 | - Ability to easily change colors ([\#82][]) 23 | - Allow creating a root logger ([\#342][]) 24 | - Bugfix: file logging with lower loglevel than stream ([PR 338][]) 25 | - Running tests with Python up to 3.9 26 | - Dependency updates 27 | 28 | 1.5.0 (2018-03-07) 29 | ------------------ 30 | 31 | - `logzero.syslog(..)` ([PR 83][]) 32 | 33 | 1.4.0 (2018-03-02) 34 | ------------------ 35 | 36 | - Allow Disabling stderr Output ([PR 83][1]) 37 | 38 | 1.3.0 (2017-07-19) 39 | ------------------ 40 | 41 | - Color output now works in Windows (supported by colorama) 42 | 43 | 1.2.1 (2017-07-09) 44 | ------------------ 45 | 46 | - Logfiles with custom loglevels (eg. stream handler with DEBUG and 47 | file handler with ERROR). 48 | 49 | 1.2.0 (2017-07-05) 50 | ------------------ 51 | 52 | - Way better API for configuring the default logger with logzero.loglevel(..), logzero.logfile(..), etc. 55 | - Built-in rotating logfile support. 56 | 57 | ``` python 58 | import logging 59 | import logzero 60 | from logzero import logger 61 | 62 | # This log message goes to the console 63 | logger.debug("hello") 64 | 65 | # Set a minimum log level 66 | logzero.loglevel(logging.INFO) 67 | 68 | # Set a logfile (all future log messages are also saved there) 69 | logzero.logfile("/tmp/logfile.log") 70 | 71 | # Set a rotating logfile (replaces the previous logfile handler) 72 | logzero.logfile("/tmp/rotating-logfile.log", maxBytes=1000000, backupCount=3) 73 | 74 | # Disable logging to a file 75 | logzero.logfile(None) 76 | 77 | # Set a custom formatter 78 | formatter = logging.Formatter('%(name)s - %(asctime)-15s - %(levelname)s: %(message)s'); 79 | logzero.formatter(formatter) 80 | 81 | # Log some variables 82 | logger.info("var1: %s, var2: %s", var1, var2) 83 | ``` 84 | 85 | 1.1.2 (2017-07-04) 86 | ------------------ 87 | 88 | - Better reconfiguration of handlers, doesn't remove custom handlers 89 | anymore 90 | 91 | 1.1.0 (2017-07-03) 92 | ------------------ 93 | 94 | - Bugfix: Disabled color logging to logfile 95 | 96 | 1.1.0 (2017-07-02) 97 | ------------------ 98 | 99 | - Global default logger instance (logzero.logger) 101 | - Ability to reconfigure the default logger with (logzero.setup\_default\_logger(..)) 103 | - More tests 104 | - More documentation 105 | 106 | 1.0.0 (2017-06-27) 107 | ------------------ 108 | 109 | - Cleanup and documentation 110 | 111 | 0.2.0 (2017-06-12) 112 | ------------------ 113 | 114 | - Working logzero package with code and tests 115 | 116 | 0.1.0 (2017-06-12) 117 | ------------------ 118 | 119 | - First release on PyPI. 120 | 121 | [PR 344]: https://github.com/metachris/logzero/pull/344 122 | [\#82]: https://github.com/metachris/logzero/issues/82 123 | [\#342]: https://github.com/metachris/logzero/pull/342 124 | [PR 338]: https://github.com/metachris/logzero/pull/338 125 | [PR 83]: https://github.com/metachris/logzero/pull/84 126 | [1]: https://github.com/metachris/logzero/pull/83 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2017, Chris Hager 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.rst 2 | include HISTORY.md 3 | include LICENSE 4 | include README.md 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst *.md conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | find . -name '*.pyc' -exec rm -f {} + 41 | find . -name '*.pyo' -exec rm -f {} + 42 | find . -name '*~' -exec rm -f {} + 43 | find . -name '__pycache__' -exec rm -fr {} + 44 | 45 | clean-test: ## remove test and coverage artifacts 46 | rm -fr .tox/ 47 | rm -f .coverage 48 | rm -fr htmlcov/ 49 | 50 | lint: ## check style with flake8 51 | flake8 logzero tests 52 | 53 | test: ## run tests quickly with the default Python 54 | py.test 55 | 56 | 57 | test-all: ## run tests on every Python version with tox 58 | tox 59 | 60 | coverage: ## check code coverage quickly with the default Python 61 | coverage run --source logzero -m pytest 62 | coverage report -m 63 | coverage html 64 | $(BROWSER) htmlcov/index.html 65 | 66 | docs: ## generate Sphinx HTML documentation, including API docs 67 | rm -f docs/logzero.rst 68 | rm -f docs/modules.rst 69 | sphinx-apidoc -o docs/ logzero 70 | $(MAKE) -C docs clean 71 | $(MAKE) -C docs html 72 | $(BROWSER) docs/_build/html/index.html 73 | 74 | servedocs: docs ## compile the docs watching for changes 75 | watchmedo shell-command -p '*.rst;*.md' -c '$(MAKE) -C docs html' -R -D . 76 | 77 | release: clean ## package and upload a release 78 | python setup.py sdist upload 79 | python setup.py bdist_wheel upload 80 | 81 | dist: clean ## builds source and wheel package 82 | python setup.py sdist 83 | python setup.py bdist_wheel 84 | ls -l dist 85 | 86 | install: clean ## install the package to the active Python's site-packages 87 | python setup.py install 88 | -------------------------------------------------------------------------------- /README-various.md: -------------------------------------------------------------------------------- 1 | Future Features & Ideas 2 | ----------------------- 3 | 4 | * Decorator for logging function calls 5 | * Easier usage of custom log handlers (currently works `like this `_) 6 | * Send logs to remote log collector (maybe) 7 | * Structured logging a la https://structlog.readthedocs.io/en/stable/index.html (maybe) 8 | 9 | 10 | Related Projects 11 | ---------------- 12 | 13 | * [pinojs](https://github.com/pinojs/pino) 14 | * https://logbook.readthedocs.io/en/stable/index.html 15 | * https://12factor.net/logs 16 | * Log collectors: fluentd, logstash, etc. 17 | * https://structlog.readthedocs.io/en/stable/why.html 18 | 19 | 20 | Notes: How to release a new version 21 | ----------------------------------- 22 | 23 | via https://cookiecutter-pypackage.readthedocs.io/en/latest/pypi_release_checklist.html 24 | 25 | .. code-block:: console 26 | 27 | # Run the tests 28 | make test 29 | make lint 30 | 31 | # Update history 32 | vi HISTORY.md 33 | git add HISTORY.md 34 | git commit -m "Changelog for upcoming release" 35 | 36 | # Update version 37 | bumpversion minor 38 | 39 | # Push 40 | git push && git push --tags 41 | 42 | Update conda-forge: https://github.com/metachris/logzero/issues/67#issuecomment-353016366 43 | 44 | 45 | Credits 46 | --------- 47 | 48 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 49 | 50 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 51 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 52 | 53 | 54 | .. _pip: https://pip.pypa.io 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logzero 2 | 3 | ![Build status for master branch](https://github.com/metachris/logzero/workflows/Run%20the%20tests/badge.svg) 4 | [![Documentation Status](https://readthedocs.org/projects/logzero/badge/?version=latest)](https://logzero.readthedocs.io/en/latest/?badge=latest) 5 | [![Latest version on PyPi](https://img.shields.io/pypi/v/logzero.svg)](https://pypi.python.org/pypi/logzero) 6 | [![Anaconda-Server Badge](https://anaconda.org/conda-forge/logzero/badges/version.svg)](https://anaconda.org/conda-forge/logzero) 7 | [![Downloads](https://pepy.tech/badge/logzero/week)](https://pepy.tech/project/logzero) 8 | 9 | Robust and effective logging for Python 2 and 3. 10 | 11 | ![Logo](https://raw.githubusercontent.com/metachris/logzero/master/docs/_static/demo-output-with-beaver.png) 12 | 13 | * Documentation: https://logzero.readthedocs.io 14 | * GitHub: https://github.com/metachris/logzero 15 | 16 | 17 | Features 18 | -------- 19 | 20 | * Easy logging to console and/or (rotating) file. 21 | * Provides a fully configured standard [Python logger object](https://docs.python.org/2/library/logging.html#module-level-functions>). 22 | * JSON logging (with integrated [python-json-logger](https://github.com/madzak/python-json-logger)) 23 | * Pretty formatting, including level-specific colors in the console. 24 | * No dependencies 25 | * Windows color output supported by [colorama](https://github.com/tartley/colorama) 26 | * Robust against str/bytes encoding problems, works with all kinds of character encodings and special characters. 27 | * Multiple loggers can write to the same logfile (also across multiple Python files and processes). 28 | * Global default logger with [logzero.logger](https://logzero.readthedocs.io/en/latest/#i-logzero-logger) and custom loggers with [logzero.setup_logger(..)](https://logzero.readthedocs.io/en/latest/#i-logzero-setup-logger). 29 | * Compatible with Python 2 and 3. 30 | * All contained in a [single file](https://github.com/metachris/logzero/blob/master/logzero/__init__.py). 31 | * Licensed under the MIT license. 32 | * Heavily inspired by the [Tornado web framework](https://github.com/tornadoweb/tornado). 33 | 34 | 35 | Installation: 36 | 37 | ```shell 38 | python -m pip install logzero 39 | ``` 40 | 41 | Example Usage 42 | ------------- 43 | 44 | ```python 45 | from logzero import logger 46 | 47 | logger.debug("hello") 48 | logger.info("info") 49 | logger.warning("warn") 50 | logger.error("error") 51 | 52 | # This is how you'd log an exception 53 | try: 54 | raise Exception("this is a demo exception") 55 | except Exception as e: 56 | logger.exception(e) 57 | 58 | # JSON logging 59 | import logzero 60 | logzero.json() 61 | 62 | logger.info("JSON test") 63 | 64 | # Start writing into a logfile 65 | logzero.logfile("/tmp/logzero-demo.log") 66 | 67 | # Set a minimum loglevel 68 | logzero.loglevel(logzero.WARNING) 69 | ``` 70 | 71 | This is the output: 72 | 73 | ![demo-output](https://raw.githubusercontent.com/metachris/logzero/master/docs/_static/demo-output-json.png) 74 | 75 | Note: You can find more examples in the documentation: https://logzero.readthedocs.io 76 | 77 | ### JSON logging 78 | 79 | JSON logging can be enabled for the default logger with `logzero.json()`, or with `setup_logger(json=True)` for custom loggers: 80 | 81 | ```python 82 | >>> logzero.json() 83 | >>> logger.info("test") 84 | {"asctime": "2020-10-21 10:42:45,808", "filename": "", "funcName": "", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "", "message": "test", "name": "logzero_default", "pathname": "", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"} 85 | 86 | >>> my_logger = setup_logger(json=True) 87 | >>> my_logger.info("test") 88 | {"asctime": "2020-10-21 10:42:45,808", "filename": "", "funcName": "", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "", "message": "test", "name": "logzero_default", "pathname": "", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"} 89 | ``` 90 | 91 | The logged JSON object has these fields: 92 | 93 | ```json 94 | { 95 | "asctime": "2020-10-21 10:43:40,765", 96 | "filename": "test.py", 97 | "funcName": "test_this", 98 | "levelname": "INFO", 99 | "levelno": 20, 100 | "lineno": 9, 101 | "module": "test", 102 | "message": "info", 103 | "name": "logzero", 104 | "pathname": "_tests/test.py", 105 | "process": 76204, 106 | "processName": "MainProcess", 107 | "threadName": "MainThread" 108 | } 109 | ``` 110 | 111 | Exceptions logged with `logger.exception(e)` have these additional JSON fields: 112 | 113 | ```json 114 | { 115 | "levelname": "ERROR", 116 | "levelno": 40, 117 | "message": "this is a demo exception", 118 | "exc_info": "Traceback (most recent call last):\n File \"_tests/test.py\", line 15, in test_this\n raise Exception(\"this is a demo exception\")\nException: this is a demo exception" 119 | } 120 | ``` 121 | 122 | Take a look at the documentation for more information and examples: 123 | 124 | * Documentation: https://logzero.readthedocs.io. 125 | 126 | 127 | Installation 128 | ------------ 129 | 130 | Install `logzero` with [pip](https://pip.pypa.io): 131 | 132 | ```shell 133 | python -m pip install logzero 134 | ``` 135 | 136 | Here's how you setup a virtualenv and download and run the demo: 137 | 138 | ```shell 139 | # Create and activate a virtualenv in ./venv/ 140 | python3 -m venv venv 141 | . venv/bin/activate 142 | 143 | # Install logzero 144 | python -m pip install logzero 145 | 146 | # Download and run demo.py 147 | wget https://raw.githubusercontent.com/metachris/logzero/master/examples/demo.py 148 | python demo.py 149 | ``` 150 | 151 | If you don't have [pip](https://pip.pypa.io) installed, this [Python installation guide](http://docs.python-guide.org/en/latest/starting/installation/) can guide 152 | you through the process. 153 | 154 | Alternatively, if you use the [Anaconda distribution](https://www.anaconda.com/download/): 155 | 156 | ```shell 157 | $ conda config --add channels conda-forge 158 | $ conda install logzero 159 | ``` 160 | 161 | You can also install `logzero` from the public [Github repo](https://github.com/metachris/logzero): 162 | 163 | ```shell 164 | $ git clone https://github.com/metachris/logzero.git 165 | $ cd logzero 166 | $ python setup.py install 167 | ``` 168 | 169 | Contributors 170 | ------------ 171 | 172 | * [Chris Hager](https://github.com/metachris) 173 | * [carlodr](https://github.com/carlodri) 174 | * [Brian Lenz](https://github.com/brianlenz) 175 | * [David Martin](https://github.com/dmartin35) 176 | * [Zakaria Zajac](madzak) (creator of [python-json-logger](https://github.com/madzak/python-json-logger)) 177 | 178 | --- 179 | 180 | Development 181 | ----------- 182 | 183 | **Getting started** 184 | 185 | ```shell 186 | $ git clone https://github.com/metachris/logzero.git 187 | $ cd logzero 188 | 189 | # Activate virtualenv 190 | $ python3 -m venv venv 191 | $ . venv/bin/activate 192 | 193 | # Install main and dev dependencies 194 | $ pip install -e . 195 | $ pip install -r requirements_dev.txt 196 | 197 | # Run the tests 198 | $ make test 199 | 200 | # Run the linter 201 | $ make lint 202 | 203 | # Generate the docs (will auto-open in Chrome) 204 | $ make docs 205 | 206 | # You can enable watching mode to automatically rebuild on changes: 207 | $ make servedocs 208 | ``` 209 | 210 | To test with Python 2.7, you can use Docker: 211 | 212 | ```shell 213 | docker run --rm -it -v /Users/chris/stream/logzero:/mnt python:2.7 /bin/bash 214 | ``` 215 | 216 | Now you have a shell with the current directory mounted into `/mnt/` inside the container. 217 | 218 | **Notes** 219 | 220 | * [pytest](https://docs.pytest.org/en/latest/) is the test runner 221 | * CI is run with [Github actions](https://github.com/metachris/logzero/tree/master/.github/workflows). 222 | * Download stats: https://pepy.tech/project/logzero 223 | 224 | --- 225 | 226 | Changelog 227 | --------- 228 | 229 | See the changelog here: https://github.com/metachris/logzero/blob/master/HISTORY.md 230 | 231 | 232 | Feedback 233 | -------- 234 | 235 | All kinds of feedback and contributions are welcome. 236 | 237 | * [Create an issue](https://github.com/metachris/logzero/issues/new) 238 | * Create a pull request 239 | * [@metachris](https://twitter.com/metachris) 240 | 241 | ![logo](https://raw.githubusercontent.com/metachris/logzero/master/docs/_static/logo-420.png) 242 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /logzero.rst 2 | /logzero.*.rst 3 | /modules.rst 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/logzero.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/logzero.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/logzero" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/logzero" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/demo-output-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/demo-output-json.png -------------------------------------------------------------------------------- /docs/_static/demo-output-with-beaver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/demo-output-with-beaver.png -------------------------------------------------------------------------------- /docs/_static/demo_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/demo_output.png -------------------------------------------------------------------------------- /docs/_static/demo_output_with_exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/demo_output_with_exception.png -------------------------------------------------------------------------------- /docs/_static/logo-420.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo-420.png -------------------------------------------------------------------------------- /docs/_static/logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo-big.png -------------------------------------------------------------------------------- /docs/_static/logo-text-wide.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo-text-wide.psd -------------------------------------------------------------------------------- /docs/_static/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo.ai -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/logo_1000x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metachris/logzero/bc19173105561650bfdd0ae22d1bb0d9dcb1be99/docs/_static/logo_1000x500.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # logzero documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another 21 | # directory, add these directories to sys.path here. If the directory is 22 | # relative to the documentation root, use os.path.abspath to make it 23 | # absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # Get the project root dir, which is the parent dir of this 27 | cwd = os.getcwd() 28 | project_root = os.path.dirname(cwd) 29 | 30 | # Insert the project root dir as the first element in the PYTHONPATH. 31 | # This lets us ensure that the source package is imported, and that its 32 | # version is used. 33 | sys.path.insert(0, project_root) 34 | 35 | import logzero 36 | 37 | # -- General configuration --------------------------------------------- 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | #needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 44 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx_rtd_theme'] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix of source filenames. 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | #source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = u'logzero' 60 | copyright = u"2017, Chris Hager" 61 | 62 | # The version info for the project you're documenting, acts as replacement 63 | # for |version| and |release|, also used in various other places throughout 64 | # the built documents. 65 | # 66 | # The short X.Y version. 67 | version = logzero.__version__ 68 | # The full version, including alpha/beta/rc tags. 69 | release = logzero.__version__ 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | #language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to 76 | # some non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ['_build'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built 107 | # documents. 108 | #keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output ------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | # html_theme = 'default' 116 | html_theme = 'sphinx_rtd_theme' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a 119 | # theme further. For a list of options available for each theme, see the 120 | # documentation. 121 | html_theme_options = { 122 | 'collapse_navigation': False, 123 | 'navigation_depth': -1 124 | } 125 | 126 | # Add any paths that contain custom themes here, relative to this directory. 127 | #html_theme_path = [] 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | #html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as 134 | # html_title. 135 | #html_short_title = None 136 | 137 | # The name of an image file (relative to this directory) to place at the 138 | # top of the sidebar. 139 | #html_logo = None 140 | 141 | # The name of an image file (within the static path) to use as favicon 142 | # of the docs. This file should be a Windows icon file (.ico) being 143 | # 16x16 or 32x32 pixels large. 144 | #html_favicon = None 145 | 146 | # Add any paths that contain custom static files (such as style sheets) 147 | # here, relative to this directory. They are copied after the builtin 148 | # static files, so a file named "default.css" will overwrite the builtin 149 | # "default.css". 150 | html_static_path = ['_static'] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page 153 | # bottom, using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names 164 | # to template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. 180 | # Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. 184 | # Default is True. 185 | #html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages 188 | # will contain a tag referring to it. The value of this option 189 | # must be the base URL from which the finished HTML is served. 190 | #html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | #html_file_suffix = None 194 | 195 | # Output file base name for HTML help builder. 196 | htmlhelp_basename = 'logzerodoc' 197 | 198 | 199 | # -- Options for LaTeX output ------------------------------------------ 200 | 201 | latex_elements = { 202 | # The paper size ('letterpaper' or 'a4paper'). 203 | #'papersize': 'letterpaper', 204 | 205 | # The font size ('10pt', '11pt' or '12pt'). 206 | #'pointsize': '10pt', 207 | 208 | # Additional stuff for the LaTeX preamble. 209 | #'preamble': '', 210 | } 211 | 212 | # Grouping the document tree into LaTeX files. List of tuples 213 | # (source start file, target name, title, author, documentclass 214 | # [howto/manual]). 215 | latex_documents = [ 216 | ('index', 'logzero.tex', 217 | u'logzero Documentation', 218 | u'Chris Hager', 'manual'), 219 | ] 220 | 221 | # The name of an image file (relative to this directory) to place at 222 | # the top of the title page. 223 | #latex_logo = None 224 | 225 | # For "manual" documents, if this is true, then toplevel headings 226 | # are parts, not chapters. 227 | #latex_use_parts = False 228 | 229 | # If true, show page references after internal links. 230 | #latex_show_pagerefs = False 231 | 232 | # If true, show URL addresses after external links. 233 | #latex_show_urls = False 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #latex_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #latex_domain_indices = True 240 | 241 | 242 | # -- Options for manual page output ------------------------------------ 243 | 244 | # One entry per manual page. List of tuples 245 | # (source start file, name, description, authors, manual section). 246 | man_pages = [ 247 | ('index', 'logzero', 248 | u'logzero Documentation', 249 | [u'Chris Hager'], 1) 250 | ] 251 | 252 | # If true, show URL addresses after external links. 253 | #man_show_urls = False 254 | 255 | 256 | # -- Options for Texinfo output ---------------------------------------- 257 | 258 | # Grouping the document tree into Texinfo files. List of tuples 259 | # (source start file, target name, title, author, 260 | # dir menu entry, description, category) 261 | texinfo_documents = [ 262 | ('index', 'logzero', 263 | u'logzero Documentation', 264 | u'Chris Hager', 265 | 'logzero', 266 | 'One line description of project.', 267 | 'Miscellaneous'), 268 | ] 269 | 270 | # Documents to append as an appendix to all manuals. 271 | #texinfo_appendices = [] 272 | 273 | # If false, no module index is generated. 274 | #texinfo_domain_indices = True 275 | 276 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 277 | #texinfo_show_urls = 'footnote' 278 | 279 | # If true, do not generate a @detailmenu in the "Top" node's menu. 280 | #texinfo_no_detailmenu = False 281 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | .. _index: 3 | 4 | =================================== 5 | `logzero`: Python logging made easy 6 | =================================== 7 | 8 | Robust and effective logging for Python 2 and 3. 9 | 10 | .. image:: _static/demo-output-with-beaver.png 11 | :alt: Logo 12 | 13 | **Features** 14 | 15 | * Easy logging to console and/or (rotating) file. 16 | * Provides a fully configured `Python logger object `_. 17 | * Pretty formatting, including level-specific colors in the console. 18 | * JSON logging support (with integrated `python-json-logger `_) 19 | * Windows color output supported by `colorama`_ 20 | * Robust against str/bytes encoding problems, works with all kinds of character encodings and special characters. 21 | * Multiple loggers can write to the same logfile (also works across multiple Python files). 22 | * Global default logger with `logzero.logger <#i-logzero-logger>`_ and custom loggers with `logzero.setup_logger(..) <#i-logzero-setup-logger>`_. 23 | * Compatible with Python 2 and 3. 24 | * All contained in a `single file`_. 25 | * Licensed under the MIT license. 26 | * Heavily inspired by the `Tornado web framework`_. 27 | * Hosted on GitHub: https://github.com/metachris/logzero 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | Install `logzero` with `pip`_: 34 | 35 | .. code-block:: console 36 | 37 | $ pip install -U logzero 38 | 39 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 40 | you through the process. 41 | 42 | You can also install `logzero` from the public `Github repo`_: 43 | 44 | .. code-block:: console 45 | 46 | $ git clone https://github.com/metachris/logzero.git 47 | $ cd logzero 48 | $ python setup.py install 49 | 50 | .. _pip: https://pip.pypa.io 51 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 52 | .. _Github repo: https://github.com/metachris/logzero 53 | .. _tarball: https://github.com/metachris/logzero/tarball/master 54 | .. _single file: https://github.com/metachris/logzero/blob/master/logzero/__init__.py 55 | .. _Tornado web framework: https://github.com/tornadoweb/tornado 56 | .. _colorama: https://github.com/tartley/colorama 57 | 58 | 59 | Example usage 60 | ============= 61 | 62 | You can use `logzero` like this (logs only to the console by default): 63 | 64 | .. code-block:: python 65 | 66 | from logzero import logger 67 | 68 | logger.debug("hello") 69 | logger.info("info") 70 | logger.warning("warn") 71 | logger.error("error") 72 | 73 | # This is how you'd log an exception 74 | try: 75 | raise Exception("this is a demo exception") 76 | except Exception as e: 77 | logger.exception(e) 78 | 79 | # JSON logging 80 | import logzero 81 | logzero.json() 82 | 83 | logger.info("JSON test") 84 | 85 | # Start writing into a logfile 86 | logzero.logfile("/tmp/logzero-demo.log") 87 | 88 | If this was a file called ``demo.py``, the output will look like this: 89 | 90 | .. image:: _static/demo-output-json.png 91 | :alt: Demo output in color 92 | 93 | Logging to files 94 | ---------------- 95 | 96 | You can add logging to a (rotating) logfile like this: 97 | 98 | .. code-block:: python 99 | 100 | import logzero 101 | from logzero import logger 102 | 103 | # non-rotating logfile 104 | logzero.logfile("/tmp/logfile.log") 105 | 106 | # rotating logfile 107 | logzero.logfile("/tmp/rotating-logfile.log", maxBytes=1e6, backupCount=3) 108 | 109 | # log messages 110 | logger.info("This log message goes to the console and the logfile") 111 | 112 | 113 | JSON logging 114 | ------------ 115 | 116 | JSON logging can be enabled for the default logger with `logzero.json()`, or with `setup_logger(json=True)` for custom loggers: 117 | 118 | .. code-block:: python 119 | 120 | # Configure the default logger to output JSON 121 | >>> logzero.json() 122 | >>> logger.info("test") 123 | {"asctime": "2020-10-21 10:42:45,808", "filename": "", "funcName": "", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "", "message": "test", "name": "logzero_default", "pathname": "", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"} 124 | 125 | # Configure a custom logger to output JSON 126 | >>> my_logger = setup_logger(json=True) 127 | >>> my_logger.info("test") 128 | {"asctime": "2020-10-21 10:42:45,808", "filename": "", "funcName": "", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "", "message": "test", "name": "logzero_default", "pathname": "", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"} 129 | 130 | The logged JSON object has these fields: 131 | 132 | .. code-block:: json 133 | 134 | { 135 | "asctime": "2020-10-21 10:43:40,765", 136 | "filename": "test.py", 137 | "funcName": "test_this", 138 | "levelname": "INFO", 139 | "levelno": 20, 140 | "lineno": 9, 141 | "module": "test", 142 | "message": "info", 143 | "name": "logzero", 144 | "pathname": "_tests/test.py", 145 | "process": 76204, 146 | "processName": "MainProcess", 147 | "threadName": "MainThread" 148 | } 149 | 150 | An exception logged with `logger.exception(e)` has these: 151 | 152 | .. code-block:: json 153 | 154 | { 155 | "asctime": "2020-10-21 10:43:25,193", 156 | "filename": "test.py", 157 | "funcName": "test_this", 158 | "levelname": "ERROR", 159 | "levelno": 40, 160 | "lineno": 17, 161 | "module": "test", 162 | "message": "this is a demo exception", 163 | "name": "logzero", 164 | "pathname": "_tests/test.py", 165 | "process": 76192, 166 | "processName": "MainProcess", 167 | "threadName": "MainThread", 168 | "exc_info": "Traceback (most recent call last):\n File \"_tests/test.py\", line 15, in test_this\n raise Exception(\"this is a demo exception\")\nException: this is a demo exception" 169 | } 170 | 171 | 172 | Advanced usage examples 173 | ----------------------- 174 | 175 | Here are more examples which show how to use logfiles, custom formatters 176 | and setting a minimum loglevel. 177 | 178 | +-----------------------------------------+--------------------------------------------------+ 179 | | Outcome | Method | 180 | +=========================================+==================================================+ 181 | | Set a minimum log level | `logzero.loglevel(..) <#i-logzero-loglevel>`_ | 182 | +-----------------------------------------+--------------------------------------------------+ 183 | | Add logging to a logfile | `logzero.logfile(..) <#i-logzero-logfile>`_ | 184 | +-----------------------------------------+--------------------------------------------------+ 185 | | Setup a rotating logfile | `logzero.logfile(..) <#i-logzero-logfile>`_ | 186 | +-----------------------------------------+--------------------------------------------------+ 187 | | Disable logging to a logfile | `logzero.logfile(None) <#i-logzero-logfile>`_ | 188 | +-----------------------------------------+--------------------------------------------------+ 189 | | JSON logging | `logzero.json(...) <#json-logging>`_ | 190 | +-----------------------------------------+--------------------------------------------------+ 191 | | Log to syslog | `logzero.syslog(...) <#i-logzero-logfile>`_ | 192 | +-----------------------------------------+--------------------------------------------------+ 193 | | Use a custom formatter | `logzero.formatter(..) <#i-logzero-formatter>`_ | 194 | +-----------------------------------------+--------------------------------------------------+ 195 | 196 | 197 | .. code-block:: python 198 | 199 | import logging 200 | import logzero 201 | from logzero import logger 202 | 203 | # This log message goes to the console 204 | logger.debug("hello") 205 | 206 | # Set a minimum log level 207 | logzero.loglevel(logzero.INFO) 208 | 209 | # Set a logfile (all future log messages are also saved there) 210 | logzero.logfile("/tmp/logfile.log") 211 | 212 | # Set a logfile (all future log messages are also saved there), but disable the default stderr logging 213 | logzero.logfile("/tmp/logfile.log", disableStderrLogger=True) 214 | 215 | # You can also set a different loglevel for the file handler 216 | logzero.logfile("/tmp/logfile.log", loglevel=logzero.ERROR) 217 | 218 | # Set a rotating logfile (replaces the previous logfile handler) 219 | logzero.logfile("/tmp/rotating-logfile.log", maxBytes=1000000, backupCount=3) 220 | 221 | # Disable logging to a file 222 | logzero.logfile(None) 223 | 224 | # Enable JSON log format 225 | logzero.json() 226 | 227 | # Disable JSON log format 228 | logzero.json(False) 229 | 230 | # Log to syslog, using default logzero logger and 'user' syslog facility 231 | logzero.syslog() 232 | 233 | # Log to syslog, using default logzero logger and 'local0' syslog facility 234 | logzero.syslog(facility=SysLogHandler.LOG_LOCAL0) 235 | 236 | # Set a custom formatter 237 | formatter = logging.Formatter('%(name)s - %(asctime)-15s - %(levelname)s: %(message)s'); 238 | logzero.formatter(formatter) 239 | 240 | # Log some variables 241 | logger.info("var1: %s, var2: %s", var1, var2) 242 | 243 | Custom logger instances 244 | ----------------------- 245 | 246 | Instead of using the default logger you can also setup specific logger instances with `logzero.setup_logger(..) <#i-logzero-setup-logger>`_: 247 | 248 | .. code-block:: python 249 | 250 | from logzero import setup_logger 251 | logger1 = setup_logger(name="mylogger1") 252 | logger2 = setup_logger(name="mylogger2", logfile="/tmp/test-logger2.log", level=logzero.INFO) 253 | logger3 = setup_logger(name="mylogger3", logfile="/tmp/test-logger3.log", level=logzero.INFO, disableStderrLogger=True) 254 | 255 | # Log something: 256 | logger1.info("info for logger 1") 257 | logger2.info("info for logger 2") 258 | 259 | # log to a file only, excluding the default stderr logger 260 | logger3.info("info for logger 3") 261 | 262 | # JSON logging in a custom logger 263 | jsonLogger = setup_logger(name="jsonLogger", json=True) 264 | jsonLogger.info("info in json") 265 | 266 | 267 | 268 | Adding custom handlers (eg. SocketHandler) 269 | ------------------------------------------ 270 | 271 | Since `logzero` uses the standard `Python logger object `_, 272 | you can attach any `Python logging handlers `_ you can imagine! 273 | 274 | This is how you add a `SocketHandler `_: 275 | 276 | .. code-block:: python 277 | 278 | import logzero 279 | import logging 280 | from logging.handlers import SocketHandler 281 | 282 | # Setup the SocketHandler 283 | socket_handler = SocketHandler(address=('localhost', logging.DEFAULT_TCP_LOGGING_PORT)) 284 | socket_handler.setLevel(logzero.DEBUG) 285 | socket_handler.setFormatter(logzero.LogFormatter(color=False)) 286 | 287 | # Attach it to the logzero default logger 288 | logzero.logger.addHandler(socket_handler) 289 | 290 | # Log messages 291 | logzero.logger.info("this is a test") 292 | 293 | 294 | Documentation 295 | ============= 296 | 297 | .. _i-logzero-logger: 298 | 299 | `logzero.logger` 300 | ---------------- 301 | 302 | `logzero.logger` is an already set up standard `Python logger instance `_ for your convenience. You can use it from all your 303 | files and modules directly like this: 304 | 305 | .. code-block:: python 306 | 307 | from logzero import logger 308 | 309 | logger.debug("hello") 310 | logger.info("info") 311 | logger.warning("warning") 312 | logger.error("error") 313 | 314 | You can reconfigure the default logger globally with `logzero.setup_default_logger(..) <#i-logzero-setup-default-logger>`_. 315 | 316 | See the documentation for the `Python logger instance `_ for more information about how you can use it. 317 | 318 | 319 | .. _i-logzero-loglevel: 320 | 321 | `logzero.loglevel(..)` 322 | -------------------------- 323 | 324 | .. autofunction:: logzero.loglevel 325 | 326 | 327 | .. _i-logzero-logfile: 328 | 329 | `logzero.logfile(..)` 330 | -------------------------- 331 | 332 | .. autofunction:: logzero.logfile 333 | 334 | 335 | .. _i-logzero-formatter: 336 | 337 | `logzero.formatter(..)` 338 | -------------------------- 339 | 340 | .. autofunction:: logzero.formatter 341 | 342 | 343 | .. _i-logzero-setup-logger: 344 | 345 | `logzero.setup_logger(..)` 346 | -------------------------- 347 | 348 | .. autofunction:: logzero.setup_logger 349 | 350 | .. _i-logzero-setup-default-logger: 351 | 352 | 353 | Default Log Format 354 | ------------------ 355 | 356 | This is the default log format string: 357 | 358 | .. code-block:: python 359 | 360 | DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s' 361 | 362 | See also the `Python LogRecord attributes `_ you can use. 363 | 364 | 365 | Custom Formatting 366 | ----------------- 367 | 368 | It is easy to use a custom formatter / a custom log format string: 369 | 370 | * Define your log format string (you can use any of the `LogRecord attributes `_). 371 | * Create a `Formatter object `_ (based on `logzero.LogFormatter` to get all the encoding helpers). 372 | * Supply the formatter object to the `formatter` argument in the `setup_logger(..)` method. 373 | 374 | This is a working example on how to setup logging with a custom format: 375 | 376 | .. code-block:: python 377 | 378 | import logzero 379 | 380 | log_format = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s' 381 | formatter = logzero.LogFormatter(fmt=log_format) 382 | logzero.setup_default_logger(formatter=formatter) 383 | 384 | 385 | Issues, Feedback & Contributions 386 | ================================ 387 | 388 | All kinds of feedback and contributions are welcome. 389 | 390 | * `Create an issue `_ 391 | * Create a pull request 392 | * https://github.com/metachris/logzero 393 | * chris@linuxuser.at // `@metachris `_ 394 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\logzero.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\logzero.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | # Import the `logzero.logger` instance 2 | from logzero import logger 3 | 4 | # Start logging 5 | logger.debug("hello") 6 | logger.info("info") 7 | logger.warning("warn") 8 | logger.error("error") 9 | 10 | # Log exceptions 11 | try: 12 | raise Exception("this is a demo exception") 13 | except Exception as e: 14 | logger.exception(e) 15 | 16 | # JSON logging 17 | print("\n\nHere starts JSON logging...\n") 18 | import logzero 19 | 20 | logzero.json() 21 | logger.info("JSON test") 22 | 23 | # Logfile (check with `cat /tmp/logzero-demo.log`) 24 | logzero.logfile("/tmp/logzero-demo.log") 25 | logger.info("going into logfile, in JSON format") 26 | 27 | # Disable JSON 28 | logzero.json(False) 29 | logger.info("going into logfile, with standard formatter") 30 | -------------------------------------------------------------------------------- /examples/demo2.py: -------------------------------------------------------------------------------- 1 | # Import the `logzero.logger` instance 2 | import logzero 3 | from logzero import logger 4 | 5 | logzero.loglevel(logzero.WARNING) 6 | 7 | # Start logging 8 | logger.debug("hello") 9 | logger.info("info") 10 | logger.warning("warn") 11 | logger.error("error") 12 | -------------------------------------------------------------------------------- /logzero/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This helper provides a versatile yet easy to use and beautiful logging setup. 4 | You can use it to log to the console and optionally to a logfile. This project 5 | is heavily inspired by the Tornado web framework. 6 | 7 | * https://logzero.readthedocs.io 8 | * https://github.com/metachris/logzero 9 | 10 | The call `logger.info("hello")` prints log messages in this format: 11 | 12 | [I 170213 15:02:00 test:203] hello 13 | 14 | Usage: 15 | 16 | from logzero import logger 17 | 18 | logger.debug("hello") 19 | logger.info("info") 20 | logger.warning("warn") 21 | logger.error("error") 22 | 23 | In order to also log to a file, just use `logzero.logfile(..)`: 24 | 25 | logzero.logfile("/tmp/test.log") 26 | 27 | If you want to use specific loggers instead of the global default logger, use 28 | `setup_logger(..)`: 29 | 30 | logger = logzero.setup_logger(logfile="/tmp/test.log") 31 | 32 | The default loglevel is `DEBUG`. You can set it with the 33 | parameter `level`. 34 | 35 | See the documentation for more information: https://logzero.readthedocs.io 36 | """ 37 | import functools 38 | import os 39 | import sys 40 | import logging 41 | from logzero.colors import Fore as ForegroundColors 42 | from logzero.jsonlogger import JsonFormatter 43 | 44 | from logging.handlers import RotatingFileHandler, SysLogHandler 45 | from logging import CRITICAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET # noqa: F401 46 | 47 | try: 48 | import curses # type: ignore 49 | except ImportError: 50 | curses = None 51 | 52 | __author__ = """Chris Hager""" 53 | __email__ = 'chris@linuxuser.at' 54 | __version__ = '1.7.0' 55 | 56 | # Python 2+3 compatibility settings for logger 57 | bytes_type = bytes 58 | if sys.version_info >= (3, ): 59 | unicode_type = str 60 | basestring_type = str 61 | xrange = range 62 | else: 63 | # The names unicode and basestring don't exist in py3 so silence flake8. 64 | unicode_type = unicode # noqa 65 | basestring_type = basestring # noqa 66 | 67 | # Formatter defaults 68 | DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s' 69 | DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S' 70 | DEFAULT_COLORS = { 71 | DEBUG: ForegroundColors.CYAN, 72 | INFO: ForegroundColors.GREEN, 73 | WARNING: ForegroundColors.YELLOW, 74 | ERROR: ForegroundColors.RED, 75 | CRITICAL: ForegroundColors.RED 76 | } 77 | 78 | # Name of the internal default logger 79 | LOGZERO_DEFAULT_LOGGER = "logzero_default" 80 | 81 | # Attribute which all internal loggers carry 82 | LOGZERO_INTERNAL_LOGGER_ATTR = "_is_logzero_internal" 83 | 84 | # Attribute signalling whether the handler has a custom loglevel 85 | LOGZERO_INTERNAL_HANDLER_IS_CUSTOM_LOGLEVEL = "_is_logzero_internal_handler_custom_loglevel" 86 | 87 | # Logzero default logger 88 | logger = None 89 | 90 | # Current state of the internal logging settings 91 | _loglevel = DEBUG 92 | _logfile = None 93 | _formatter = None 94 | 95 | # Setup colorama on Windows 96 | if os.name == 'nt': 97 | from colorama import init as colorama_init 98 | colorama_init() 99 | 100 | 101 | def setup_logger(name=__name__, logfile=None, level=DEBUG, formatter=None, maxBytes=0, backupCount=0, fileLoglevel=None, disableStderrLogger=False, isRootLogger=False, json=False, json_ensure_ascii=False): 102 | """ 103 | Configures and returns a fully configured logger instance, no hassles. 104 | If a logger with the specified name already exists, it returns the existing instance, 105 | else creates a new one. 106 | 107 | If you set the ``logfile`` parameter with a filename, the logger will save the messages to the logfile, 108 | but does not rotate by default. If you want to enable log rotation, set both ``maxBytes`` and ``backupCount``. 109 | 110 | Usage: 111 | 112 | .. code-block:: python 113 | 114 | from logzero import setup_logger 115 | logger = setup_logger() 116 | logger.info("hello") 117 | 118 | :arg string name: Name of the `Logger object `_. Multiple calls to ``setup_logger()`` with the same name will always return a reference to the same Logger object. (default: ``__name__``) 119 | :arg string logfile: If set, also write logs to the specified filename. 120 | :arg int level: Minimum `logging-level `_ to display (default: ``DEBUG``). 121 | :arg Formatter formatter: `Python logging Formatter object `_ (by default uses the internal LogFormatter). 122 | :arg int maxBytes: Size of the logfile when rollover should occur. Defaults to 0, rollover never occurs. 123 | :arg int backupCount: Number of backups to keep. Defaults to 0, rollover never occurs. 124 | :arg int fileLoglevel: Minimum `logging-level `_ for the file logger (is not set, it will use the loglevel from the ``level`` argument) 125 | :arg bool disableStderrLogger: Should the default stderr logger be disabled. Defaults to False. 126 | :arg bool isRootLogger: If True then returns a root logger. Defaults to False. (see also the `Python docs `_). 127 | :arg bool json: If True then log in JSON format. Defaults to False. (uses `python-json-logger `_). 128 | :arg bool json_ensure_ascii: Passed to json.dumps as `ensure_ascii`, default: False (if False: writes utf-8 characters, if True: ascii only representation of special characters - eg. '\u00d6\u00df') 129 | :return: A fully configured Python logging `Logger object `_ you can use with ``.debug("msg")``, etc. 130 | """ 131 | _logger = logging.getLogger(None if isRootLogger else name) 132 | _logger.propagate = False 133 | 134 | # set the minimum level needed for the logger itself (the lowest handler level) 135 | minLevel = fileLoglevel if fileLoglevel and fileLoglevel < level else level 136 | _logger.setLevel(minLevel) 137 | 138 | # Setup default formatter 139 | _formatter = _get_json_formatter(json_ensure_ascii) if json else formatter or LogFormatter() 140 | 141 | # Reconfigure existing handlers 142 | stderr_stream_handler = None 143 | for handler in list(_logger.handlers): 144 | if hasattr(handler, LOGZERO_INTERNAL_LOGGER_ATTR): 145 | if isinstance(handler, logging.FileHandler): 146 | # Internal FileHandler needs to be removed and re-setup to be able 147 | # to set a new logfile. 148 | _logger.removeHandler(handler) 149 | continue 150 | elif isinstance(handler, logging.StreamHandler): 151 | stderr_stream_handler = handler 152 | 153 | # reconfigure handler 154 | handler.setLevel(level) 155 | handler.setFormatter(_formatter) 156 | 157 | # remove the stderr handler (stream_handler) if disabled 158 | if disableStderrLogger: 159 | if stderr_stream_handler is not None: 160 | _logger.removeHandler(stderr_stream_handler) 161 | elif stderr_stream_handler is None: 162 | stderr_stream_handler = logging.StreamHandler() 163 | setattr(stderr_stream_handler, LOGZERO_INTERNAL_LOGGER_ATTR, True) 164 | stderr_stream_handler.setLevel(level) 165 | stderr_stream_handler.setFormatter(_formatter) 166 | _logger.addHandler(stderr_stream_handler) 167 | 168 | if logfile: 169 | rotating_filehandler = RotatingFileHandler(filename=logfile, maxBytes=maxBytes, backupCount=backupCount) 170 | setattr(rotating_filehandler, LOGZERO_INTERNAL_LOGGER_ATTR, True) 171 | rotating_filehandler.setLevel(fileLoglevel or level) 172 | rotating_filehandler.setFormatter(_formatter) 173 | _logger.addHandler(rotating_filehandler) 174 | 175 | return _logger 176 | 177 | 178 | class LogFormatter(logging.Formatter): 179 | """ 180 | Log formatter used in Tornado. Key features of this formatter are: 181 | * Color support when logging to a terminal that supports it. 182 | * Timestamps on every log line. 183 | * Robust against str/bytes encoding problems. 184 | """ 185 | def __init__(self, 186 | color=True, 187 | fmt=DEFAULT_FORMAT, 188 | datefmt=DEFAULT_DATE_FORMAT, 189 | colors=DEFAULT_COLORS): 190 | r""" 191 | :arg bool color: Enables color support. 192 | :arg string fmt: Log message format. 193 | It will be applied to the attributes dict of log records. The 194 | text between ``%(color)s`` and ``%(end_color)s`` will be colored 195 | depending on the level if color support is on. 196 | :arg dict colors: color mappings from logging level to terminal color 197 | code 198 | :arg string datefmt: Datetime format. 199 | Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``. 200 | .. versionchanged:: 3.2 201 | Added ``fmt`` and ``datefmt`` arguments. 202 | """ 203 | logging.Formatter.__init__(self, datefmt=datefmt) 204 | 205 | self._fmt = fmt 206 | self._colors = {} 207 | self._normal = '' 208 | 209 | if color and _stderr_supports_color(): 210 | self._colors = colors 211 | self._normal = ForegroundColors.RESET 212 | 213 | def format(self, record): 214 | try: 215 | message = record.getMessage() 216 | assert isinstance(message, 217 | basestring_type) # guaranteed by logging 218 | # Encoding notes: The logging module prefers to work with character 219 | # strings, but only enforces that log messages are instances of 220 | # basestring. In python 2, non-ascii bytestrings will make 221 | # their way through the logging framework until they blow up with 222 | # an unhelpful decoding error (with this formatter it happens 223 | # when we attach the prefix, but there are other opportunities for 224 | # exceptions further along in the framework). 225 | # 226 | # If a byte string makes it this far, convert it to unicode to 227 | # ensure it will make it out to the logs. Use repr() as a fallback 228 | # to ensure that all byte strings can be converted successfully, 229 | # but don't do it by default so we don't add extra quotes to ascii 230 | # bytestrings. This is a bit of a hacky place to do this, but 231 | # it's worth it since the encoding errors that would otherwise 232 | # result are so useless (and tornado is fond of using utf8-encoded 233 | # byte strings wherever possible). 234 | record.message = _safe_unicode(message) 235 | except Exception as e: 236 | record.message = "Bad message (%r): %r" % (e, record.__dict__) 237 | 238 | record.asctime = self.formatTime(record, self.datefmt) 239 | 240 | if record.levelno in self._colors: 241 | record.color = self._colors[record.levelno] 242 | record.end_color = self._normal 243 | else: 244 | record.color = record.end_color = '' 245 | 246 | formatted = self._fmt % record.__dict__ 247 | 248 | if record.exc_info: 249 | if not record.exc_text: 250 | record.exc_text = self.formatException(record.exc_info) 251 | if record.exc_text: 252 | # exc_text contains multiple lines. We need to _safe_unicode 253 | # each line separately so that non-utf8 bytes don't cause 254 | # all the newlines to turn into '\n'. 255 | lines = [formatted.rstrip()] 256 | lines.extend( 257 | _safe_unicode(ln) for ln in record.exc_text.split('\n')) 258 | formatted = '\n'.join(lines) 259 | return formatted.replace("\n", "\n ") 260 | 261 | 262 | def _stderr_supports_color(): 263 | # Colors can be forced with an env variable 264 | if os.getenv('LOGZERO_FORCE_COLOR') == '1': 265 | return True 266 | 267 | # Windows supports colors with colorama 268 | if os.name == 'nt': 269 | return True 270 | 271 | # Detect color support of stderr with curses (Linux/macOS) 272 | if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): 273 | try: 274 | curses.setupterm() 275 | if curses.tigetnum("colors") > 0: 276 | return True 277 | 278 | except Exception: 279 | pass 280 | 281 | return False 282 | 283 | 284 | _TO_UNICODE_TYPES = (unicode_type, type(None)) 285 | 286 | 287 | def to_unicode(value): 288 | """ 289 | Converts a string argument to a unicode string. 290 | If the argument is already a unicode string or None, it is returned 291 | unchanged. Otherwise it must be a byte string and is decoded as utf8. 292 | """ 293 | if isinstance(value, _TO_UNICODE_TYPES): 294 | return value 295 | if not isinstance(value, bytes): 296 | raise TypeError( 297 | "Expected bytes, unicode, or None; got %r" % type(value)) 298 | return value.decode("utf-8") 299 | 300 | 301 | def _safe_unicode(s): 302 | try: 303 | return to_unicode(s) 304 | except UnicodeDecodeError: 305 | return repr(s) 306 | 307 | 308 | def setup_default_logger(logfile=None, level=DEBUG, formatter=None, maxBytes=0, backupCount=0, disableStderrLogger=False): 309 | """ 310 | Deprecated. Use `logzero.loglevel(..)`, `logzero.logfile(..)`, etc. 311 | 312 | Globally reconfigures the default `logzero.logger` instance. 313 | 314 | Usage: 315 | 316 | .. code-block:: python 317 | 318 | from logzero import logger, setup_default_logger 319 | setup_default_logger(level=WARN) 320 | logger.info("hello") # this will not be displayed anymore because minimum loglevel was set to WARN 321 | 322 | :arg string logfile: If set, also write logs to the specified filename. 323 | :arg int level: Minimum `logging-level `_ to display (default: `DEBUG`). 324 | :arg Formatter formatter: `Python logging Formatter object `_ (by default uses the internal LogFormatter). 325 | :arg int maxBytes: Size of the logfile when rollover should occur. Defaults to 0, rollover never occurs. 326 | :arg int backupCount: Number of backups to keep. Defaults to 0, rollover never occurs. 327 | :arg bool disableStderrLogger: Should the default stderr logger be disabled. Defaults to False. 328 | """ 329 | global logger 330 | logger = setup_logger(name=LOGZERO_DEFAULT_LOGGER, logfile=logfile, level=level, formatter=formatter, backupCount=backupCount, disableStderrLogger=disableStderrLogger) 331 | return logger 332 | 333 | 334 | def reset_default_logger(): 335 | """ 336 | Resets the internal default logger to the initial configuration 337 | """ 338 | global logger 339 | global _loglevel 340 | global _logfile 341 | global _formatter 342 | _loglevel = DEBUG 343 | _logfile = None 344 | _formatter = None 345 | 346 | # Remove all handlers on exiting logger 347 | if logger: 348 | for handler in list(logger.handlers): 349 | logger.removeHandler(handler) 350 | 351 | # Resetup 352 | logger = setup_logger(name=LOGZERO_DEFAULT_LOGGER, logfile=_logfile, level=_loglevel, formatter=_formatter) 353 | 354 | 355 | # Initially setup the default logger 356 | reset_default_logger() 357 | 358 | 359 | def loglevel(level=DEBUG, update_custom_handlers=False): 360 | """ 361 | Set the minimum loglevel for the default logger (`logzero.logger`) and all handlers. 362 | 363 | This reconfigures only the internal handlers of the default logger (eg. stream and logfile). 364 | You can also update the loglevel for custom handlers by using `update_custom_handlers=True`. 365 | 366 | :arg int level: Minimum `logging-level `_ to display (default: `DEBUG`). 367 | :arg bool update_custom_handlers: If you added custom handlers to this logger and want this to update them too, you need to set `update_custom_handlers` to `True` 368 | """ 369 | logger.setLevel(level) 370 | 371 | # Reconfigure existing internal handlers 372 | for handler in list(logger.handlers): 373 | if hasattr(handler, LOGZERO_INTERNAL_LOGGER_ATTR) or update_custom_handlers: 374 | # Don't update the loglevel if this handler uses a custom one 375 | if hasattr(handler, LOGZERO_INTERNAL_HANDLER_IS_CUSTOM_LOGLEVEL): 376 | continue 377 | 378 | # Update the loglevel for all default handlers 379 | handler.setLevel(level) 380 | 381 | global _loglevel 382 | _loglevel = level 383 | 384 | 385 | def formatter(formatter, update_custom_handlers=False): 386 | """ 387 | Set the formatter for all handlers of the default logger (``logzero.logger``). 388 | 389 | This reconfigures only the logzero internal handlers by default, but you can also 390 | reconfigure custom handlers by using ``update_custom_handlers=True``. 391 | 392 | Beware that setting a formatter which uses colors also may write the color codes 393 | to logfiles. 394 | 395 | :arg Formatter formatter: `Python logging Formatter object `_ (by default uses the internal LogFormatter). 396 | :arg bool update_custom_handlers: If you added custom handlers to this logger and want this to update them too, you need to set ``update_custom_handlers`` to `True` 397 | """ 398 | for handler in list(logger.handlers): 399 | if hasattr(handler, LOGZERO_INTERNAL_LOGGER_ATTR) or update_custom_handlers: 400 | handler.setFormatter(formatter) 401 | 402 | global _formatter 403 | _formatter = formatter 404 | 405 | 406 | def logfile(filename, formatter=None, mode='a', maxBytes=0, backupCount=0, encoding=None, loglevel=None, disableStderrLogger=False): 407 | """ 408 | Setup logging to file (using a `RotatingFileHandler `_ internally). 409 | 410 | By default, the file grows indefinitely (no rotation). You can use the ``maxBytes`` and 411 | ``backupCount`` values to allow the file to rollover at a predetermined size. When the 412 | size is about to be exceeded, the file is closed and a new file is silently opened 413 | for output. Rollover occurs whenever the current log file is nearly ``maxBytes`` in length; 414 | if either of ``maxBytes`` or ``backupCount`` is zero, rollover never occurs. 415 | 416 | If ``backupCount`` is non-zero, the system will save old log files by appending the 417 | extensions ‘.1’, ‘.2’ etc., to the filename. For example, with a ``backupCount`` of 5 418 | and a base file name of app.log, you would get app.log, app.log.1, app.log.2, up to 419 | app.log.5. The file being written to is always app.log. When this file is filled, 420 | it is closed and renamed to app.log.1, and if files app.log.1, app.log.2, etc. exist, 421 | then they are renamed to app.log.2, app.log.3 etc. respectively. 422 | 423 | :arg string filename: Filename of the logfile. Set to `None` to disable logging to the logfile. 424 | :arg Formatter formatter: `Python logging Formatter object `_ (by default uses the internal LogFormatter). 425 | :arg string mode: mode to open the file with. Defaults to ``a`` 426 | :arg int maxBytes: Size of the logfile when rollover should occur. Defaults to 0, rollover never occurs. 427 | :arg int backupCount: Number of backups to keep. Defaults to 0, rollover never occurs. 428 | :arg string encoding: Used to open the file with that encoding. 429 | :arg int loglevel: Set a custom loglevel for the file logger, else uses the current global loglevel. 430 | :arg bool disableStderrLogger: Should the default stderr logger be disabled. Defaults to False. 431 | """ 432 | # First, remove any existing file logger 433 | __remove_internal_loggers(logger, disableStderrLogger) 434 | 435 | # If no filename supplied, all is done 436 | if not filename: 437 | return 438 | 439 | # Now add 440 | rotating_filehandler = RotatingFileHandler(filename, mode=mode, maxBytes=maxBytes, backupCount=backupCount, encoding=encoding) 441 | 442 | # Set internal attributes on this handler 443 | setattr(rotating_filehandler, LOGZERO_INTERNAL_LOGGER_ATTR, True) 444 | if loglevel: 445 | setattr(rotating_filehandler, LOGZERO_INTERNAL_HANDLER_IS_CUSTOM_LOGLEVEL, True) 446 | 447 | # Configure the handler and add it to the logger 448 | rotating_filehandler.setLevel(loglevel or _loglevel) 449 | rotating_filehandler.setFormatter(formatter or _formatter or LogFormatter(color=False)) 450 | logger.addHandler(rotating_filehandler) 451 | 452 | # If wanting to use a lower loglevel for the file handler, we need to reconfigure the logger level 453 | # (note: this won't change the StreamHandler loglevel) 454 | if loglevel and loglevel < logger.level: 455 | logger.setLevel(loglevel) 456 | 457 | 458 | def __remove_internal_loggers(logger_to_update, disableStderrLogger=True): 459 | """ 460 | Remove the internal loggers (e.g. stderr logger and file logger) from the specific logger 461 | :param logger_to_update: the logger to remove internal loggers from 462 | :param disableStderrLogger: should the default stderr logger be disabled? defaults to True 463 | """ 464 | for handler in list(logger_to_update.handlers): 465 | if hasattr(handler, LOGZERO_INTERNAL_LOGGER_ATTR): 466 | if isinstance(handler, RotatingFileHandler): 467 | logger_to_update.removeHandler(handler) 468 | elif isinstance(handler, SysLogHandler): 469 | logger_to_update.removeHandler(handler) 470 | elif isinstance(handler, logging.StreamHandler) and disableStderrLogger: 471 | logger_to_update.removeHandler(handler) 472 | 473 | 474 | def syslog(logger_to_update=logger, facility=SysLogHandler.LOG_USER, disableStderrLogger=True): 475 | """ 476 | Setup logging to syslog and disable other internal loggers 477 | :param logger_to_update: the logger to enable syslog logging for 478 | :param facility: syslog facility to log to 479 | :param disableStderrLogger: should the default stderr logger be disabled? defaults to True 480 | :return the new SysLogHandler, which can be modified externally (e.g. for custom log level) 481 | """ 482 | # remove internal loggers 483 | __remove_internal_loggers(logger_to_update, disableStderrLogger) 484 | 485 | # Setup logzero to only use the syslog handler with the specified facility 486 | syslog_handler = SysLogHandler(facility=facility) 487 | setattr(syslog_handler, LOGZERO_INTERNAL_LOGGER_ATTR, True) 488 | logger_to_update.addHandler(syslog_handler) 489 | return syslog_handler 490 | 491 | 492 | def json(enable=True, json_ensure_ascii=False, update_custom_handlers=False): 493 | """ 494 | Enable/disable json logging for all handlers. 495 | 496 | Params: 497 | * json_ensure_ascii ... Passed to json.dumps as `ensure_ascii`, default: False (if False: writes utf-8 characters, if True: ascii only representation of special characters - eg. '\u00d6\u00df') 498 | """ 499 | 500 | formatter(_get_json_formatter(json_ensure_ascii) if enable else LogFormatter(), update_custom_handlers=update_custom_handlers) 501 | 502 | 503 | def _get_json_formatter(json_ensure_ascii): 504 | supported_keys = [ 505 | 'asctime', 506 | 'filename', 507 | 'funcName', 508 | 'levelname', 509 | 'levelno', 510 | 'lineno', 511 | 'module', 512 | 'message', 513 | 'name', 514 | 'pathname', 515 | 'process', 516 | 'processName', 517 | 'threadName' 518 | ] 519 | 520 | def log_format(x): 521 | return ['%({0:s})s'.format(i) for i in x] 522 | custom_format = ' '.join(log_format(supported_keys)) 523 | return JsonFormatter(custom_format, json_ensure_ascii=json_ensure_ascii) 524 | 525 | 526 | def log_function_call(func): 527 | @functools.wraps(func) 528 | def wrap(*args, **kwargs): 529 | args_str = ", ".join([str(arg) for arg in args]) 530 | kwargs_str = ", ".join(["%s=%s" % (key, kwargs[key]) for key in kwargs]) 531 | if args_str and kwargs_str: 532 | all_args_str = ", ".join([args_str, kwargs_str]) 533 | else: 534 | all_args_str = args_str or kwargs_str 535 | logger.debug("%s(%s)", func.__name__, all_args_str) 536 | return func(*args, **kwargs) 537 | return wrap 538 | 539 | 540 | if __name__ == "__main__": 541 | _logger = setup_logger() 542 | _logger.info("hello") 543 | -------------------------------------------------------------------------------- /logzero/colors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Source: https://github.com/tartley/colorama/blob/master/colorama/ansi.py 3 | Copyright: Jonathan Hartley 2013. BSD 3-Clause license. 4 | """ 5 | 6 | CSI = '\033[' 7 | OSC = '\033]' 8 | BEL = '\007' 9 | 10 | 11 | def code_to_chars(code): 12 | return CSI + str(code) + 'm' 13 | 14 | 15 | def set_title(title): 16 | return OSC + '2;' + title + BEL 17 | 18 | 19 | def clear_screen(mode=2): 20 | return CSI + str(mode) + 'J' 21 | 22 | 23 | def clear_line(mode=2): 24 | return CSI + str(mode) + 'K' 25 | 26 | 27 | class AnsiCodes(object): 28 | def __init__(self): 29 | # the subclasses declare class attributes which are numbers. 30 | # Upon instantiation we define instance attributes, which are the same 31 | # as the class attributes but wrapped with the ANSI escape sequence 32 | for name in dir(self): 33 | if not name.startswith('_'): 34 | value = getattr(self, name) 35 | setattr(self, name, code_to_chars(value)) 36 | 37 | 38 | class AnsiCursor(object): 39 | def UP(self, n=1): 40 | return CSI + str(n) + 'A' 41 | 42 | def DOWN(self, n=1): 43 | return CSI + str(n) + 'B' 44 | 45 | def FORWARD(self, n=1): 46 | return CSI + str(n) + 'C' 47 | 48 | def BACK(self, n=1): 49 | return CSI + str(n) + 'D' 50 | 51 | def POS(self, x=1, y=1): 52 | return CSI + str(y) + ';' + str(x) + 'H' 53 | 54 | 55 | class AnsiFore(AnsiCodes): 56 | BLACK = 30 57 | RED = 31 58 | GREEN = 32 59 | YELLOW = 33 60 | BLUE = 34 61 | MAGENTA = 35 62 | CYAN = 36 63 | WHITE = 37 64 | RESET = 39 65 | 66 | # These are fairly well supported, but not part of the standard. 67 | LIGHTBLACK_EX = 90 68 | LIGHTRED_EX = 91 69 | LIGHTGREEN_EX = 92 70 | LIGHTYELLOW_EX = 93 71 | LIGHTBLUE_EX = 94 72 | LIGHTMAGENTA_EX = 95 73 | LIGHTCYAN_EX = 96 74 | LIGHTWHITE_EX = 97 75 | 76 | 77 | class AnsiBack(AnsiCodes): 78 | BLACK = 40 79 | RED = 41 80 | GREEN = 42 81 | YELLOW = 43 82 | BLUE = 44 83 | MAGENTA = 45 84 | CYAN = 46 85 | WHITE = 47 86 | RESET = 49 87 | 88 | # These are fairly well supported, but not part of the standard. 89 | LIGHTBLACK_EX = 100 90 | LIGHTRED_EX = 101 91 | LIGHTGREEN_EX = 102 92 | LIGHTYELLOW_EX = 103 93 | LIGHTBLUE_EX = 104 94 | LIGHTMAGENTA_EX = 105 95 | LIGHTCYAN_EX = 106 96 | LIGHTWHITE_EX = 107 97 | 98 | 99 | class AnsiStyle(AnsiCodes): 100 | BRIGHT = 1 101 | DIM = 2 102 | NORMAL = 22 103 | RESET_ALL = 0 104 | 105 | 106 | Fore = AnsiFore() 107 | Back = AnsiBack() 108 | Style = AnsiStyle() 109 | Cursor = AnsiCursor() 110 | -------------------------------------------------------------------------------- /logzero/jsonlogger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | https://github.com/madzak/python-json-logger 3 | 4 | This library is provided to allow standard python logging 5 | to output log data as JSON formatted strings 6 | ''' 7 | import sys 8 | import logging 9 | import json 10 | import re 11 | import traceback 12 | import importlib 13 | from inspect import istraceback 14 | from collections import OrderedDict 15 | from datetime import date, datetime, time 16 | 17 | if sys.version_info >= (3, ): 18 | from datetime import timezone 19 | tz = timezone.utc 20 | else: 21 | tz = None 22 | 23 | 24 | # skip natural LogRecord attributes 25 | # http://docs.python.org/library/logging.html#logrecord-attributes 26 | RESERVED_ATTRS = ( 27 | 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', 28 | 'funcName', 'levelname', 'levelno', 'lineno', 'module', 29 | 'msecs', 'message', 'msg', 'name', 'pathname', 'process', 30 | 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName') 31 | 32 | 33 | def merge_record_extra(record, target, reserved): 34 | """ 35 | Merges extra attributes from LogRecord object into target dictionary 36 | 37 | :param record: logging.LogRecord 38 | :param target: dict to update 39 | :param reserved: dict or list with reserved keys to skip 40 | """ 41 | for key, value in record.__dict__.items(): 42 | # this allows to have numeric keys 43 | if (key not in reserved and not (hasattr(key, "startswith") and key.startswith('_'))): 44 | target[key] = value 45 | return target 46 | 47 | 48 | class JsonEncoder(json.JSONEncoder): 49 | """ 50 | A custom encoder extending the default JSONEncoder 51 | """ 52 | 53 | def default(self, obj): 54 | if isinstance(obj, (date, datetime, time)): 55 | return self.format_datetime_obj(obj) 56 | 57 | elif istraceback(obj): 58 | return ''.join(traceback.format_tb(obj)).strip() 59 | 60 | elif type(obj) == Exception \ 61 | or isinstance(obj, Exception) \ 62 | or type(obj) == type: 63 | return str(obj) 64 | 65 | try: 66 | return super(JsonEncoder, self).default(obj) 67 | 68 | except TypeError: 69 | try: 70 | return str(obj) 71 | 72 | except Exception: 73 | return None 74 | 75 | def format_datetime_obj(self, obj): 76 | return obj.isoformat() 77 | 78 | 79 | class JsonFormatter(logging.Formatter): 80 | """ 81 | A custom formatter to format logging records as json strings. 82 | Extra values will be formatted as str() if not supported by 83 | json default encoder 84 | """ 85 | 86 | def __init__(self, *args, **kwargs): 87 | """ 88 | :param json_default: a function for encoding non-standard objects 89 | as outlined in http://docs.python.org/2/library/json.html 90 | :param json_encoder: optional custom encoder 91 | :param json_serializer: a :meth:`json.dumps`-compatible callable 92 | that will be used to serialize the log record. 93 | :param json_indent: an optional :meth:`json.dumps`-compatible numeric value 94 | that will be used to customize the indent of the output json. 95 | :param prefix: an optional string prefix added at the beginning of 96 | the formatted string 97 | :param rename_fields: an optional dict, used to rename field names in the output. 98 | Rename message to @message: {'message': '@message'} 99 | :param json_indent: indent parameter for json.dumps 100 | :param json_ensure_ascii: ensure_ascii parameter for json.dumps 101 | :param reserved_attrs: an optional list of fields that will be skipped when 102 | outputting json log record. Defaults to all log record attributes: 103 | http://docs.python.org/library/logging.html#logrecord-attributes 104 | :param timestamp: an optional string/boolean field to add a timestamp when 105 | outputting the json log record. If string is passed, timestamp will be added 106 | to log record using string as key. If True boolean is passed, timestamp key 107 | will be "timestamp". Defaults to False/off. 108 | """ 109 | self.json_default = self._str_to_fn(kwargs.pop("json_default", None)) 110 | self.json_encoder = self._str_to_fn(kwargs.pop("json_encoder", None)) 111 | self.json_serializer = self._str_to_fn(kwargs.pop("json_serializer", json.dumps)) 112 | self.json_indent = kwargs.pop("json_indent", None) 113 | self.json_ensure_ascii = kwargs.pop("json_ensure_ascii", True) 114 | self.prefix = kwargs.pop("prefix", "") 115 | self.rename_fields = kwargs.pop("rename_fields", {}) 116 | reserved_attrs = kwargs.pop("reserved_attrs", RESERVED_ATTRS) 117 | self.reserved_attrs = dict(zip(reserved_attrs, reserved_attrs)) 118 | self.timestamp = kwargs.pop("timestamp", False) 119 | 120 | # super(JsonFormatter, self).__init__(*args, **kwargs) 121 | logging.Formatter.__init__(self, *args, **kwargs) 122 | if not self.json_encoder and not self.json_default: 123 | self.json_encoder = JsonEncoder 124 | 125 | self._required_fields = self.parse() 126 | self._skip_fields = dict(zip(self._required_fields, 127 | self._required_fields)) 128 | self._skip_fields.update(self.reserved_attrs) 129 | 130 | def _str_to_fn(self, fn_as_str): 131 | """ 132 | If the argument is not a string, return whatever was passed in. 133 | Parses a string such as package.module.function, imports the module 134 | and returns the function. 135 | 136 | :param fn_as_str: The string to parse. If not a string, return it. 137 | """ 138 | if not isinstance(fn_as_str, str): 139 | return fn_as_str 140 | 141 | path, _, function = fn_as_str.rpartition('.') 142 | module = importlib.import_module(path) 143 | return getattr(module, function) 144 | 145 | def parse(self): 146 | """ 147 | Parses format string looking for substitutions 148 | 149 | This method is responsible for returning a list of fields (as strings) 150 | to include in all log messages. 151 | """ 152 | standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE) 153 | return standard_formatters.findall(self._fmt) 154 | 155 | def add_fields(self, log_record, record, message_dict): 156 | """ 157 | Override this method to implement custom logic for adding fields. 158 | """ 159 | for field in self._required_fields: 160 | if field in self.rename_fields: 161 | log_record[self.rename_fields[field]] = record.__dict__.get(field) 162 | else: 163 | log_record[field] = record.__dict__.get(field) 164 | log_record.update(message_dict) 165 | merge_record_extra(record, log_record, reserved=self._skip_fields) 166 | 167 | if self.timestamp: 168 | key = self.timestamp if type(self.timestamp) == str else 'timestamp' 169 | log_record[key] = datetime.fromtimestamp(record.created, tz=tz) 170 | 171 | def process_log_record(self, log_record): 172 | """ 173 | Override this method to implement custom logic 174 | on the possibly ordered dictionary. 175 | """ 176 | return log_record 177 | 178 | def jsonify_log_record(self, log_record): 179 | """Returns a json string of the log record.""" 180 | return self.json_serializer(log_record, 181 | default=self.json_default, 182 | cls=self.json_encoder, 183 | indent=self.json_indent, 184 | ensure_ascii=self.json_ensure_ascii) 185 | 186 | def serialize_log_record(self, log_record): 187 | """Returns the final representation of the log record.""" 188 | return "%s%s" % (self.prefix, self.jsonify_log_record(log_record)) 189 | 190 | def format(self, record): 191 | """Formats a log record and serializes to json""" 192 | message_dict = {} 193 | if isinstance(record.msg, dict): 194 | message_dict = record.msg 195 | record.message = None 196 | else: 197 | record.message = record.getMessage() 198 | # only format time if needed 199 | if "asctime" in self._required_fields: 200 | record.asctime = self.formatTime(record, self.datefmt) 201 | 202 | # Display formatted exception, but allow overriding it in the 203 | # user-supplied dict. 204 | if record.exc_info and not message_dict.get('exc_info'): 205 | message_dict['exc_info'] = self.formatException(record.exc_info) 206 | if not message_dict.get('exc_info') and record.exc_text: 207 | message_dict['exc_info'] = record.exc_text 208 | # Display formatted record of stack frames 209 | # default format is a string returned from :func:`traceback.print_stack` 210 | try: 211 | if record.stack_info and not message_dict.get('stack_info'): 212 | message_dict['stack_info'] = self.formatStack(record.stack_info) 213 | except AttributeError: 214 | # Python2.7 doesn't have stack_info. 215 | pass 216 | 217 | try: 218 | log_record = OrderedDict() 219 | except NameError: 220 | log_record = {} 221 | 222 | self.add_fields(log_record, record, message_dict) 223 | log_record = self.process_log_record(log_record) 224 | 225 | return self.serialize_log_record(log_record) 226 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest==4.6 # pyup: ignore 2 | pytest-runner==5.2 3 | bumpversion==0.6.0 4 | watchdog==0.10.3 5 | flake8==3.8.4 6 | coverage==5.3 7 | cryptography==3.4.6 8 | Sphinx==3.3.1 9 | sphinx-rtd-theme==0.5.0 10 | -------------------------------------------------------------------------------- /requirements_windows.txt: -------------------------------------------------------------------------------- 1 | colorama==0.4.4 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.7.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:logzero/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | ignore = E501 20 | 21 | [aliases] 22 | test = pytest 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | with open('README.md') as readme_file: 9 | readme = readme_file.read() 10 | 11 | with open('HISTORY.md') as history_file: 12 | history = history_file.read() 13 | 14 | setup( 15 | name='logzero', 16 | version='1.7.0', 17 | description="Robust and effective logging for Python 2 and 3", 18 | long_description=readme + '\n\n' + history, 19 | long_description_content_type='text/markdown', 20 | author="Chris Hager", 21 | author_email='chris@linuxuser.at', 22 | url='https://github.com/metachris/logzero', 23 | packages=find_packages(include=['logzero']), 24 | include_package_data=True, 25 | license="MIT license", 26 | zip_safe=False, 27 | keywords='logzero', 28 | classifiers=[ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Natural Language :: English', 33 | "Programming Language :: Python :: 2", 34 | 'Programming Language :: Python :: 2.6', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Programming Language :: Python :: 3.8', 42 | 'Programming Language :: Python :: 3.9', 43 | ], 44 | extras_require={ 45 | ':sys_platform=="win32"': ['colorama'] 46 | } 47 | ) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for logzero.""" 4 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | test json related things 3 | """ 4 | import json 5 | import tempfile 6 | import logzero 7 | 8 | 9 | def _test_json_obj_content(obj): 10 | # Check that all fields are contained 11 | attrs = ['asctime', 'filename', 'funcName', 'levelname', 'levelno', 'lineno', 'module', 'message', 'name', 'pathname', 'process', 'processName', 'threadName'] 12 | assert obj["message"] == "info" 13 | for attr in attrs: 14 | if attr not in obj: 15 | raise Exception("obj missing key '%s'" % attr) 16 | 17 | 18 | def test_json(capsys): 19 | """ 20 | Test json logging 21 | """ 22 | # Test setup_logger 23 | logger = logzero.setup_logger(json=True) 24 | logger.info('info') 25 | out, err = capsys.readouterr() 26 | _test_json_obj_content(json.loads(err)) 27 | 28 | 29 | def test_json_default_logger(capsys): 30 | # Test default logger 31 | logzero.reset_default_logger() 32 | logzero.logger.info('info') 33 | out, err = capsys.readouterr() 34 | assert "] info" in err 35 | 36 | logzero.json() 37 | logzero.logger.info('info') 38 | out, err = capsys.readouterr() 39 | _test_json_obj_content(json.loads(err)) 40 | 41 | logzero.json(False) 42 | logzero.logger.info('info') 43 | out, err = capsys.readouterr() 44 | assert "] info" in err 45 | 46 | 47 | def test_json_logfile(capsys): 48 | # Test default logger 49 | logzero.reset_default_logger() 50 | temp = tempfile.NamedTemporaryFile() 51 | try: 52 | logger = logzero.setup_logger(logfile=temp.name, json=True) 53 | logger.info('info') 54 | 55 | with open(temp.name) as f: 56 | content = f.read() 57 | _test_json_obj_content(json.loads(content)) 58 | 59 | finally: 60 | temp.close() 61 | 62 | 63 | def test_json_encoding(capsys): 64 | """ 65 | see logzero.json(json_ensure_ascii=True) 66 | """ 67 | logzero.reset_default_logger() 68 | 69 | # UTF-8 mode 70 | logzero.json(json_ensure_ascii=False) 71 | logzero.logger.info('ß') 72 | out, err = capsys.readouterr() 73 | json.loads(err) # make sure JSON is valid 74 | assert 'ß' in err 75 | assert 'u00df' not in err 76 | 77 | # ASCII mode 78 | logzero.json(json_ensure_ascii=True) 79 | logzero.logger.info('ß') 80 | out, err = capsys.readouterr() 81 | json.loads(err) # make sure JSON is valid 82 | assert 'u00df' in err 83 | assert 'ß' not in err 84 | 85 | # Default JSON mode should be utf-8 86 | logzero.json() 87 | logzero.logger.info('ß') 88 | out, err = capsys.readouterr() 89 | json.loads(err) # make sure JSON is valid 90 | assert 'ß' in err 91 | assert 'u00df' not in err 92 | -------------------------------------------------------------------------------- /tests/test_logzero.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | test_logzero 5 | ---------------------------------- 6 | 7 | Tests for `logzero` module. 8 | """ 9 | import os 10 | import tempfile 11 | import logging 12 | 13 | import logzero 14 | 15 | 16 | def test_write_to_logfile_and_stderr(capsys): 17 | """ 18 | When using `logfile=`, should by default log to a file and stderr. 19 | """ 20 | logzero.reset_default_logger() 21 | temp = tempfile.NamedTemporaryFile() 22 | 23 | try: 24 | logger = logzero.setup_logger('test_write_to_logfile_and_stderr', logfile=temp.name) 25 | logger.info("test log output") 26 | 27 | _out, err = capsys.readouterr() 28 | assert " test_logzero:" in err 29 | assert err.endswith("test log output\n") 30 | 31 | with open(temp.name) as f: 32 | content = f.read() 33 | assert " test_logzero:" in content 34 | assert content.endswith("test log output\n") 35 | finally: 36 | temp.close() 37 | 38 | 39 | def test_custom_formatter(): 40 | """ 41 | Should work with a custom formatter. 42 | """ 43 | logzero.reset_default_logger() 44 | temp = tempfile.NamedTemporaryFile() 45 | try: 46 | log_format = '%(color)s[%(levelname)1.1s %(asctime)s customnametest:%(lineno)d]%(end_color)s %(message)s' 47 | formatter = logzero.LogFormatter(fmt=log_format) 48 | logger = logzero.setup_logger(logfile=temp.name, formatter=formatter) 49 | logger.info("test log output") 50 | 51 | with open(temp.name) as f: 52 | content = f.read() 53 | assert " customnametest:" in content 54 | assert content.endswith("test log output\n") 55 | 56 | finally: 57 | temp.close() 58 | 59 | 60 | def test_loglevel(): 61 | """ 62 | Should not log any debug messages if minimum level is set to INFO 63 | """ 64 | logzero.reset_default_logger() 65 | temp = tempfile.NamedTemporaryFile() 66 | try: 67 | logger = logzero.setup_logger(logfile=temp.name, level=logzero.INFO) 68 | logger.debug("test log output") 69 | 70 | with open(temp.name) as f: 71 | content = f.read() 72 | assert len(content.strip()) == 0 73 | 74 | finally: 75 | temp.close() 76 | 77 | 78 | def test_bytes(): 79 | """ 80 | Should properly log bytes 81 | """ 82 | logzero.reset_default_logger() 83 | temp = tempfile.NamedTemporaryFile() 84 | try: 85 | logger = logzero.setup_logger(logfile=temp.name) 86 | 87 | testbytes = os.urandom(20) 88 | logger.debug(testbytes) 89 | logger.debug(None) 90 | 91 | # with open(temp.name) as f: 92 | # content = f.read() 93 | # # assert str(testbytes) in content 94 | 95 | finally: 96 | temp.close() 97 | 98 | 99 | def test_unicode(): 100 | """ 101 | Should log unicode 102 | """ 103 | logzero.reset_default_logger() 104 | temp = tempfile.NamedTemporaryFile() 105 | try: 106 | logger = logzero.setup_logger(logfile=temp.name) 107 | 108 | logger.debug("😄 😁 😆 😅 😂") 109 | 110 | with open(temp.name, "rb") as f: 111 | content = f.read() 112 | assert "\\xf0\\x9f\\x98\\x84 \\xf0\\x9f\\x98\\x81 \\xf0\\x9f\\x98\\x86 \\xf0\\x9f\\x98\\x85 \\xf0\\x9f\\x98\\x82\\n" in repr(content) 113 | 114 | finally: 115 | temp.close() 116 | 117 | 118 | def test_multiple_loggers_one_logfile(): 119 | """ 120 | Should properly log bytes 121 | """ 122 | logzero.reset_default_logger() 123 | temp = tempfile.NamedTemporaryFile() 124 | try: 125 | logger1 = logzero.setup_logger(name="logger1", logfile=temp.name) 126 | logger2 = logzero.setup_logger(name="logger2", logfile=temp.name) 127 | logger3 = logzero.setup_logger(name="logger3", logfile=temp.name) 128 | 129 | logger1.info("logger1") 130 | logger2.info("logger2") 131 | logger3.info("logger3") 132 | 133 | with open(temp.name) as f: 134 | content = f.read().strip() 135 | assert "logger1" in content 136 | assert "logger2" in content 137 | assert "logger3" in content 138 | assert len(content.split("\n")) == 3 139 | 140 | finally: 141 | temp.close() 142 | 143 | 144 | def test_default_logger(disableStdErrorLogger=False): 145 | """ 146 | Default logger should work and be able to be reconfigured. 147 | """ 148 | logzero.reset_default_logger() 149 | temp = tempfile.NamedTemporaryFile() 150 | try: 151 | logzero.setup_default_logger(logfile=temp.name, disableStderrLogger=disableStdErrorLogger) 152 | logzero.logger.debug("debug1") # will be logged 153 | 154 | # Reconfigure with loglevel INFO 155 | logzero.setup_default_logger(logfile=temp.name, level=logzero.INFO, disableStderrLogger=disableStdErrorLogger) 156 | logzero.logger.debug("debug2") # will not be logged 157 | logzero.logger.info("info1") # will be logged 158 | 159 | # Reconfigure with a different formatter 160 | log_format = '%(color)s[xxx]%(end_color)s %(message)s' 161 | formatter = logzero.LogFormatter(fmt=log_format) 162 | logzero.setup_default_logger(logfile=temp.name, level=logzero.INFO, formatter=formatter, disableStderrLogger=disableStdErrorLogger) 163 | 164 | logzero.logger.info("info2") # will be logged with new formatter 165 | logzero.logger.debug("debug3") # will not be logged 166 | 167 | with open(temp.name) as f: 168 | content = f.read() 169 | _test_default_logger_output(content) 170 | 171 | finally: 172 | temp.close() 173 | 174 | 175 | def _test_default_logger_output(content): 176 | assert "] debug1" in content 177 | assert "] debug2" not in content 178 | assert "] info1" in content 179 | assert "xxx] info2" in content 180 | assert "] debug3" not in content 181 | 182 | 183 | def test_setup_logger_reconfiguration(): 184 | """ 185 | Should be able to reconfigure without loosing custom handlers 186 | """ 187 | logzero.reset_default_logger() 188 | temp = tempfile.NamedTemporaryFile() 189 | temp2 = tempfile.NamedTemporaryFile() 190 | try: 191 | logzero.setup_default_logger(logfile=temp.name) 192 | 193 | # Add a custom file handler 194 | filehandler = logging.FileHandler(temp2.name) 195 | filehandler.setLevel(logzero.DEBUG) 196 | filehandler.setFormatter(logzero.LogFormatter(color=False)) 197 | logzero.logger.addHandler(filehandler) 198 | 199 | # First debug message goes to both files 200 | logzero.logger.debug("debug1") 201 | 202 | # Reconfigure logger to remove logfile 203 | logzero.setup_default_logger() 204 | logzero.logger.debug("debug2") 205 | 206 | # Reconfigure logger to add logfile 207 | logzero.setup_default_logger(logfile=temp.name) 208 | logzero.logger.debug("debug3") 209 | 210 | # Reconfigure logger to set minimum loglevel to INFO 211 | logzero.setup_default_logger(logfile=temp.name, level=logzero.INFO) 212 | logzero.logger.debug("debug4") 213 | logzero.logger.info("info1") 214 | 215 | # Reconfigure logger to set minimum loglevel back to DEBUG 216 | logzero.setup_default_logger(logfile=temp.name, level=logzero.DEBUG) 217 | logzero.logger.debug("debug5") 218 | 219 | with open(temp.name) as f: 220 | content = f.read() 221 | assert "] debug1" in content 222 | assert "] debug2" not in content 223 | assert "] debug3" in content 224 | assert "] debug4" not in content 225 | assert "] info1" in content 226 | assert "] debug5" in content 227 | 228 | with open(temp2.name) as f: 229 | content = f.read() 230 | assert "] debug1" in content 231 | assert "] debug2" in content 232 | assert "] debug3" in content 233 | assert "] debug4" not in content 234 | assert "] info1" in content 235 | assert "] debug5" in content 236 | 237 | finally: 238 | temp.close() 239 | 240 | 241 | def test_setup_logger_logfile_custom_loglevel(capsys): 242 | """ 243 | setup_logger(..) with filelogger and custom loglevel 244 | """ 245 | logzero.reset_default_logger() 246 | temp = tempfile.NamedTemporaryFile() 247 | try: 248 | logger = logzero.setup_logger(logfile=temp.name, fileLoglevel=logzero.WARN) 249 | logger.info("info1") 250 | logger.warning("warn1") 251 | 252 | with open(temp.name) as f: 253 | content = f.read() 254 | assert "] info1" not in content 255 | assert "] warn1" in content 256 | 257 | finally: 258 | temp.close() 259 | 260 | 261 | def test_log_function_call(): 262 | @logzero.log_function_call 263 | def example(): 264 | """example doc""" 265 | pass 266 | 267 | assert example.__name__ == "example" 268 | assert example.__doc__ == "example doc" 269 | 270 | 271 | def test_default_logger_logfile_only(capsys): 272 | """ 273 | Run the ``test_default_logger`` with ``disableStdErrorLogger`` set to ``True`` and 274 | confirm that no data is written to stderr 275 | """ 276 | test_default_logger(disableStdErrorLogger=True) 277 | out, err = capsys.readouterr() 278 | assert err == '' 279 | 280 | 281 | def test_default_logger_stderr_output(capsys): 282 | """ 283 | Run the ``test_default_logger`` and confirm that the proper data is written to stderr 284 | """ 285 | test_default_logger() 286 | out, err = capsys.readouterr() 287 | _test_default_logger_output(err) 288 | 289 | 290 | def test_default_logger_syslog_only(capsys): 291 | """ 292 | Run a test logging to ``syslog`` and confirm that no data is written to stderr. 293 | Note that the output in syslog is not currently being captured or checked. 294 | """ 295 | logzero.reset_default_logger() 296 | logzero.syslog() 297 | logzero.logger.error('debug') 298 | out, err = capsys.readouterr() 299 | assert out == '' and err == '' 300 | 301 | 302 | def test_logfile_lower_loglevel(capsys): 303 | """ 304 | logzero.logfile(..) should work with a lower loglevel than the StreamHandler 305 | """ 306 | logzero.reset_default_logger() 307 | temp = tempfile.NamedTemporaryFile() 308 | try: 309 | logzero.loglevel(level=logzero.INFO) 310 | logzero.logfile(temp.name, loglevel=logzero.DEBUG) 311 | 312 | logzero.logger.debug("debug") 313 | logzero.logger.info("info") 314 | 315 | with open(temp.name) as f: 316 | content = f.read() 317 | assert "] debug" in content 318 | assert "] info" in content 319 | 320 | finally: 321 | temp.close() 322 | 323 | 324 | def test_logfile_lower_loglevel_setup_logger(capsys): 325 | """ 326 | logzero.setup_logger(..) should work with a lower loglevel than the StreamHandler 327 | """ 328 | temp = tempfile.NamedTemporaryFile() 329 | try: 330 | logger = logzero.setup_logger(level=logzero.INFO, logfile=temp.name, fileLoglevel=logzero.DEBUG) 331 | logger.debug("debug") 332 | logger.info("info") 333 | with open(temp.name) as f: 334 | content = f.read() 335 | assert "] debug" in content 336 | assert "] info" in content 337 | finally: 338 | temp.close() 339 | 340 | 341 | def test_root_logger(capsys): 342 | """ 343 | Test creating a root logger 344 | """ 345 | logzero.reset_default_logger() 346 | logger1 = logzero.setup_logger() 347 | assert logger1.name == 'logzero' 348 | 349 | logger2 = logzero.setup_logger(isRootLogger=True) 350 | assert logger2.name == 'root' 351 | 352 | logger3 = logzero.setup_logger(name='') 353 | assert logger3.name == 'root' 354 | -------------------------------------------------------------------------------- /tests/test_new_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | test_logzero 5 | ---------------------------------- 6 | 7 | Tests for `logzero` module. 8 | """ 9 | import os 10 | import tempfile 11 | import logzero 12 | 13 | 14 | def test_api_logfile(capsys): 15 | """ 16 | logzero.logfile(..) should work as expected 17 | """ 18 | logzero.reset_default_logger() 19 | temp = tempfile.NamedTemporaryFile() 20 | try: 21 | logzero.logger.info("info1") 22 | 23 | # Set logfile 24 | logzero.logfile(temp.name) 25 | logzero.logger.info("info2") 26 | 27 | # Remove logfile 28 | logzero.logfile(None) 29 | logzero.logger.info("info3") 30 | 31 | # Set logfile again 32 | logzero.logfile(temp.name) 33 | logzero.logger.info("info4") 34 | 35 | with open(temp.name) as f: 36 | content = f.read() 37 | assert "] info1" not in content 38 | assert "] info2" in content 39 | assert "] info3" not in content 40 | assert "] info4" in content 41 | 42 | finally: 43 | temp.close() 44 | 45 | 46 | def test_api_loglevel(capsys): 47 | """ 48 | Should reconfigure the internal logger loglevel 49 | """ 50 | logzero.reset_default_logger() 51 | temp = tempfile.NamedTemporaryFile() 52 | try: 53 | logzero.logfile(temp.name) 54 | logzero.logger.info("info1") 55 | logzero.loglevel(logzero.WARN) 56 | logzero.logger.info("info2") 57 | logzero.logger.warning("warn1") 58 | 59 | with open(temp.name) as f: 60 | content = f.read() 61 | assert "] info1" in content 62 | assert "] info2" not in content 63 | assert "] warn1" in content 64 | 65 | finally: 66 | temp.close() 67 | 68 | 69 | def test_api_loglevel_custom_handlers(capsys): 70 | """ 71 | Should reconfigure the internal logger loglevel and custom handlers 72 | """ 73 | logzero.reset_default_logger() 74 | # TODO 75 | pass 76 | # temp = tempfile.NamedTemporaryFile() 77 | # try: 78 | # logzero.logfile(temp.name) 79 | # logzero.logger.info("info1") 80 | # logzero.loglevel(logzero.WARN) 81 | # logzero.logger.info("info2") 82 | # logzero.logger.warning("warn1") 83 | 84 | # with open(temp.name) as f: 85 | # content = f.read() 86 | # assert "] info1" in content 87 | # assert "] info2" not in content 88 | # assert "] warn1" in content 89 | 90 | # finally: 91 | # temp.close() 92 | 93 | 94 | def test_api_rotating_logfile(capsys): 95 | """ 96 | logzero.rotating_logfile(..) should work as expected 97 | """ 98 | logzero.reset_default_logger() 99 | temp = tempfile.NamedTemporaryFile() 100 | try: 101 | logzero.logger.info("info1") 102 | 103 | # Set logfile 104 | logzero.logfile(temp.name, maxBytes=10, backupCount=3) 105 | logzero.logger.info("info2") 106 | logzero.logger.info("info3") 107 | 108 | with open(temp.name) as f: 109 | content = f.read() 110 | assert "] info1" not in content # logged before setting up logfile 111 | assert "] info2" not in content # already rotated out 112 | assert "] info3" in content # already rotated out 113 | 114 | fn_rotated = temp.name + ".1" 115 | assert os.path.exists(fn_rotated) 116 | with open(fn_rotated) as f: 117 | content = f.read() 118 | assert "] info2" in content 119 | 120 | finally: 121 | temp.close() 122 | 123 | 124 | def test_api_logfile_custom_loglevel(): 125 | """ 126 | logzero.logfile(..) should be able to use a custom loglevel 127 | """ 128 | logzero.reset_default_logger() 129 | temp = tempfile.NamedTemporaryFile() 130 | try: 131 | # Set logfile with custom loglevel 132 | logzero.logfile(temp.name, loglevel=logzero.WARN) 133 | logzero.logger.info("info1") 134 | logzero.logger.warning("warn1") 135 | 136 | # If setting a loglevel with logzero.loglevel(..) it will not overwrite 137 | # the custom loglevel of the file handler 138 | logzero.loglevel(logzero.INFO) 139 | logzero.logger.info("info2") 140 | logzero.logger.warning("warn2") 141 | 142 | with open(temp.name) as f: 143 | content = f.read() 144 | assert "] info1" not in content 145 | assert "] warn1" in content 146 | assert "] info2" not in content 147 | assert "] warn2" in content 148 | 149 | finally: 150 | temp.close() 151 | --------------------------------------------------------------------------------