Skip to main content

Limit excessive log output with Python's standard logging framework.

Project description

log-rate-limit - limit excessive log output

PyPI Version Build Status Code Coverage Checked with mypy Formatted with black Code Complexity

A logging filter 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:

from log_rate_limit import StreamRateLimitFilter, RateLimit

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

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:

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:

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).

Usage examples

Rate-limiting by default

Example of rate-limiting with default options where each log message is assigned to its own stream:

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:

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:

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:

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.:

# 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:

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:

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.

Dynamic stream ID

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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

log_rate_limit-1.4.0.tar.gz (18.2 kB view details)

Uploaded Source

Built Distribution

log_rate_limit-1.4.0-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file log_rate_limit-1.4.0.tar.gz.

File metadata

  • Download URL: log_rate_limit-1.4.0.tar.gz
  • Upload date:
  • Size: 18.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.2 CPython/3.8.10 Linux/5.15.90.1-microsoft-standard-WSL2

File hashes

Hashes for log_rate_limit-1.4.0.tar.gz
Algorithm Hash digest
SHA256 c47f891ff27b33e8984f362b6201de92fe3237533e031dfdbc8e0a7173e055b1
MD5 e5b4c3fd22c1c8a0b358d26d5a19a54b
BLAKE2b-256 ddb5f08d207ecb82d6625bc50a96552fcf9ed69cf1df59efba56171571eefa82

See more details on using hashes here.

File details

Details for the file log_rate_limit-1.4.0-py3-none-any.whl.

File metadata

  • Download URL: log_rate_limit-1.4.0-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.2 CPython/3.8.10 Linux/5.15.90.1-microsoft-standard-WSL2

File hashes

Hashes for log_rate_limit-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0baff5ef8a5034caa17c7fdd5981fdbb8b6c55be7bbbeba8fea0912e087fec0c
MD5 06ee316470f68a5e5994c9de353a5432
BLAKE2b-256 061ecb6a7b8467fa5cb3ae62f28e21a79b33b03066f64ecac36c0cc46f061902

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page