log-rate-limit


Namelog-rate-limit JSON
Version 1.4.1 PyPI version JSON
download
home_pagehttps://github.com/samuller/log-rate-limit
SummaryLimit excessive log output with Python's standard logging framework.
upload_time2023-11-02 19:44:18
maintainer
docs_urlNone
authorSimon Muller
requires_python>=3.8.1,<4.0.0
license
keywords logging log limit rate
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # log-rate-limit - limit excessive log output

[![PyPI Version](https://img.shields.io/pypi/v/log-rate-limit)](https://pypi.org/project/log-rate-limit/)
[![Build Status](https://github.com/samuller/log-rate-limit/actions/workflows/tests.yml/badge.svg)](https://github.com/samuller/log-rate-limit/actions/workflows/tests.yml)
[![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/samuller/pgmerge/actions)
[![Checked with mypy](https://img.shields.io/badge/mypy-strict-blue)](http://mypy-lang.org/)
[![Formatted with black](https://img.shields.io/badge/code%20style-black-black)](https://black.readthedocs.io/en/stable/)
[![Code Complexity](https://img.shields.io/badge/max--complexity-%3C10-blue)](https://flake8.pycqa.org/en/6.1.0/user/options.html#cmdoption-flake8-max-complexity)

A [logging filter](https://docs.python.org/3/library/logging.html#filter-objects) using Python's standard logging mechanisms to rate-limit logs - i.e. suppress logs when they are being output too fast.

Log commands are grouped into separate **streams** that will each have their own rate limitation applied without affecting the logs in other streams. By default every log message is assigned a unique stream so that only repeated log messages will be suppressed.

However, logs can also be assigned streams manually to achieve various outcomes:
- A log can be assigned to an undefined/`None` stream so that rate-limiting doesn't apply to it.
- Logs in different parts of the code can be grouped into the same stream so that they share a rate-limit, e.g. when they all trigger due to the same issue and only some are needed to indicate it.

The default can also be changed so that rate-limiting is disabled by default and only applies to logs for which a `stream_id` has been manually set.

## Quick usage

Import the filter:

```python
from log_rate_limit import StreamRateLimitFilter, RateLimit
```

Use the filter with your `logger` - with default parameters it will rate-limit all repetitive log messages:

```python
logger.addFilter(StreamRateLimitFilter(period_sec=30))
```

All logs on `logger` will now be rate-limited, but this can be disabled per-log by setting the `stream_id` to `None`:

```python
logger.warning("Something went wrong!", extra=RateLimit(stream_id=None))
```

If you have a log message that's continually changing (e.g. contains a timestamp), you can manually define how those messages are defined as similar enough to be rate-limited together by setting a custom `stream_id`:

```python
logger.warning("Something went wrong with %s at %s!", device_id, timestamp, extra=RateLimit(stream_id=f"went_wrong_{device_id}"))
```

Note that if the timestamps are added by a `logging.Formatter` then they won't affect our rate-limiting filter as these filters run before formatting is applied (see [full workflow](https://docs.python.org/3/howto/logging.html#logging-flow)).

## Usage examples

### Rate-limiting by default

Example of rate-limiting with default options where each log message is assigned to its own stream:
```python
import time
import logging
from log_rate_limit import StreamRateLimitFilter, RateLimit
# Setup logging
logging.basicConfig()
logger = logging.getLogger(__name__)

# Add our filter
logger.addFilter(StreamRateLimitFilter(period_sec=1))
# Log many warnings
for _ in range(100):
    logger.warning("Wolf!")
for i in range(100):
    logger.warning("No really, a wolf!")
    if i == 98:
        time.sleep(1)
# Prevent rate-limited by setting/overriding the stream to be undefined (None)
for _ in range(3):
    logger.warning("Sheep!", extra=RateLimit(stream_id=None))
``` 
Which only outputs the following:
```log
WARNING:__main__:Wolf!
WARNING:__main__:No really, a wolf!
WARNING:__main__:No really, a wolf!
+ skipped 98 logs due to rate-limiting
WARNING:__main__:Sheep!
WARNING:__main__:Sheep!
WARNING:__main__:Sheep!
```
Note that (unless overridden) logs were only repeated after the `sleep()` call, and the repeated log also included an extra summary message added afterwards.

When we override rate-limiting above, you'll see our filter reads dynamic configs from logging's `extra` parameter.

> Be very careful not to forget the `extra=` name part of the argument, as then the logging framework will assume you're passing arguments meant for formatting in the logging message and your options will silently be ignored!

### Rate-limit only when specified

If you want most of your logs to be unaffected and you only have some you want to specifically rate-limit, then you can do the following:
```python
import logging
from log_rate_limit import StreamRateLimitFilter, RateLimit
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Add our filter, but don't assign unique streams to logs by default
logger.addFilter(StreamRateLimitFilter(period_sec=1, default_stream_id=None))
# Normal logs are now not rate-limited
for i in range(3):
    logger.info(f"Status update: {i}")
# Only those we manually assign a stream will be rate-limited
for _ in range(3):
    logger.warning("Issue!", extra=RateLimit(stream_id="issue"))
```
Which only outputs the following:
```log
INFO:__main__:Status update: 0
INFO:__main__:Status update: 1
INFO:__main__:Status update: 2
WARNING:__main__:Issue!
```

### Dynamically override configuration options

Some options set during creation of the initial filter can be overridden for individual log calls. This is done by adding the `extra` parameter to any specific log call, e.g.:
```python
# Override the rate-limit period for this specific log call
logger.warning("Test1", extra=RateLimit(stream_id="stream1", period_sec=30))
# Override the allow_next_n value for a set of logs in the same stream so that this group of logs don't restrict one
# another from occuring consecutively
logger.warning("Test", extra=RateLimit(stream_id="stream2", allow_next_n=2))
logger.info("Extra", extra=RateLimit(stream_id="stream2"))
logger.debug("Info", extra=RateLimit(stream_id="stream2"))
```

If you want to set custom options for a large group of log calls without repeatedly adding the `extra` parameter, it's possible to use a [LoggerAdapter](https://docs.python.org/3/library/logging.html#loggeradapter-objects):
```python
import logging
from log_rate_limit import StreamRateLimitFilter, RateLimit

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Add our filter
logger.addFilter(StreamRateLimitFilter(period_sec=1))

# Use LoggerAdapter to assign additional "extra" parameters to all calls using this logger
global_extra = RateLimit(stream_id="custom_stream", period_sec=20)
logger = logging.LoggerAdapter(logger, global_extra)
# Log many warnings
for _ in range(100):
    logger.warning("Wolf!")
for i in range(100):
    logger.warning("No really, a wolf!")
```
Which merely outputs:
```log
WARNING:__main__:Wolf!
```
Since both log calls are in the same stream.

Alternatively (to a LoggerAdapter), custom options can also be added by writing your own [logging.Filter](https://docs.python.org/3.8/howto/logging-cookbook.html#using-filters-to-impart-contextual-information).

### Dynamic stream ID

Dynamic stream IDs can be assigned based on any criteria you want, e.g.:

```python
logger.warning(f"Error occured on device {device_id}!", extra=RateLimit(stream_id=f"error_on_{device_id}"))
```

## Installation

### Install from PyPI

With `Python 3` installed on your system, you can run:

    pip install log-rate-limit

To test that installation worked, you can run:

    python -c "import log_rate_limit"

and you can uninstall at any time with:

    pip uninstall log-rate-limit

To install with poetry:

    poetry add log-rate-limit

### Install from Github

To install the newest code directly from Github:

    pip install git+https://github.com/samuller/log-rate-limit

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/samuller/log-rate-limit",
    "name": "log-rate-limit",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8.1,<4.0.0",
    "maintainer_email": "",
    "keywords": "logging,log,limit,rate",
    "author": "Simon Muller",
    "author_email": "samullers@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/91/73/9c015029fc6759ecc9196f756224302a13d2bbc18a12f48cedabfb13ca8f/log_rate_limit-1.4.1.tar.gz",
    "platform": null,
    "description": "# log-rate-limit - limit excessive log output\n\n[![PyPI Version](https://img.shields.io/pypi/v/log-rate-limit)](https://pypi.org/project/log-rate-limit/)\n[![Build Status](https://github.com/samuller/log-rate-limit/actions/workflows/tests.yml/badge.svg)](https://github.com/samuller/log-rate-limit/actions/workflows/tests.yml)\n[![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/samuller/pgmerge/actions)\n[![Checked with mypy](https://img.shields.io/badge/mypy-strict-blue)](http://mypy-lang.org/)\n[![Formatted with black](https://img.shields.io/badge/code%20style-black-black)](https://black.readthedocs.io/en/stable/)\n[![Code Complexity](https://img.shields.io/badge/max--complexity-%3C10-blue)](https://flake8.pycqa.org/en/6.1.0/user/options.html#cmdoption-flake8-max-complexity)\n\nA [logging filter](https://docs.python.org/3/library/logging.html#filter-objects) using Python's standard logging mechanisms to rate-limit logs - i.e. suppress logs when they are being output too fast.\n\nLog commands are grouped into separate **streams** that will each have their own rate limitation applied without affecting the logs in other streams. By default every log message is assigned a unique stream so that only repeated log messages will be suppressed.\n\nHowever, logs can also be assigned streams manually to achieve various outcomes:\n- A log can be assigned to an undefined/`None` stream so that rate-limiting doesn't apply to it.\n- Logs in different parts of the code can be grouped into the same stream so that they share a rate-limit, e.g. when they all trigger due to the same issue and only some are needed to indicate it.\n\nThe default can also be changed so that rate-limiting is disabled by default and only applies to logs for which a `stream_id` has been manually set.\n\n## Quick usage\n\nImport the filter:\n\n```python\nfrom log_rate_limit import StreamRateLimitFilter, RateLimit\n```\n\nUse the filter with your `logger` - with default parameters it will rate-limit all repetitive log messages:\n\n```python\nlogger.addFilter(StreamRateLimitFilter(period_sec=30))\n```\n\nAll logs on `logger` will now be rate-limited, but this can be disabled per-log by setting the `stream_id` to `None`:\n\n```python\nlogger.warning(\"Something went wrong!\", extra=RateLimit(stream_id=None))\n```\n\nIf you have a log message that's continually changing (e.g. contains a timestamp), you can manually define how those messages are defined as similar enough to be rate-limited together by setting a custom `stream_id`:\n\n```python\nlogger.warning(\"Something went wrong with %s at %s!\", device_id, timestamp, extra=RateLimit(stream_id=f\"went_wrong_{device_id}\"))\n```\n\nNote that if the timestamps are added by a `logging.Formatter` then they won't affect our rate-limiting filter as these filters run before formatting is applied (see [full workflow](https://docs.python.org/3/howto/logging.html#logging-flow)).\n\n## Usage examples\n\n### Rate-limiting by default\n\nExample of rate-limiting with default options where each log message is assigned to its own stream:\n```python\nimport time\nimport logging\nfrom log_rate_limit import StreamRateLimitFilter, RateLimit\n# Setup logging\nlogging.basicConfig()\nlogger = logging.getLogger(__name__)\n\n# Add our filter\nlogger.addFilter(StreamRateLimitFilter(period_sec=1))\n# Log many warnings\nfor _ in range(100):\n    logger.warning(\"Wolf!\")\nfor i in range(100):\n    logger.warning(\"No really, a wolf!\")\n    if i == 98:\n        time.sleep(1)\n# Prevent rate-limited by setting/overriding the stream to be undefined (None)\nfor _ in range(3):\n    logger.warning(\"Sheep!\", extra=RateLimit(stream_id=None))\n``` \nWhich only outputs the following:\n```log\nWARNING:__main__:Wolf!\nWARNING:__main__:No really, a wolf!\nWARNING:__main__:No really, a wolf!\n+ skipped 98 logs due to rate-limiting\nWARNING:__main__:Sheep!\nWARNING:__main__:Sheep!\nWARNING:__main__:Sheep!\n```\nNote that (unless overridden) logs were only repeated after the `sleep()` call, and the repeated log also included an extra summary message added afterwards.\n\nWhen we override rate-limiting above, you'll see our filter reads dynamic configs from logging's `extra` parameter.\n\n> Be very careful not to forget the `extra=` name part of the argument, as then the logging framework will assume you're passing arguments meant for formatting in the logging message and your options will silently be ignored!\n\n### Rate-limit only when specified\n\nIf you want most of your logs to be unaffected and you only have some you want to specifically rate-limit, then you can do the following:\n```python\nimport logging\nfrom log_rate_limit import StreamRateLimitFilter, RateLimit\n# Setup logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# Add our filter, but don't assign unique streams to logs by default\nlogger.addFilter(StreamRateLimitFilter(period_sec=1, default_stream_id=None))\n# Normal logs are now not rate-limited\nfor i in range(3):\n    logger.info(f\"Status update: {i}\")\n# Only those we manually assign a stream will be rate-limited\nfor _ in range(3):\n    logger.warning(\"Issue!\", extra=RateLimit(stream_id=\"issue\"))\n```\nWhich only outputs the following:\n```log\nINFO:__main__:Status update: 0\nINFO:__main__:Status update: 1\nINFO:__main__:Status update: 2\nWARNING:__main__:Issue!\n```\n\n### Dynamically override configuration options\n\nSome options set during creation of the initial filter can be overridden for individual log calls. This is done by adding the `extra` parameter to any specific log call, e.g.:\n```python\n# Override the rate-limit period for this specific log call\nlogger.warning(\"Test1\", extra=RateLimit(stream_id=\"stream1\", period_sec=30))\n# Override the allow_next_n value for a set of logs in the same stream so that this group of logs don't restrict one\n# another from occuring consecutively\nlogger.warning(\"Test\", extra=RateLimit(stream_id=\"stream2\", allow_next_n=2))\nlogger.info(\"Extra\", extra=RateLimit(stream_id=\"stream2\"))\nlogger.debug(\"Info\", extra=RateLimit(stream_id=\"stream2\"))\n```\n\nIf you want to set custom options for a large group of log calls without repeatedly adding the `extra` parameter, it's possible to use a [LoggerAdapter](https://docs.python.org/3/library/logging.html#loggeradapter-objects):\n```python\nimport logging\nfrom log_rate_limit import StreamRateLimitFilter, RateLimit\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n# Add our filter\nlogger.addFilter(StreamRateLimitFilter(period_sec=1))\n\n# Use LoggerAdapter to assign additional \"extra\" parameters to all calls using this logger\nglobal_extra = RateLimit(stream_id=\"custom_stream\", period_sec=20)\nlogger = logging.LoggerAdapter(logger, global_extra)\n# Log many warnings\nfor _ in range(100):\n    logger.warning(\"Wolf!\")\nfor i in range(100):\n    logger.warning(\"No really, a wolf!\")\n```\nWhich merely outputs:\n```log\nWARNING:__main__:Wolf!\n```\nSince both log calls are in the same stream.\n\nAlternatively (to a LoggerAdapter), custom options can also be added by writing your own [logging.Filter](https://docs.python.org/3.8/howto/logging-cookbook.html#using-filters-to-impart-contextual-information).\n\n### Dynamic stream ID\n\nDynamic stream IDs can be assigned based on any criteria you want, e.g.:\n\n```python\nlogger.warning(f\"Error occured on device {device_id}!\", extra=RateLimit(stream_id=f\"error_on_{device_id}\"))\n```\n\n## Installation\n\n### Install from PyPI\n\nWith `Python 3` installed on your system, you can run:\n\n    pip install log-rate-limit\n\nTo test that installation worked, you can run:\n\n    python -c \"import log_rate_limit\"\n\nand you can uninstall at any time with:\n\n    pip uninstall log-rate-limit\n\nTo install with poetry:\n\n    poetry add log-rate-limit\n\n### Install from Github\n\nTo install the newest code directly from Github:\n\n    pip install git+https://github.com/samuller/log-rate-limit\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Limit excessive log output with Python's standard logging framework.",
    "version": "1.4.1",
    "project_urls": {
        "Homepage": "https://github.com/samuller/log-rate-limit",
        "Repository": "https://github.com/samuller/log-rate-limit"
    },
    "split_keywords": [
        "logging",
        "log",
        "limit",
        "rate"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "8bf287bf50e06e69c3a5f50f8b465a2e397399887a7e76844c0845989e0ff312",
                "md5": "dbd6f498d783c482ffa325a29885f865",
                "sha256": "7cf0dd44ebc6ef4ae3b1f749ab4f67d8cb47036acf18d59cca5aacdd1883ecb5"
            },
            "downloads": -1,
            "filename": "log_rate_limit-1.4.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "dbd6f498d783c482ffa325a29885f865",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8.1,<4.0.0",
            "size": 17780,
            "upload_time": "2023-11-02T19:44:17",
            "upload_time_iso_8601": "2023-11-02T19:44:17.008623Z",
            "url": "https://files.pythonhosted.org/packages/8b/f2/87bf50e06e69c3a5f50f8b465a2e397399887a7e76844c0845989e0ff312/log_rate_limit-1.4.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "91739c015029fc6759ecc9196f756224302a13d2bbc18a12f48cedabfb13ca8f",
                "md5": "6fb966e05f4f282f1174a5f38ffad492",
                "sha256": "3718705b392736675f5eaad8f42e4f319e5c6b6cefd912ea146c77339c19af35"
            },
            "downloads": -1,
            "filename": "log_rate_limit-1.4.1.tar.gz",
            "has_sig": false,
            "md5_digest": "6fb966e05f4f282f1174a5f38ffad492",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8.1,<4.0.0",
            "size": 18318,
            "upload_time": "2023-11-02T19:44:18",
            "upload_time_iso_8601": "2023-11-02T19:44:18.580563Z",
            "url": "https://files.pythonhosted.org/packages/91/73/9c015029fc6759ecc9196f756224302a13d2bbc18a12f48cedabfb13ca8f/log_rate_limit-1.4.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-11-02 19:44:18",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "samuller",
    "github_project": "log-rate-limit",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "log-rate-limit"
}
        
Elapsed time: 0.13226s