Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in log filter for pacing logs #497

Merged
merged 13 commits into from
Sep 5, 2024
9 changes: 8 additions & 1 deletion kytos/core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,14 @@ def _patch_core_loggers():
reloadable_mods = [module for mod_name, module in sys.modules.items()
if mod_name[:str_len] == match_str]
for module in reloadable_mods:
module.LOG = logging.getLogger(module.__name__)
new_logger = logging.getLogger(module.__name__)
if hasattr(module, "LOG"):
old_logger = module.LOG
for handler in old_logger.handlers:
new_logger.addHandler(handler)
for log_filter in old_logger.filters:
new_logger.addFilter(log_filter)
module.LOG = new_logger

@staticmethod
def loggers():
Expand Down
4 changes: 4 additions & 0 deletions kytos/core/pacing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
from limits import RateLimitItem, parse
from limits.storage import storage_from_string

from kytos.logging.filters import RepeateMessageFilter

LOG = logging.getLogger(__name__)

LOG.addFilter(RepeateMessageFilter(3.0, 512))
viniarck marked this conversation as resolved.
Show resolved Hide resolved


class EmptyStrategy(limits.strategies.FixedWindowRateLimiter):
"""Rate limiter, that doesn't actually rate limit."""
Expand Down
123 changes: 123 additions & 0 deletions kytos/logging/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

import time
from logging import LogRecord
from threading import Lock
from typing import Generic, Self, TypeVar

T = TypeVar("T")


class _DoublyLinkedList(Generic[T]):
value: T
previous: Self
next: Self

def __init__(self, value: T):
self.value = value
self.previous = self
self.next = self

def swap_next(self, other: Self):

curr_next = self.next
other_curr_next = other.next

self.next = other_curr_next
other_curr_next.previous = self
other.next = curr_next
curr_next.previous = other

def swap_previous(self, other: Self):
curr_prev = self.previous
other_curr_prev = other.previous

self.previous = other_curr_prev
other_curr_prev.next = self
other.previous = curr_prev
curr_prev.next = other

def remove(self):
self.swap_next(self.previous)

def __iter__(self):
yield self.value
curr = self.next
while curr is not self:
yield curr.value
curr = curr.next


K = TypeVar("K")
V = TypeVar("V")


class _LimitedCache(Generic[K, V]):
viniarck marked this conversation as resolved.
Show resolved Hide resolved
root: _DoublyLinkedList[tuple[K, V] | None]
cache: dict[K, _DoublyLinkedList[tuple[K, V]]]
max_size: int

def __init__(self, max_size=512):
self.root = _DoublyLinkedList(None)
self.cache = {}
self.max_size = max_size

def __getitem__(self, key: K) -> V:
entry = self.cache[key]
_, v = entry.value
return v

def __contains__(self, key: K) -> bool:
return key in self.cache

def __setitem__(self, key: K, value: V):
if key in self.cache:
entry = self.cache[key]
entry.remove()
entry.value = (key, value)
else:
entry = _DoublyLinkedList(
(key, value)
)
self.cache[key] = entry
if len(self.cache) == self.max_size:
self._remove_oldest()

self.root.swap_next(entry)

def __delitem__(self, key: K):
entry = self.cache[key]
del self.cache[key]
entry.remove()

def _remove_oldest(self):
oldest_entry = self.root.previous
k, _ = oldest_entry.value
del self.cache[k]
oldest_entry.remove()


class RepeateMessageFilter:
lockout_time: float
_cache: _LimitedCache
_lock: Lock

def __init__(self, lockout_time: float, cache_size: int = 512):
viniarck marked this conversation as resolved.
Show resolved Hide resolved
self.lockout_time = lockout_time
self._cache = _LimitedCache(cache_size)
self._lock = Lock()

def filter(self, record: LogRecord) -> bool:
key = self._record_key(record)
current_time = time.time()
with self._lock:
if key not in self._cache:
self._cache[key] = current_time
return True
elif current_time - self._cache[key] > self.lockout_time:
self._cache[key] = current_time
return True
return False

@staticmethod
def _record_key(record: LogRecord):
return (record.module, record.levelno, record.msg, record.args)
viniarck marked this conversation as resolved.
Show resolved Hide resolved
Loading