├── polog ├── core │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── not_none_to_dict.py │ │ ├── exception_to_dict.py │ │ ├── is_json.py │ │ ├── exception_is_suppressed.py │ │ ├── exception_escaping.py │ │ ├── cut_traceback.py │ │ ├── read_only_singleton.py │ │ ├── get_traceback.py │ │ ├── get_errors_level.py │ │ ├── name_manager.py │ │ ├── pony_names_generator.py │ │ ├── time_limit.py │ │ └── signature_matcher.py │ ├── engine │ │ ├── __init__.py │ │ └── real_engines │ │ │ ├── __init__.py │ │ │ ├── multithreaded │ │ │ ├── __init__.py │ │ │ ├── pool.py │ │ │ ├── worker.py │ │ │ └── engine.py │ │ │ ├── singlethreaded │ │ │ ├── __init__.py │ │ │ └── engine.py │ │ │ ├── fabric.py │ │ │ └── abstract.py │ └── stores │ │ ├── __init__.py │ │ ├── settings │ │ ├── __init__.py │ │ └── actions │ │ │ ├── reload_engine.py │ │ │ ├── __init__.py │ │ │ ├── fields_intersection.py │ │ │ ├── set_log_as_built_in.py │ │ │ ├── decorator.py │ │ │ └── integration_with_logging.py │ │ ├── fields.py │ │ ├── handlers.py │ │ └── levels.py ├── tests │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── test_not_none_to_dict.py │ │ │ ├── test_exception_to_dict.py │ │ │ ├── test_is_json.py │ │ │ ├── test_read_only_singleton.py │ │ │ ├── test_exception_escaping.py │ │ │ ├── test_cut_traceback.py │ │ │ ├── test_get_traceback.py │ │ │ ├── test_get_errors_level.py │ │ │ ├── test_exception_is_suppressed.py │ │ │ ├── test_pony_names_generator.py │ │ │ ├── test_time_limit.py │ │ │ └── test_name_manager.py │ │ ├── engine │ │ │ └── real_engines │ │ │ │ ├── __init__.py │ │ │ │ ├── multithreaded │ │ │ │ ├── __init__.py │ │ │ │ ├── test_worker.py │ │ │ │ └── test_engine.py │ │ │ │ └── singlethreaded │ │ │ │ ├── __init__.py │ │ │ │ └── test_engine.py │ │ └── stores │ │ │ ├── settings │ │ │ ├── __init__.py │ │ │ └── actions │ │ │ │ ├── __init__.py │ │ │ │ ├── test_reload_engine.py │ │ │ │ ├── test_set_log_as_built_in.py │ │ │ │ └── test_decorator.py │ │ │ └── test_levels.py │ ├── data │ │ └── .gitkeep │ ├── loggers │ │ ├── __init__.py │ │ ├── auto │ │ │ └── __init__.py │ │ └── handle │ │ │ ├── __init__.py │ │ │ └── test_abstract.py │ ├── utils │ │ ├── __init__.py │ │ └── test_json_vars.py │ ├── handlers │ │ ├── file │ │ │ ├── __init__.py │ │ │ ├── locks │ │ │ │ ├── __init__.py │ │ │ │ ├── test_abstract_single_lock.py │ │ │ │ ├── test_thread_lock.py │ │ │ │ └── test_file_lock.py │ │ │ ├── rotation │ │ │ │ ├── __init__.py │ │ │ │ ├── rules │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── rules │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── tokenization │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ ├── tokens │ │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ │ ├── abstractions │ │ │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ │ │ └── test_meta_token.py │ │ │ │ │ │ │ │ ├── test_dot_token.py │ │ │ │ │ │ │ │ ├── test_number_token.py │ │ │ │ │ │ │ │ ├── test_size_token.py │ │ │ │ │ │ │ │ └── test_tokens_group.py │ │ │ │ │ │ │ └── test_tokenizator.py │ │ │ │ │ │ ├── test_abstract_rule.py │ │ │ │ │ │ └── test_file_size_rule.py │ │ │ │ │ ├── polog │ │ │ │ │ │ └── tests │ │ │ │ │ │ │ └── data │ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ └── test_rules_elector.py │ │ │ │ ├── test_parser.py │ │ │ │ └── test_rotator.py │ │ │ └── polog │ │ │ │ └── tests │ │ │ │ └── data │ │ │ │ └── .gitkeep │ │ ├── abstract │ │ │ ├── __init__.py │ │ │ └── test_base.py │ │ ├── memory │ │ │ ├── __init__.py │ │ │ └── test_saver.py │ │ └── smtp │ │ │ └── test_sender.py │ ├── data_structures │ │ ├── __init__.py │ │ ├── trees │ │ │ ├── __init__.py │ │ │ └── named_tree │ │ │ │ ├── __init__.py │ │ │ │ ├── test_printer.py │ │ │ │ └── test_projector.py │ │ └── wrappers │ │ │ ├── __init__.py │ │ │ ├── weak_linked │ │ │ └── __init__.py │ │ │ └── fields_container │ │ │ ├── __init__.py │ │ │ └── test_container.py │ ├── test_config_without_log.py │ ├── test_errors.py │ ├── conftest.py │ └── test_field.py ├── utils │ ├── __init__.py │ └── json_vars.py ├── handlers │ ├── __init__.py │ ├── file │ │ ├── __init__.py │ │ ├── locks │ │ │ ├── __init__.py │ │ │ ├── thread_lock.py │ │ │ ├── abstract_single_lock.py │ │ │ ├── file_lock.py │ │ │ └── double_lock.py │ │ ├── rotation │ │ │ ├── __init__.py │ │ │ ├── rules │ │ │ │ ├── __init__.py │ │ │ │ ├── rules │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── tokenization │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── tokens │ │ │ │ │ │ │ ├── abstractions │ │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ │ ├── meta_token.py │ │ │ │ │ │ │ │ └── abstract_token.py │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ ├── number_token.py │ │ │ │ │ │ │ ├── dot_token.py │ │ │ │ │ │ │ └── size_token.py │ │ │ │ │ │ └── tokenizator.py │ │ │ │ │ ├── period_rule.py │ │ │ │ │ ├── date_time_rule.py │ │ │ │ │ ├── number_rule.py │ │ │ │ │ ├── file_size_rule.py │ │ │ │ │ └── abstract_rule.py │ │ │ │ └── rules_elector.py │ │ │ └── parser.py │ │ └── writer.py │ ├── memory │ │ ├── __init__.py │ │ └── saver.py │ ├── smtp │ │ ├── __init__.py │ │ └── smtp_dependency_wrapper.py │ └── abstract │ │ └── __init__.py ├── loggers │ ├── __init__.py │ ├── auto │ │ ├── __init__.py │ │ └── class_logger.py │ ├── handle │ │ ├── __init__.py │ │ ├── message.py │ │ ├── smart_assert.py │ │ └── handle_log.py │ └── partial.py ├── data_structures │ ├── __init__.py │ ├── trees │ │ ├── __init__.py │ │ └── named_tree │ │ │ ├── __init__.py │ │ │ ├── projector.py │ │ │ ├── printer.py │ │ │ └── walker.py │ └── wrappers │ │ ├── __init__.py │ │ ├── weak_linked │ │ ├── __init__.py │ │ └── dictionary.py │ │ └── fields_container │ │ ├── __init__.py │ │ └── container.py ├── __init__.py ├── errors.py ├── unlog.py └── field.py ├── coverage_report.sh ├── requirements_dev.txt ├── .gitignore ├── setup.py ├── LICENSE └── .github └── workflows └── coverage.yml /polog/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/loggers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/engine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/stores/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/memory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/smtp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/loggers/auto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/loggers/handle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/loggers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/stores/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/abstract/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/locks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/loggers/auto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/loggers/handle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/trees/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/abstract/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/locks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/memory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/stores/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/trees/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/trees/named_tree/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/stores/settings/actions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/polog/tests/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/multithreaded/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/singlethreaded/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/wrappers/weak_linked/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/trees/named_tree/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/data_structures/wrappers/fields_container/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/multithreaded/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/singlethreaded/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/wrappers/weak_linked/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/data_structures/wrappers/fields_container/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/polog/tests/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/core/stores/fields.py: -------------------------------------------------------------------------------- 1 | in_place_fields = {} 2 | engine_fields = {} 3 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/abstractions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/abstractions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /coverage_report.sh: -------------------------------------------------------------------------------- 1 | coverage run --source=polog --omit="*tests*" -m pytest --cache-clear 2 | coverage html 3 | open htmlcov/index.html 4 | pygount polog --suffix=py --format=summary 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | ujson==5.4.0 2 | wheel==0.33.1 3 | pip==21.1 4 | twine==1.13.0 5 | pyparsing>=2.0.2 6 | pytest==7.1.2 7 | pytest-xdist==2.4.0 8 | coverage==5.5 9 | pygount==1.2.4 10 | termcolor==1.1.0 11 | -------------------------------------------------------------------------------- /polog/core/utils/not_none_to_dict.py: -------------------------------------------------------------------------------- 1 | def not_none_to_dict(args_dict, key, value): 2 | """ 3 | Если значение не None, кладем его в словарь. 4 | """ 5 | if not (value is None): 6 | args_dict[key] = value 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | virt 3 | venv 4 | .venv 5 | env 6 | .DS_Store 7 | tests*.py 8 | __pycache__ 9 | *.db 10 | *.egg-info 11 | dist 12 | build 13 | .pytest_cache 14 | *.log 15 | *.logs 16 | .coverage 17 | htmlcov 18 | *.lock 19 | -------------------------------------------------------------------------------- /polog/core/stores/handlers.py: -------------------------------------------------------------------------------- 1 | from polog.data_structures.trees.named_tree.tree import NamedTree 2 | from polog.core.utils.signature_matcher import SignatureMatcher 3 | 4 | 5 | global_handlers = NamedTree( 6 | value_checker=SignatureMatcher.is_handler, 7 | ) 8 | -------------------------------------------------------------------------------- /polog/core/utils/exception_to_dict.py: -------------------------------------------------------------------------------- 1 | def exception_to_dict(args_dict, exc): 2 | """ 3 | Заполняем словарь с аргументами информацией об исключении. 4 | """ 5 | args_dict['exception_message'] = str(exc) 6 | args_dict['exception_type'] = type(exc).__name__ 7 | -------------------------------------------------------------------------------- /polog/core/utils/is_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def is_json(value): 5 | """ 6 | Функция, проверяющая, является ли переданное ей значение валидной строкой формата json. 7 | """ 8 | try: 9 | json.loads(value) 10 | except Exception: 11 | return False 12 | return True 13 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/period_rule.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.file_size_rule import AbstractRule 2 | 3 | 4 | class PeriodRule(AbstractRule): 5 | @classmethod 6 | def prove_source(cls, source): 7 | raise NotImplementedError 8 | 9 | def check(self): 10 | raise NotImplementedError 11 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/date_time_rule.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.file_size_rule import AbstractRule 2 | 3 | 4 | class DateTimeRule(AbstractRule): 5 | @classmethod 6 | def prove_source(cls, source): 7 | raise NotImplementedError 8 | 9 | def check(self): 10 | raise NotImplementedError 11 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/__init__.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.size_token import SizeToken 2 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.number_token import NumberToken 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.dot_token import DotToken 4 | -------------------------------------------------------------------------------- /polog/tests/test_config_without_log.py: -------------------------------------------------------------------------------- 1 | from polog import config, file_writer 2 | 3 | 4 | def do_log(filename): 5 | """ 6 | Здесь используется, но нигде не импортируется функция log(). 7 | Проверяем, что так можно. 8 | """ 9 | config.add_handlers(file_writer(filename)) 10 | config.set(log_is_built_in=True) 11 | 12 | log('kek') 13 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/reload_engine.py: -------------------------------------------------------------------------------- 1 | from polog.core.stores.settings.actions.decorator import is_action 2 | 3 | 4 | @is_action 5 | def reload_engine(old_value, new_value, store): 6 | """ 7 | Перезапуск движка Polog. 8 | 9 | Функция используется для обхода циклических импортов. 10 | """ 11 | from polog.core.engine.engine import Engine 12 | Engine().reload() 13 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/number_rule.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.file_size_rule import AbstractRule 2 | 3 | 4 | class NumberRule(AbstractRule): 5 | """ 6 | Правило для ротации логов раз в n записей. 7 | """ 8 | @classmethod 9 | def prove_source(cls, source): 10 | raise NotImplementedError 11 | 12 | def check(self): 13 | raise NotImplementedError 14 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from polog.core.stores.settings.actions.reload_engine import reload_engine 2 | from polog.core.stores.settings.actions.fields_intersection import fields_intersection_action 3 | from polog.core.stores.settings.actions.set_log_as_built_in import set_log_as_built_in 4 | from polog.core.stores.settings.actions.integration_with_logging import integration_with_logging, from_logging_filter_to_polog 5 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_not_none_to_dict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.not_none_to_dict import not_none_to_dict 4 | 5 | 6 | def test_base(): 7 | """ 8 | Проверяем, что None в словаре не сохраняется, прочие значения - сохраняются. 9 | """ 10 | data = {} 11 | not_none_to_dict(data, 'key', 'value') 12 | assert data['key'] == 'value' 13 | data = {} 14 | not_none_to_dict(data, 'key', None) 15 | assert data.get('key') is None 16 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/number_token.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.abstractions.abstract_token import AbstractToken 2 | 3 | 4 | class NumberToken(AbstractToken): 5 | regexp_letter = 'n' 6 | 7 | @classmethod 8 | def its_me(cls, chunk): 9 | try: 10 | number = int(chunk) 11 | return True 12 | except Exception: 13 | return False 14 | 15 | def parse(self): 16 | return int(self.source) 17 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_exception_to_dict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.exception_to_dict import exception_to_dict 4 | 5 | 6 | def test_exception_to_dict(): 7 | """ 8 | Проверяем, что из исключения в словарь извлекаются его название и сообщение. 9 | """ 10 | args = {} 11 | try: 12 | raise Exception('hi!') 13 | except Exception as e: 14 | exception_to_dict(args, e) 15 | assert args['exception_message'] == 'hi!' 16 | assert args['exception_type'] == 'Exception' 17 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/test_abstract_rule.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.abstract_rule import AbstractRule 4 | 5 | 6 | def test_abstract_rule_repr_method(): 7 | """ 8 | Проверяем, что у экземпляров отнаследованного класса корректно работает строковая репрезентация содержимого. 9 | """ 10 | class KekRule(AbstractRule): 11 | def prove_source(self): 12 | return False 13 | 14 | assert str(KekRule('kek', None)) == 'KekRule("kek")' 15 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/singlethreaded/engine.py: -------------------------------------------------------------------------------- 1 | from polog.core.engine.real_engines.abstract import AbstractRealEngine 2 | from polog.core.utils.exception_escaping import exception_escaping 3 | 4 | 5 | class SingleThreadedRealEngine(AbstractRealEngine): 6 | """ 7 | Однопоточная синхронная реализация движка. 8 | """ 9 | def write(self, log_item): 10 | """ 11 | "Выполняем" лог, то есть запускаем все привязанные к нему действия - извлечения полей, передачу лога в обработчики и т. д. 12 | """ 13 | log_item() 14 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/fields_intersection.py: -------------------------------------------------------------------------------- 1 | from polog.core.log_item import LogItem 2 | from polog.core.stores.settings.actions.decorator import is_action 3 | 4 | 5 | @is_action 6 | def fields_intersection_action(old_value, new_value, store): 7 | """ 8 | Коллбек, вызываемый при изменении настройки 'fields_intersection'. 9 | 10 | Внутри класса LogItem кэшируется значение данной настройки, поэтому нужно синхронизировать изменения внутри хранилища настроек и внутри класса. 11 | """ 12 | LogItem._fields_intersection = new_value 13 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/fabric.py: -------------------------------------------------------------------------------- 1 | from polog.core.engine.real_engines.multithreaded.engine import MultiThreadedRealEngine 2 | from polog.core.engine.real_engines.singlethreaded.engine import SingleThreadedRealEngine 3 | 4 | 5 | def real_engine_fabric(settings): 6 | """ 7 | Здесь "порождаются" движки Polog. 8 | 9 | Т. к. их несколько, выбор движка осуществляется исходя из актуальных настроек. 10 | """ 11 | if settings.force_get('pool_size') == 0: 12 | return SingleThreadedRealEngine(settings) 13 | return MultiThreadedRealEngine(settings) 14 | -------------------------------------------------------------------------------- /polog/__init__.py: -------------------------------------------------------------------------------- 1 | from polog.loggers.auto.function_logger import flog 2 | from polog.loggers.auto.class_logger import clog 3 | from polog.loggers.handle.handle_log import handle_log 4 | from polog.loggers.handle.message import message 5 | from polog.loggers.router import log 6 | from polog.loggers.handle.smart_assert import smart_assert as ass 7 | from polog.config import config 8 | from polog.unlog import unlog 9 | from polog.utils.json_vars import json_vars 10 | from polog.field import field 11 | from polog.handlers.file.writer import file_writer 12 | from polog.handlers.smtp.sender import SMTP_sender 13 | -------------------------------------------------------------------------------- /polog/core/utils/exception_is_suppressed.py: -------------------------------------------------------------------------------- 1 | def exception_is_suppressed(exception, suppressed_exceptions, config): 2 | """ 3 | Здесь происходит проверка того, является ли переданное первым аргументом исключение одним из списка подавляемых. 4 | """ 5 | if config['suppress_exception_subclasses']: 6 | if any(isinstance(exception, suppressed_exception) for suppressed_exception in suppressed_exceptions): 7 | return True 8 | else: 9 | if any(type(exception) is suppressed_exception for suppressed_exception in suppressed_exceptions): 10 | return True 11 | return False 12 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/set_log_as_built_in.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from contextlib import suppress 3 | from polog.core.stores.settings.actions.decorator import is_action 4 | 5 | 6 | @is_action 7 | def set_log_as_built_in(old_value, new_value, store): 8 | """ 9 | Устанавливаем функцию log() в качестве системной. 10 | После этого ее можно будет использовать из любого места программы без дополнительного импорта. 11 | """ 12 | if new_value == True: 13 | from polog import log 14 | builtins.log = log 15 | else: 16 | with suppress(AttributeError): 17 | delattr(builtins, 'log') 18 | -------------------------------------------------------------------------------- /polog/tests/test_errors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.errors import IncorrectUseLoggerError, IncorrectUseOfTheDecoratorError, IncorrectUseOfTheContextManagerError, DoubleSettingError, AfterStartSettingError, RewritingLogError, HandlerNotFoundError 4 | 5 | 6 | errors = [IncorrectUseLoggerError, IncorrectUseOfTheDecoratorError, IncorrectUseOfTheContextManagerError, DoubleSettingError, AfterStartSettingError, RewritingLogError, HandlerNotFoundError] 7 | 8 | def test_multiraise(): 9 | """ 10 | Проверяем, что исключения поднимаются. 11 | """ 12 | for error in errors: 13 | with pytest.raises(error): 14 | raise error 15 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_is_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from polog.core.utils.is_json import is_json 6 | 7 | 8 | def test_recognize_valid_json(): 9 | """ 10 | Валидные строки json-формата должны распознаваться как таковые. Проверяем. 11 | """ 12 | assert is_json(json.dumps({'lol': 'kek'})) 13 | 14 | def test_recognize_not_valid_json(): 15 | """ 16 | Невалидные в рамках json-формата строки тоже должны распознаваться как таковые. Проверяем. 17 | """ 18 | assert not is_json('{') 19 | assert not is_json(']') 20 | assert not is_json(json.dumps({'lol': 'kek'})[:-1]) 21 | assert not is_json(1) 22 | assert not is_json('kek') 23 | -------------------------------------------------------------------------------- /polog/tests/core/stores/settings/actions/test_reload_engine.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.stores.settings.actions.reload_engine import reload_engine 4 | from polog import log 5 | from polog.core.stores.settings.settings_store import SettingsStore 6 | from polog.core.engine.engine import Engine 7 | 8 | 9 | def test_new_engine_generated(handler): 10 | """ 11 | Проверяем, что при вызове reload_engine() объект real_engine заменяется. 12 | """ 13 | log('kek') 14 | 15 | old_real_engine = Engine().real_engine 16 | 17 | reload_engine(1, 2, SettingsStore()) 18 | 19 | new_real_engine = Engine().real_engine 20 | 21 | assert old_real_engine is not new_real_engine 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as readme_file: 4 | readme = readme_file.read() 5 | 6 | requirements = [] 7 | 8 | setup( 9 | name="polog", 10 | version="0.0.17", 11 | author="Evgeniy Blinov", 12 | author_email="zheni-b@yandex.ru", 13 | description="The new generation logger", 14 | long_description=readme, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/pomponchik/polog", 17 | packages=find_packages(), 18 | install_requires=requirements, 19 | classifiers=[ 20 | "Programming Language :: Python :: 3.8", 21 | "License :: OSI Approved :: MIT License", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/multithreaded/test_worker.py: -------------------------------------------------------------------------------- 1 | import time 2 | from queue import Queue 3 | 4 | import pytest 5 | 6 | from polog.core.engine.real_engines.multithreaded.worker import Worker 7 | from polog.core.stores.settings.settings_store import SettingsStore 8 | from polog.core.log_item import LogItem 9 | 10 | 11 | queue = Queue() 12 | worker = Worker(queue, 1, SettingsStore()) 13 | 14 | 15 | def test_do(handler): 16 | """ 17 | Проверяем, что хендлер вызывается. 18 | """ 19 | log = LogItem() 20 | log.set_handlers([handler]) 21 | log.set_data({'lol': 'kek'}) 22 | 23 | worker.do_anything(log) 24 | assert handler.last is not None 25 | 26 | worker.set_stop_flag() 27 | worker.stop() 28 | -------------------------------------------------------------------------------- /polog/handlers/file/locks/thread_lock.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | from polog.handlers.file.locks.abstract_single_lock import AbstractSingleLock 4 | 5 | 6 | class ThreadLock(AbstractSingleLock): 7 | """ 8 | Обертка вокруг обычного тред-лока (см. https://en.wikipedia.org/wiki/Lock_(computer_science)). 9 | 10 | Предназначена, чтобы сделать лок отключаемым. 11 | """ 12 | def __init__(self, on=True): 13 | if not on: 14 | self.off() 15 | else: 16 | self.lock = Lock() 17 | 18 | def acquire(self): 19 | """ 20 | Взять лок. 21 | """ 22 | self.lock.acquire() 23 | 24 | def release(self): 25 | """ 26 | Отпустить лок. 27 | """ 28 | self.lock.release() 29 | -------------------------------------------------------------------------------- /polog/core/utils/exception_escaping.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from inspect import iscoroutinefunction 3 | 4 | 5 | def exception_escaping(function): 6 | """ 7 | Декоратор для подавления любых исключений. 8 | 9 | Применять с осторожностью и только по отношению к пользовательским функциям. Недопустимо экранировать внутренние ошибки Polog. 10 | """ 11 | @wraps(function) 12 | def wrapper(*args, **kwargs): 13 | try: 14 | return function(*args, **kwargs) 15 | except: 16 | pass 17 | @wraps(function) 18 | async def async_wrapper(*args, **kwargs): 19 | try: 20 | return await function(*args, **kwargs) 21 | except: 22 | pass 23 | if iscoroutinefunction(function): 24 | return async_wrapper 25 | return wrapper 26 | -------------------------------------------------------------------------------- /polog/core/utils/cut_traceback.py: -------------------------------------------------------------------------------- 1 | try: 2 | from sys import exc_info 3 | from _testcapi import set_exc_info 4 | 5 | 6 | def cut_traceback(store): 7 | """ 8 | Обрезаем 1 уровень трейсбека. Полезно для использования в декораторах, чтобы они не "мусорили" в трейсбек. 9 | """ 10 | if store['traceback_cutting']: 11 | tp, exc, tb = exc_info() 12 | set_exc_info(tp, exc, tb.tb_next) 13 | 14 | except ImportError: # pragma: no cover 15 | def cut_traceback(store): 16 | """ 17 | Модуль _testcapi может быть не доступен в некоторых интерпретаторах. В этих случаях обрезание трейсбека происходить не будет. 18 | 19 | См.: 20 | 1. https://stackoverflow.com/a/68908998/14522393 21 | 2. https://stackoverflow.com/a/73418463/14522393 22 | """ 23 | pass 24 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/dot_token.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.abstractions.abstract_token import AbstractToken 2 | 3 | 4 | class DotToken(AbstractToken): 5 | """ 6 | Токен "по умолчанию". Любая строка может представляет токен данного типа, если только она не представляет токен какого-то другого типа. 7 | """ 8 | regexp_letter = 'd' 9 | 10 | @classmethod 11 | def its_me(cls, chunk): 12 | """ 13 | Любой строке говорим "да" - она представляет DotToken. 14 | Поэтому на принадлежность к DotToken'у нужно проверять в последнюю очередь. 15 | """ 16 | return True 17 | 18 | def parse(self): 19 | """ 20 | Возвращаем исходную строку, чтобы работали сравнения токенов. 21 | """ 22 | return self.source 23 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/test_dot_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.dot_token import DotToken 4 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.number_token import NumberToken 5 | 6 | 7 | def test_equal_to_dot_token(): 8 | """ 9 | Проверяем, что проверка на равенство универсального токена работает корректно. 10 | """ 11 | assert DotToken('kek') == DotToken('kek') 12 | assert DotToken('lol') != DotToken('kek') 13 | assert DotToken('lol') != NumberToken('20') 14 | assert DotToken('lol') != True 15 | 16 | def test_str_representation_of_dot_token(): 17 | """ 18 | Пробуем преобразовать токен в строку при помощи str(), должно срабатывать. 19 | """ 20 | assert str(DotToken('.')) == 'DotToken(".")' 21 | assert str(DotToken('kek')) == 'DotToken("kek")' 22 | -------------------------------------------------------------------------------- /polog/core/utils/read_only_singleton.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | 4 | class ReadOnlySingleton: 5 | """ 6 | Базовый класс синглтона, в котором лочится только момент первого создания экземпляра, для защиты от состояния гонки. 7 | Подразумевается, что прочие аспекты существования класса пользователь защищает от состояния гонки самостоятельно. 8 | """ 9 | create_lock = Lock() 10 | 11 | def __new__(cls, **kwargs): 12 | """ 13 | Данный метод отрабатывает только один раз. 14 | После чего подменяется объявленной внутри него же функцией __new__(), которая всегда возвращает экземпляр без дополнительных проверок. 15 | """ 16 | with cls.create_lock: 17 | def __new__(cls, **kwargs): 18 | return cls.instance 19 | cls.instance = super().__new__(cls) 20 | cls.__new__ = __new__ 21 | return cls.instance 22 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/parser.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules_elector import RulesElector 2 | 3 | 4 | class Parser: 5 | """ 6 | Здесь исходная строка с правилами ротации превращается в список объектов правил. 7 | """ 8 | def __init__(self, file, elector=RulesElector): 9 | self.file = file 10 | self.elector = elector(file) 11 | 12 | def extract_rules(self, rules): 13 | """ 14 | Делим строку по разделителю и скармливаем получившиеся подстроки распознавателю правил. 15 | """ 16 | result = [] 17 | splitted_rules = self.split_source(rules) 18 | for source in splitted_rules: 19 | rule = self.elector.choose(source) 20 | result.append(rule) 21 | return result 22 | 23 | def split_source(self, source): 24 | """ 25 | Делим строку по разделителю. 26 | """ 27 | result = [x.strip() for x in source.replace(';', ',').split(',') if x.strip()] 28 | return result 29 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/test_rules_elector.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules_elector import RulesElector 4 | from polog.handlers.file.rotation.rules.rules.file_size_rule import FileSizeRule 5 | from polog.handlers.file.file_dependency_wrapper import FileDependencyWrapper 6 | 7 | 8 | def test_rules_elector_base_behavior_with_file_size_rule(filename_for_test): 9 | file = FileDependencyWrapper([filename_for_test], lock_type='thread+file') 10 | elector = RulesElector(file) 11 | 12 | assert isinstance(elector.choose('20 mb'), FileSizeRule) 13 | 14 | def test_rules_elector_base_behavior_with_no_rules(filename_for_test): 15 | file = FileDependencyWrapper([filename_for_test], lock_type='thread+file') 16 | elector = RulesElector(file) 17 | 18 | with pytest.raises(ValueError): 19 | elector.choose('20 milliwatt') 20 | 21 | with pytest.raises(ValueError): 22 | elector.choose('twenty mb') 23 | 24 | with pytest.raises(ValueError): 25 | elector.choose('jnsdvkjnsdfkjncvs') 26 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/test_file_size_rule.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.file_size_rule import FileSizeRule 4 | from polog.handlers.file.file_dependency_wrapper import FileDependencyWrapper 5 | 6 | 7 | class FileDependencyWrapperMock: 8 | def __init__(self, size): 9 | self.size = size 10 | def get_size(self): 11 | return self.size 12 | 13 | 14 | def test_less_or_equal_than_zero_multiplier_in_file_size(filename_for_test): 15 | """ 16 | Проверяем, что нельзя указать отрицательный или нулевой размер файла. 17 | """ 18 | assert FileSizeRule('-12 mb', None).prove_source() == False 19 | assert FileSizeRule('0 mb', None).prove_source() == False 20 | 21 | def test_check_file_size(): 22 | """ 23 | Проверяем, что проверка размера файла проходит корректно. 24 | """ 25 | assert FileSizeRule('12 mb', FileDependencyWrapperMock(12 * 1024 * 1024)).check() == True 26 | assert FileSizeRule('12 mb', FileDependencyWrapperMock(12 * 1024 * 1024 - 1)).check() == False 27 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules_elector.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.file_size_rule import FileSizeRule 2 | 3 | 4 | class RulesElector: 5 | """ 6 | Класс для распознавания правил, представленных в виде строк, и преобразования их в функциональные объекты. 7 | """ 8 | def __init__(self, file, rules=[FileSizeRule]): 9 | self.file = file 10 | self.rules = rules 11 | 12 | def choose(self, source): 13 | """ 14 | Берем строку source и скармливаем ее в конструкторы классов, перечисленных в self.rules. 15 | 16 | Если конкретное правило не узнает себя в переданной строке, метод .prove_source() у него вернет False и мы переходим к следующему правилу. 17 | Если ни одно из правил себя не узнало, поднимется исключение. Это значит, что форматирование исходной строки некорректно. 18 | """ 19 | for rule in self.rules: 20 | rule = rule(source, self.file) 21 | if rule.prove_source(): 22 | return rule 23 | raise ValueError(f'The rule "{source}" is formatted incorrectly. Read the documentation.') 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pomponchik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_read_only_singleton.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | import pytest 4 | 5 | from polog.core.utils.read_only_singleton import ReadOnlySingleton 6 | 7 | 8 | def test_singleton(): 9 | """ 10 | Проверяем, что объект отнаследованного от ReadOnlySingleton класса всегда один. 11 | """ 12 | class ExampleSingleton(ReadOnlySingleton): 13 | pass 14 | assert ExampleSingleton() is ExampleSingleton() 15 | 16 | def test_stress(): 17 | """ 18 | Проверка блокировки создания объекта. 19 | 20 | Несколько потоков одновременно генерируют объекты класса, отнаследованного от ReadOnlySingleton. 21 | Проверяем, что они всегда создают один и тот же объект. 22 | """ 23 | class ExampleSingleton(ReadOnlySingleton): 24 | pass 25 | ids = set() 26 | def target(): 27 | for index in range(1000): 28 | ids.add(id(ExampleSingleton())) 29 | treads = [] 30 | for number in range(5): 31 | tread = Thread(target=target) 32 | tread.daemon = True 33 | treads.append(tread) 34 | tread.start() 35 | for thread in treads: 36 | tread.join() 37 | assert len(ids) == 1 38 | -------------------------------------------------------------------------------- /polog/core/utils/get_traceback.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from polog.core.stores.settings.settings_store import SettingsStore 5 | from polog.utils.json_vars import json_vars 6 | 7 | 8 | store = SettingsStore() 9 | 10 | def get_traceback(cut_string_at_begin=0): 11 | """ 12 | Получаем последний фрейм трейсбека в виде списка строк и сразу сериализуем этот список в json. 13 | """ 14 | try: 15 | json = store['json_module'] 16 | trace = sys.exc_info()[2] 17 | trace_list = traceback.format_tb(trace) 18 | trace_list = trace_list[cut_string_at_begin:] 19 | trace_json = json.dumps(trace_list) 20 | return trace_json 21 | except Exception: 22 | return json.dumps([]) 23 | 24 | def get_locals_from_traceback(): 25 | """ 26 | Забираем из последнего фрейма трейсбека локальные переменные и упаковываем их в json. 27 | """ 28 | trace = sys.exc_info()[2] 29 | try: 30 | try: 31 | local_variables = trace.tb_next.tb_frame.f_locals 32 | except: 33 | local_variables = trace.tb_frame.f_locals 34 | local_variables_json = json_vars(**local_variables) 35 | return local_variables_json 36 | except Exception as e: 37 | return '' 38 | -------------------------------------------------------------------------------- /polog/tests/core/stores/settings/actions/test_set_log_as_built_in.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog import config 4 | from polog.core.stores.settings.actions.set_log_as_built_in import set_log_as_built_in 5 | 6 | 7 | def test_set_log_as_built_in_true(handler): 8 | """ 9 | Проверяем, что если включить данную настройку, функция log() будет доступна как встроенная. 10 | """ 11 | config.set(pool_size=0, level=0, default_level=5) 12 | set_log_as_built_in(False, True, {}) 13 | 14 | log('kek') 15 | 16 | assert handler.last is not None 17 | assert handler.last['message'] == 'kek' 18 | 19 | def test_set_log_as_built_in_false(handler): 20 | """ 21 | Пробуем отключить данную настройку и проверяем, что функция log() перестает быть доступной. 22 | """ 23 | config.set(pool_size=0, level=0, default_level=5) 24 | 25 | set_log_as_built_in(True, False, {}) 26 | with pytest.raises(NameError): 27 | log('kek') 28 | assert handler.last is None 29 | 30 | set_log_as_built_in(False, True, {}) 31 | log('kek') 32 | assert handler.last is not None 33 | handler.clean() 34 | 35 | set_log_as_built_in(True, False, {}) 36 | with pytest.raises(NameError): 37 | log('kek') 38 | assert handler.last is None 39 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/file_size_rule.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.abstract_rule import AbstractRule 2 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.size_token import SizeToken 3 | 4 | 5 | class FileSizeRule(AbstractRule): 6 | """ 7 | Правило для ротации логов в зависимости от размера файла, куда они пишутся. 8 | """ 9 | def prove_source(self): 10 | if not self.tokens.check_regexp('ns'): 11 | return False 12 | if self.tokens['n'][0].content <= 0: 13 | return False 14 | return True 15 | 16 | def check(self): 17 | file_wrapper = self.file 18 | return file_wrapper.get_size() >= self.size_limit 19 | 20 | def extract_data_from_string(self): 21 | """ 22 | Заполняем self.size_limit. 23 | 24 | self.size_limit - это количество байт размера файла, которое нельзя превышать. 25 | Образуется путем перемножения 2-х переменных: количества и размерности. 26 | Скажем, в строке '5 megabytes' количество - это 5, а размерность - количество байт в мегабайте. 27 | """ 28 | number = self.tokens['n'][0].content 29 | dimension = self.tokens['s'][0].content 30 | self.size_limit = number * dimension 31 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/decorator.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from polog.core.utils.signature_matcher import SignatureMatcher 4 | 5 | 6 | class ItIsNotAnActionError(ValueError): 7 | """ 8 | Исключение, которое поднимается в случае, если сигнатура функции не совпадает с ожидаемой от коллбека для настроек. 9 | Предназначено только для использования внутри Polog, оно не должно быть видимым для пользователя. 10 | """ 11 | pass 12 | 13 | def is_action(function): 14 | """ 15 | Декоратор, делающий из обычной функции коллбек, вызываемый при изменении отдельных пунктов настроек. 16 | 17 | Он делает 2 вещи: 18 | 1. Проверяет сигнатуру обернутой функции и поднимает исключение (на этапе инициализации) в случае несовпадения. 19 | 2. Оборачивает вызов коллбека в условие. Коллбек будет вызываться только при условии, что старый пункт настроек и новый - не равны. 20 | """ 21 | @wraps(function) 22 | def wrapper(old_value, new_value, store): 23 | if old_value != new_value: 24 | return function(old_value, new_value, store) 25 | if not SignatureMatcher('.', '.', '.').match(function): 26 | raise ItIsNotAnActionError('The signature of the function you passed does not match the one expected from an action.') 27 | return wrapper 28 | -------------------------------------------------------------------------------- /polog/errors.py: -------------------------------------------------------------------------------- 1 | class IncorrectUseLoggerError(ValueError): 2 | """ 3 | Когда логгер используется неправильно, но пока точно невозможно сказать, в каком контексте. 4 | """ 5 | pass 6 | 7 | class IncorrectUseOfTheDecoratorError(IncorrectUseLoggerError): 8 | """ 9 | Когда в декоратор передали что-то не то. 10 | """ 11 | pass 12 | 13 | class IncorrectUseOfTheContextManagerError(IncorrectUseLoggerError): 14 | """ 15 | Когда неправильно используется контекстный менеджер. 16 | """ 17 | pass 18 | 19 | class DoubleSettingError(ValueError): 20 | """ 21 | Некоторые поля настроек пользователю может быть запрещено изменять дважды. 22 | Если он это делает, поднимается данное исключение. 23 | """ 24 | pass 25 | 26 | class AfterStartSettingError(ValueError): 27 | """ 28 | Поднимается при попытке изменить настройку, которую запрещено изменять после записи первого лога. 29 | """ 30 | pass 31 | 32 | class RewritingLogError(RuntimeError): 33 | """ 34 | Поднимается при попытке одного из обработчиков отредактировать запись лога. 35 | """ 36 | pass 37 | 38 | class HandlerNotFoundError(ValueError): 39 | """ 40 | Поднимается при попытке получить обработчик по ключу, который ранее не был зарегистрирован. 41 | """ 42 | pass 43 | -------------------------------------------------------------------------------- /polog/tests/core/stores/settings/actions/test_decorator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.stores.settings.actions.decorator import is_action, ItIsNotAnActionError 4 | 5 | 6 | def test_not_an_action(): 7 | """ 8 | Пробуем скормить декоратору функции, не подходящие по сигнатуре, чтобы быть коллбеками для настроек. 9 | Должно подниматься ItIsNotAnActionError. 10 | """ 11 | with pytest.raises(ItIsNotAnActionError): 12 | @is_action 13 | def not_action(): 14 | pass 15 | 16 | with pytest.raises(ItIsNotAnActionError): 17 | @is_action 18 | def not_action(lol): 19 | pass 20 | 21 | with pytest.raises(ItIsNotAnActionError): 22 | @is_action 23 | def not_action(lol, kek): 24 | pass 25 | 26 | with pytest.raises(ItIsNotAnActionError): 27 | @is_action 28 | def not_action(lol, kek, cheburek, perekek): 29 | pass 30 | 31 | def test_args_in_action_are_equal(): 32 | """ 33 | Коллбек должен срабатывать только в том случае, когда первый и второй аргументы отличаются. 34 | Проверяем, что это так. 35 | """ 36 | @is_action 37 | def action(old_value, new_value, store): 38 | return 5 39 | 40 | assert action(1, 1, 2) is None 41 | assert action(1, 2, 3) == 5 42 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/singlethreaded/test_engine.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import active_count 3 | 4 | import pytest 5 | 6 | from polog import log 7 | from polog.core.stores.settings.settings_store import SettingsStore 8 | from polog.core.engine.real_engines.singlethreaded.engine import SingleThreadedRealEngine 9 | from polog.core.engine.engine import Engine 10 | 11 | 12 | def test_simple_behavior(handler): 13 | """ 14 | Проверяем, что лог в принципе записывается. 15 | """ 16 | settings = SettingsStore() 17 | settings['max_queue_size'] = 0 18 | settings['pool_size'] = 0 19 | 20 | log('kek') 21 | 22 | assert handler.last is not None 23 | 24 | settings['pool_size'] = 2 25 | 26 | def test_change_engine(handler): 27 | """ 28 | Проверяем, объект движка подменяется при манипуляциях с настройками. 29 | """ 30 | settings = SettingsStore() 31 | engine_wrapper = Engine() 32 | 33 | settings['max_queue_size'] = 0 34 | settings['pool_size'] = 2 35 | 36 | log('kek') 37 | 38 | assert not isinstance(engine_wrapper.real_engine, SingleThreadedRealEngine) 39 | 40 | settings['pool_size'] = 0 41 | 42 | assert isinstance(engine_wrapper.real_engine, SingleThreadedRealEngine) 43 | 44 | settings['pool_size'] = 2 45 | 46 | assert not isinstance(engine_wrapper.real_engine, SingleThreadedRealEngine) 47 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_exception_escaping.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from polog.core.utils.exception_escaping import exception_escaping 6 | 7 | 8 | def test_base_escaping(): 9 | """ 10 | Проверяем, что экранирование работает на обычной функции. 11 | """ 12 | @exception_escaping 13 | def lol(): 14 | raise ValueError('kek') 15 | 16 | lol() 17 | 18 | def test_async_escaping(): 19 | """ 20 | Проверяем, что экранирование работает на корутинной функции. 21 | """ 22 | @exception_escaping 23 | async def lol(): 24 | raise ValueError('kek') 25 | 26 | asyncio.run(lol()) 27 | 28 | def test_base_behaviour(): 29 | """ 30 | Проверяем, что экранирование не нарушает работу задекорированной функции. 31 | """ 32 | cheburek = 0 33 | 34 | @exception_escaping 35 | def lol(): 36 | nonlocal cheburek 37 | cheburek = 1 38 | raise ValueError('kek') 39 | 40 | lol() 41 | assert cheburek == 1 42 | 43 | def test_async_behaviour(): 44 | """ 45 | Проверяем, что экранирование не нарушает работу задекорированной корутинной функции. 46 | """ 47 | cheburek = 0 48 | 49 | @exception_escaping 50 | async def lol(): 51 | nonlocal cheburek 52 | cheburek = 1 53 | raise ValueError('kek') 54 | 55 | asyncio.run(lol()) 56 | assert cheburek == 1 57 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/locks/test_abstract_single_lock.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.locks.abstract_single_lock import AbstractSingleLock 4 | 5 | 6 | class LittleLessAbstractLock(AbstractSingleLock): 7 | def __init__(self, on): 8 | if not on: 9 | self.off() 10 | 11 | def test_off_lock_is_working(): 12 | """ 13 | Проверяем, что метод .off() - работает. 14 | Его задача - переопределять методы взятия и освобождения блокировки "болванками". 15 | """ 16 | # Ничего не происходит, блокировка отключена за счет перегрузки метода. 17 | LittleLessAbstractLock(False).acquire() 18 | LittleLessAbstractLock(False).release() 19 | 20 | with pytest.raises(NotImplementedError): 21 | # Блокировка НЕ отключена, срабатывает метод, который должен быть переопределен в "нормальных" наследниках, поднимающий исключение. 22 | LittleLessAbstractLock(True).acquire() 23 | 24 | with pytest.raises(NotImplementedError): 25 | # Блокировка НЕ отключена, срабатывает метод, который должен быть переопределен в "нормальных" наследниках, поднимающий исключение. 26 | LittleLessAbstractLock(True).release() 27 | 28 | def test_active_flag_is_working_for_abstract(): 29 | """ 30 | Проверяем, что атрибут .active проставляется правильно. 31 | """ 32 | assert LittleLessAbstractLock(True).active == True 33 | assert LittleLessAbstractLock(False).active == False 34 | -------------------------------------------------------------------------------- /polog/utils/json_vars.py: -------------------------------------------------------------------------------- 1 | from polog.core.stores.settings.settings_store import SettingsStore 2 | 3 | 4 | # Простые типы, имеющие соответствия в стандарте json. Если передается объект другого типа, он приводится к str. 5 | BASE_JSON_TYPES = (bool, int, float, str) 6 | store = SettingsStore() 7 | 8 | def get_item(item): 9 | for one in BASE_JSON_TYPES: 10 | if isinstance(item, one): 11 | return {'value': item, 'type': one.__name__} 12 | return {'value': str(item), 'type': type(item).__name__} 13 | 14 | def json_vars(*args, **kwargs): 15 | """ 16 | Преобразуем любые аргументы в json-объект. Каждый элемент в json-объекте сопровождается названием типа данных. Это полезно в случаях, когда тип данных не соответствует стандартным для json. Все нестандартные объекты приводятся к типу str. 17 | Функция не поддерживает глубокую рекурсию. 18 | """ 19 | json = store['json_module'] 20 | if not (len(args) + len(kwargs)): 21 | return None 22 | args = [get_item(x) for x in args] 23 | kwargs = {key: get_item(value) for key, value in kwargs.items()} 24 | result = {} 25 | if len(args): 26 | result['args'] = args 27 | if len(kwargs): 28 | result['kwargs'] = kwargs 29 | return json.dumps(result) 30 | 31 | def json_one_variable(variable): 32 | json = store['json_module'] 33 | variable = get_item(variable) 34 | result = json.dumps(variable) 35 | return result 36 | -------------------------------------------------------------------------------- /polog/tests/handlers/smtp/test_sender.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from polog.handlers.smtp.sender import SMTP_sender 6 | from polog import handle_log as log, config 7 | 8 | 9 | lst = [] 10 | 11 | class DependencyWrapper: 12 | def __init__(self, v1, v2, v3, v4, v5): 13 | pass 14 | 15 | def send(self, message): 16 | lst.append(message) 17 | 18 | sender = SMTP_sender('fff', 'fff', 'fff', 'fff', smtp_wrapper=DependencyWrapper) 19 | 20 | 21 | def test_send_normal(): 22 | """ 23 | Проверяем, что что-то проходит через обработчик в DependencyWrapper. 24 | """ 25 | config.add_handlers(sender) 26 | 27 | log('hello') 28 | time.sleep(0.0001) 29 | assert lst[0] 30 | lst.pop() 31 | 32 | config.delete_handlers(sender) 33 | 34 | def test_send_error(): 35 | """ 36 | Проверка, что при исключении тоже что-то приходит в обработчик. 37 | """ 38 | config.add_handlers(test_send_error=sender) 39 | 40 | log('hello', exception=ValueError()) 41 | time.sleep(0.0001) 42 | assert lst[0] 43 | lst.pop() 44 | 45 | config.delete_handlers(sender) 46 | 47 | def test_repr(): 48 | """ 49 | Проверяем, что метод .__repr__ обработчика подчиняется заданному формату отображения. 50 | """ 51 | assert repr(sender) == 'SMTP_sender(email_from="fff", password=, smtp_server="fff", email_to="fff", port=465, text_assembler=None, subject_assembler=None, alt=None)' 52 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Test-Package 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [macos-latest, ubuntu-latest] 13 | python-version: ['3.9', '3.10'] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | 23 | - name: Install dependencies 24 | shell: bash 25 | run: pip install pytest==7.1.2 26 | 27 | - name: Install dependencies 2 28 | shell: bash 29 | run: pip install pytest-cov==4.0.0 30 | 31 | - name: Install dependencies 2 32 | shell: bash 33 | run: pip install termcolor==2.1.0 34 | 35 | - name: Install dependencies 2 36 | shell: bash 37 | run: pip install ujson==5.5.0 38 | 39 | - name: Run tests and show coverage on the command line 40 | run: coverage run --source=polog --omit="*tests*" -m pytest --cache-clear && coverage report -m 41 | 42 | - name: Upload reports to codecov 43 | env: 44 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 45 | if: runner.os == 'Linux' 46 | run: | 47 | curl -Os https://uploader.codecov.io/latest/linux/codecov 48 | find . -iregex "codecov.*" 49 | chmod +x codecov 50 | ./codecov -t ${CODECOV_TOKEN} 51 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_cut_traceback.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from polog.core.utils.cut_traceback import cut_traceback 6 | 7 | 8 | def test_import_testcapi(): 9 | import _testcapi 10 | 11 | 12 | def test_compare_counters_traceback_cutting_on_and_off(): 13 | """ 14 | Проверяем, что обрезание трейсбека действительно работает, то есть трейсбек становится меньше, чем если обрезание не делать. 15 | Также проверяем, что при настройке 'traceback_cutting' в положении False длина трейсбека аналогична тому, что обрезание не запускалось бы вовсе. 16 | """ 17 | try: 18 | raise ValueError 19 | except: 20 | cut_traceback({'traceback_cutting': True}) 21 | _, _, tb = sys.exc_info() 22 | counter_1 = 0 23 | while tb: 24 | counter_1 += 1 25 | tb = tb.tb_next 26 | 27 | try: 28 | raise ValueError 29 | except: 30 | cut_traceback({'traceback_cutting': False}) 31 | _, _, tb = sys.exc_info() 32 | counter_2 = 0 33 | while tb: 34 | counter_2 += 1 35 | tb = tb.tb_next 36 | 37 | try: 38 | raise ValueError 39 | except: 40 | _, _, tb = sys.exc_info() 41 | counter_3 = 0 42 | while tb: 43 | counter_3 += 1 44 | tb = tb.tb_next 45 | 46 | assert counter_1 < counter_2 47 | assert counter_1 < counter_3 48 | assert counter_2 == counter_3 49 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/test_number_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.number_token import NumberToken 4 | 5 | 6 | def test_content_extraction_for_number_token(): 7 | """ 8 | Проверяем, что значение из строки извлекается корректно. 9 | """ 10 | assert NumberToken('20').content == 20 11 | assert NumberToken('0').content == 0 12 | assert NumberToken('-0').content == 0 13 | assert NumberToken('-1000').content == -1000 14 | assert NumberToken('34340').content == 34340 15 | 16 | def test_equal_to_number_token(): 17 | """ 18 | Проверяем, что проверка на равенство универсального токена работает корректно. 19 | """ 20 | assert NumberToken('20') == NumberToken('20') 21 | assert NumberToken('0') == NumberToken('0') 22 | assert NumberToken('-0') == NumberToken('0') 23 | 24 | assert NumberToken('20') != NumberToken('21') 25 | assert NumberToken('20') != NumberToken('-20') 26 | 27 | def test_str_representation_of_number_token(): 28 | """ 29 | Пробуем преобразовать токен в строку при помощи str(), должно срабатывать. 30 | """ 31 | assert str(NumberToken('20')) == 'NumberToken(20)' 32 | 33 | def test_creating_number_token_with_error(): 34 | """ 35 | Проверяем, что, если строка не является числом, токен не создастся. 36 | """ 37 | with pytest.raises(ValueError): 38 | NumberToken('kek') 39 | -------------------------------------------------------------------------------- /polog/core/utils/get_errors_level.py: -------------------------------------------------------------------------------- 1 | from polog.core.stores.settings.settings_store import SettingsStore 2 | from polog.core.stores.levels import Levels 3 | 4 | 5 | def get_errors_level(error_level_in_function, simple_level_in_function): 6 | """ 7 | Ошибка по умолчанию логируется с уровнем, который берется из глобальных настроек логгера, однако локально в конкретном декораторе это можно изменить. 8 | 9 | В данной функции мы смотрим, определен ли уровень логирования ошибок в конкретном декораторе. Если определен - используем его. 10 | Если нет - берем максимальный из трех уровней: дефолтный уровень для всех логов ('default_level' в настройках), дефолтный уровень для ошибок ('default_error_level') и уровень для всех событий, переданный в декоратор. 11 | 12 | Если в декоратор передано какое-то имя уровня, и оно не зарегистрировано в Polog ранее, оно игнорируется (декоратор ведет себя так, будто оно не передано). 13 | """ 14 | if error_level_in_function is not None: 15 | try: 16 | return Levels.get(error_level_in_function) 17 | except (KeyError, ValueError): 18 | pass 19 | 20 | store = SettingsStore() 21 | 22 | levels_to_choose = [store['default_level'], store['default_error_level']] 23 | if simple_level_in_function is not None: 24 | try: 25 | levels_to_choose.append(Levels.get(simple_level_in_function)) 26 | except (KeyError, ValueError): 27 | pass 28 | 29 | return max(levels_to_choose) 30 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_get_traceback.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | 4 | import pytest 5 | 6 | from polog.core.utils.get_traceback import get_traceback, get_locals_from_traceback 7 | from polog import json_vars, config 8 | 9 | 10 | non_local = [] 11 | 12 | def test_locals_full(): 13 | """ 14 | Проверяем, что локальные переменные из исключения извлекаются и представлены в стандартной схеме json. 15 | """ 16 | a = 5 17 | b = 'lol' 18 | try: 19 | c = 5 / 0 20 | except: 21 | non_local.append(get_locals_from_traceback()) 22 | non_local.append(json_vars(**locals())) 23 | assert non_local[0] == non_local[1] 24 | non_local.pop() 25 | non_local.pop() 26 | 27 | def test_get_traceback(): 28 | """ 29 | Проверяем, что извлеченный трейсбэк не пустой, то есть содержит хотя бы 1 символ. 30 | Содержимое трейсбэка здесь не проверяется. 31 | """ 32 | a = 5 33 | b = 'lol' 34 | try: 35 | c = 5 / 0 36 | except: 37 | assert len(get_traceback()) 38 | 39 | def test_get_traceback_json_format(): 40 | """ 41 | Проверяем, что извлеченный трейсбэк корректно декодируется из формата json, то есть изначально в нем представлен. 42 | """ 43 | a = 5 44 | b = 'lol' 45 | try: 46 | c = 5 / 0 47 | except: 48 | try: 49 | trace = get_traceback() 50 | trace = json.loads(trace) 51 | assert True 52 | except: 53 | assert False 54 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/abstractions/test_meta_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.abstractions.meta_token import MetaToken 4 | 5 | 6 | def test_raise_errors_in_metatoken(): 7 | """ 8 | Пробуем создавать "неправильные" классы на основе метакласса. Должны подниматься исключения. 9 | """ 10 | with pytest.raises(AttributeError): 11 | class TestToken(metaclass=MetaToken): 12 | regexp_letter = '[' 13 | 14 | with pytest.raises(AttributeError): 15 | class TestToken(metaclass=MetaToken): 16 | regexp_letter = ']' 17 | 18 | with pytest.raises(AttributeError): 19 | class TestToken(metaclass=MetaToken): 20 | regexp_letter = '*' 21 | 22 | with pytest.raises(AttributeError): 23 | class TestToken(metaclass=MetaToken): 24 | regexp_letter = 'lol' 25 | 26 | with pytest.raises(AttributeError): 27 | class TestToken(metaclass=MetaToken): 28 | pass 29 | 30 | with pytest.raises(AttributeError): 31 | class TestToken(metaclass=MetaToken): 32 | regexp_letter = True 33 | 34 | for letter in 'nsd': 35 | with pytest.raises(AttributeError): 36 | class TestToken(metaclass=MetaToken): 37 | regexp_letter = letter 38 | 39 | with pytest.raises(AttributeError): 40 | class AbstractToken(metaclass=MetaToken): 41 | regexp_letter = 'k' 42 | -------------------------------------------------------------------------------- /polog/handlers/file/locks/abstract_single_lock.py: -------------------------------------------------------------------------------- 1 | class AbstractSingleLock: 2 | """ 3 | Все единичные классы блокировок, унаследованные от данного класса: 4 | 1. Должны переопределить методы .acquire() и .release(). 5 | 2. Являются отключаемыми. То есть, после вызова метода .off() у их экземпляров они просто не работают. 6 | """ 7 | active = True 8 | 9 | def off(self): 10 | """ 11 | Отключение блокировки. 12 | 13 | После вызова данного метода методы .acquire() и .release() перестают работать. Откатить это нельзя, операция одноразовая, поэтому рекомендуется применять при инициализации экземпляра отнаследованного класса. 14 | """ 15 | self.acquire = self.empty_acquire 16 | self.release = self.empty_release 17 | self.active = False 18 | 19 | def acquire(self): 20 | """ 21 | Взять лок. 22 | 23 | Должно быть переопределено наследником. 24 | """ 25 | raise NotImplementedError('The basic action for the blocking class is not spelled out.') 26 | 27 | def release(self): 28 | """ 29 | Отпустить лок. 30 | 31 | Должно быть переопределено наследником. 32 | """ 33 | raise NotImplementedError('The basic action for the blocking class is not spelled out.') 34 | 35 | def empty_acquire(self): 36 | """ 37 | Сделать вид, что взял лок. 38 | """ 39 | pass 40 | 41 | def empty_release(self): 42 | """ 43 | Сделать вид, что отпустил лок. 44 | """ 45 | pass 46 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/test_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.parser import Parser 4 | from polog.handlers.file.file_dependency_wrapper import FileDependencyWrapper 5 | 6 | 7 | class RulesElectorMock: 8 | def __init__(self, file): 9 | pass 10 | def choose(self, source): 11 | return source 12 | 13 | def test_parse_single_rule_with_mock(filename_for_test): 14 | """ 15 | Проверяем, что содержимое строки передавалось в RulesElectorMock. 16 | """ 17 | file = FileDependencyWrapper([filename_for_test], lock_type='thread+file') 18 | parser = Parser(file, elector=RulesElectorMock) 19 | 20 | assert parser.extract_rules('20 mb ') == ['20 mb'] 21 | assert parser.extract_rules(',20 mb ,') == ['20 mb'] 22 | 23 | def test_parse_multiple_rules_with_mock(filename_for_test): 24 | """ 25 | Проверяем, что строка корректно сплитится. 26 | """ 27 | file = FileDependencyWrapper([filename_for_test], lock_type='thread+file') 28 | parser = Parser(file, elector=RulesElectorMock) 29 | 30 | assert parser.extract_rules('20 mb, 30 mb ') == ['20 mb', '30 mb'] 31 | assert parser.extract_rules('20 mb; 30 mb ') == ['20 mb', '30 mb'] 32 | 33 | assert parser.extract_rules(' 20 mb ; 30 mb ') == ['20 mb', '30 mb'] 34 | assert parser.extract_rules(' 20 mb , 30 mb ') == ['20 mb', '30 mb'] 35 | 36 | assert parser.extract_rules(' 20 mb , 30 mb , ') == ['20 mb', '30 mb'] 37 | assert parser.extract_rules(',,,, 20 mb , 30 mb , ') == ['20 mb', '30 mb'] 38 | -------------------------------------------------------------------------------- /polog/handlers/memory/saver.py: -------------------------------------------------------------------------------- 1 | from threading import Lock, BoundedSemaphore 2 | 3 | from polog.handlers.abstract.base import BaseHandler 4 | from polog.core.utils.read_only_singleton import ReadOnlySingleton 5 | 6 | 7 | class memory_saver(ReadOnlySingleton, BaseHandler): 8 | """ 9 | Класс-заглушка обработчика для тестов. 10 | Подключается как обычный обработчик, но никуда не записывает и не отправляет логи, а только сохраняет их в оперативной памяти. 11 | Сохраненные данным классом логи - экземпляры LogItem. 12 | Последний лог всегда в атрибуте 'last'. Все логи - в атрибуте 'all'. 13 | """ 14 | last = None 15 | all_semaphore = BoundedSemaphore(value=1) 16 | 17 | def __init__(self): 18 | with Lock(): 19 | if not hasattr(self, 'inited'): 20 | super().__init__() 21 | self.all = [] 22 | self.inited = True 23 | 24 | def __call__(self, log): 25 | """ 26 | При вызове экземпляра класса, обновляем информацию о последнем логе, и добавляем новый лог в список со всеми логами. 27 | """ 28 | with Lock(): 29 | with self.all_semaphore: 30 | self.all.append(log) 31 | self.last = log 32 | 33 | def clean(self): 34 | """ 35 | Очистка старых записей. 36 | Семафор используется, чтобы случайно не очистить список старых логов в тот момент, когда в него идет запись из другого потока. 37 | """ 38 | with self.all_semaphore: 39 | self.all = [] 40 | self.last = None 41 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/abstract.py: -------------------------------------------------------------------------------- 1 | class AbstractRealEngine: 2 | """ 3 | "Движок" - это некоторый объект, ответственный за вызов обработчиков при наступлении логируемого события. 4 | 5 | Реализаций движков может быть несколько, с акцентами на разные характеристики. 6 | К примеру, асинхронные движки обеспечивают бОльшую производительность ценой отсутствия гарантий правильного порядка записи логов, а также большей сложности их внутренней реализации. Также движки могут помимо базовой функциональности делать что-то еще. К примеру, можно написать мета-движок, который мог бы автоматически переключаться между разными типами движков, опираясь на накопленную статистику их производительности. 7 | 8 | Каждый движок вне зависимости от реализации должен поддерживать два метода: 9 | write() - передача информации о событии обработчикам. 10 | stop() - освобождение любых выделенных движком ресурсов. 11 | """ 12 | 13 | def __init__(self, settings): 14 | self.settings = settings 15 | 16 | def write(self, log_item): 17 | """ 18 | Запись лога. 19 | 20 | Ожидается, что здесь будет логика, ответственная за вызов всех обработчиков. 21 | """ 22 | raise NotImplementedError # pragma: no cover 23 | 24 | def stop(self): 25 | """ 26 | Остановка движка. 27 | 28 | Скажем, если движок использовал дополнительные потоки или процессы для своей работы, какие-нибудь файловые дескрипторы - все эти ресурсы должны быть освобождены при вызове данного метода. 29 | """ 30 | raise NotImplementedError 31 | -------------------------------------------------------------------------------- /polog/data_structures/trees/named_tree/projector.py: -------------------------------------------------------------------------------- 1 | from polog.data_structures.trees.named_tree.tree import NamedTree 2 | 3 | 4 | class TreeProjector: 5 | """ 6 | Объект данного класса порождает новые деревья на базе других деревьев. Как бы "проецирует" их. 7 | 8 | Проекция осуществляется не дерева целиком, а отдельных его "веток". 9 | 10 | Важно: операции вставки в получившееся по итогу дерево могут аффектить также и исходное дерево. Ноды не копируются, а перемещаются из дерева в дерево. 11 | """ 12 | def __init__(self, other_tree): 13 | if not isinstance(other_tree, NamedTree): 14 | raise ValueError('An instance of the NamedTree was expected to be passed as an argument.') 15 | self.tree = other_tree 16 | 17 | def on(self, paths, not_to_root=True): 18 | """ 19 | Генерируем новое дерево с копированием избранных веток из старого. 20 | 21 | paths - список путей к нодам из старого дерева (self.tree), которые должны быть скопированы в новое. 22 | not_to_root (bool) - флаг, означающий, что нельзя подменять дерево целиком. 23 | """ 24 | new_tree = NamedTree() 25 | 26 | for path in paths: 27 | if path == self.tree.keys_separator and not_to_root: 28 | raise ValueError('Dangerous intersection of different versions of trees. This happens when trying to insert a node into the root of the tree.') 29 | node_to_put = self.tree.search_or_create_node(self.tree.get_converted_keys(path)) 30 | new_tree = new_tree.put_node(path, node_to_put) 31 | 32 | return new_tree 33 | -------------------------------------------------------------------------------- /polog/tests/data_structures/trees/named_tree/test_printer.py: -------------------------------------------------------------------------------- 1 | from sys import getrecursionlimit 2 | 3 | import pytest 4 | 5 | from polog.data_structures.trees.named_tree.printer import TreePrinter 6 | from polog.data_structures.trees.named_tree.tree import NamedTree 7 | 8 | 9 | def test_recursion_limit(): 10 | """ 11 | Проверяем, что при слишком глубоком дереве не поднимается RecursionError, а все же возвращается строковое представление объекта (болванка без данных). 12 | """ 13 | tree = NamedTree() 14 | 15 | for number in range(getrecursionlimit() + 1): 16 | node_name = ('k.' * (number + 1))[:-1] 17 | tree[node_name] = 'kek' 18 | 19 | assert str(tree) == '' 20 | 21 | def test_base_behavior(): 22 | """ 23 | Проверка основных параметров строкового представления дерева. 24 | """ 25 | tree = NamedTree() 26 | 27 | tree['lol'] = 'kek' 28 | tree['kek'] = 'cheburek' 29 | tree['kek.pek'] = 'berebek' 30 | 31 | representation = str(tree) 32 | 33 | assert type(representation) is str 34 | assert representation.startswith('') 36 | 37 | assert len(tree) + 2 == len(representation.split('\n')) 38 | 39 | assert 'lol' in representation 40 | assert 'kek' in representation 41 | assert 'pek' in representation 42 | 43 | def test_print_empty_tree(): 44 | """ 45 | Проверка, что если в дереве нет ни одного сохраненного значения, оно распечатывается в виде "болванки". 46 | """ 47 | tree = NamedTree() 48 | 49 | assert str(tree) == '' 50 | -------------------------------------------------------------------------------- /polog/handlers/smtp/smtp_dependency_wrapper.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | 3 | 4 | class SMTPDependencyWrapper: 5 | """ 6 | Политика отправки сообщений в Polog отделена от реализации. 7 | Данный класс представляет реализацию, т. е. низкоуровневые методы для работы с SMTP-протоколом. 8 | Это необходимо, чтобы класс с политикой был тестируемым. 9 | """ 10 | def __init__(self, server, port, email_from, password, email_to): 11 | self.server = server 12 | self.port = port 13 | self.email_from = email_from 14 | self.password = password 15 | self.email_to = email_to 16 | 17 | def send(self, message): 18 | """ 19 | Обертка для отправки сообщения. 20 | При каждой отправке сообщения объект соединения с сервером создается заново. 21 | """ 22 | self.create_smtp_server() 23 | self.send_mail(message) 24 | self.quit_from_server() 25 | 26 | def create_smtp_server(self): 27 | """ 28 | Создание объекта SMTP-сервера и логин. 29 | """ 30 | self._server = smtplib.SMTP_SSL(self.server, self.port) 31 | self._server.login(self.email_from, self.password) 32 | 33 | def send_mail(self, message): 34 | """ 35 | Отправляем сообщение. 36 | """ 37 | self._server.sendmail(self.email_from, [self.email_to], message.as_string()) 38 | 39 | def quit_from_server(self): 40 | """ 41 | Разлогиниваемся на сервере. 42 | """ 43 | if hasattr(self, '_server'): 44 | try: 45 | self._server.quit() 46 | except: 47 | pass 48 | -------------------------------------------------------------------------------- /polog/tests/data_structures/trees/named_tree/test_projector.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.data_structures.trees.named_tree.tree import NamedTree 4 | from polog.data_structures.trees.named_tree.projector import TreeProjector 5 | 6 | 7 | def test_project_tree(): 8 | """ 9 | Проверяем, что новое дерево создается, не является старым деревом, в нем доступны ноды, которые указано спроецировать, и не доступны остальные. 10 | """ 11 | tree = NamedTree() 12 | projector = TreeProjector(tree) 13 | 14 | tree['lol'] = 1 15 | tree['lol.kek'] = 2 16 | tree['lol.kek.cheburek'] = 3 17 | 18 | new_tree = projector.on(['lol.kek']) 19 | 20 | assert tree is not new_tree 21 | 22 | assert tree['lol.kek'] == new_tree['lol.kek'] 23 | assert tree['lol.kek.cheburek'] == new_tree['lol.kek.cheburek'] 24 | 25 | assert new_tree.get('lol') is None 26 | 27 | def test_project_tree_of_root(): 28 | """ 29 | Пробуем проецировать дерево целиком. 30 | """ 31 | tree = NamedTree() 32 | tree['lol'] = 1 33 | 34 | projector = TreeProjector(tree) 35 | with pytest.raises(ValueError): 36 | new_tree = projector.on(['.']) 37 | 38 | projector = TreeProjector(tree) 39 | new_tree = projector.on(['.'], not_to_root=False) 40 | assert new_tree['lol'] == tree['lol'] 41 | 42 | def test_not_tree_passed_to_projector_init(): 43 | """ 44 | Пробуем передать в конструктор не дерево, а что-то еще. 45 | """ 46 | with pytest.raises(ValueError): 47 | projector = TreeProjector(1) 48 | with pytest.raises(ValueError): 49 | projector = TreeProjector('kek') 50 | with pytest.raises(ValueError): 51 | projector = TreeProjector([1, 2, 3]) 52 | -------------------------------------------------------------------------------- /polog/data_structures/trees/named_tree/printer.py: -------------------------------------------------------------------------------- 1 | class TreePrinter: 2 | """ 3 | Класс, инкапсулирующий логику, связанную со строковым представлением дерева. 4 | """ 5 | def __init__(self, tree, filler='\t'): 6 | self.tree = tree 7 | self.filler = filler 8 | 9 | def get_indented_representation(self): 10 | """ 11 | Метод возвращает строковое представление дерева. 12 | 13 | Ноды дерева получаются путем обхода в глубину, соответствующим образом формируется и их порядок. 14 | 15 | Поскольку обход в глубину реализован через рекурсию, при превышении максимально допустимой глубины возвращается "болванка". 16 | То же самое происходит в случае, если все ноды дерева - пустые. 17 | 18 | Глубина расположения той или иной ноды дерева показана ее отступом от левого края. 19 | """ 20 | data = [] 21 | if not len(self.tree): 22 | return f'<{type(self.tree).__name__} empty object>' 23 | try: 24 | for node in self.tree.walker.dfs(): 25 | full_name = node.get_full_name() 26 | indent = full_name.count(self.tree.keys_separator) + 1 if full_name != self.tree.keys_separator else 0 27 | filler = self.filler * indent 28 | name = node.name if node.name is not None else self.tree.keys_separator 29 | full_string = f'{filler}|\033[4m{name}\033[0m' 30 | data.append(full_string) 31 | except RecursionError: 32 | return f'<{type(self.tree).__name__} very big object>' 33 | indented_representation = '\n'.join(data) 34 | full_representation = f'<{type(self.tree).__name__}:\n{indented_representation}>' 35 | return full_representation 36 | -------------------------------------------------------------------------------- /polog/data_structures/trees/named_tree/walker.py: -------------------------------------------------------------------------------- 1 | class TreeWalker: 2 | """ 3 | Объекты этого класса инкапсулируют логику обходов по дереву. 4 | """ 5 | def __init__(self, tree): 6 | self.tree = tree 7 | 8 | def bfs(self): 9 | """ 10 | Функция-генератор для обхода дерева в ширину (BFS). 11 | 12 | На каждой итерации возвращает ноду. Ноды не проверяются на пустоту, возвращаются "как есть". 13 | """ 14 | layer = [self.tree] 15 | while layer: 16 | next_layer = [] 17 | for node in layer: 18 | yield node 19 | for child in node.childs.values(): 20 | next_layer.append(child) 21 | layer = next_layer 22 | 23 | def bfs_values(self): 24 | """ 25 | Функция-генератор для обхода дерева в ширину (BFS). 26 | 27 | На каждой итерации возвращает значение, хранящееся в ноде. Пустые ноды игнорируются. 28 | """ 29 | for node in self.bfs(): 30 | if node.value is not None: 31 | yield node.value 32 | 33 | def dfs(self): 34 | """ 35 | Функция-генератор для обхода дерева в глубину (DFS). 36 | 37 | На каждой итерации возвращает ноду. Ноды не проверяются на пустоту, возвращаются "как есть". 38 | """ 39 | yield from self._recursive_dfs(self.tree) 40 | 41 | def _recursive_dfs(self, node): 42 | """ 43 | Вспомогательный метод для реализации обхода в глубину. 44 | 45 | Поскольку обход реализован через рекурсию, его глубина ограничена и при ее превышении поднимется RecursionError. 46 | """ 47 | yield node 48 | for child in node.childs.values(): 49 | yield from self._recursive_dfs(child) 50 | -------------------------------------------------------------------------------- /polog/tests/loggers/handle/test_abstract.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from polog.loggers.handle.abstract import AbstractHandleLogger 6 | from polog import config 7 | 8 | 9 | class HandleLogger(AbstractHandleLogger): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self.logs_store = [] 13 | 14 | def _push(self, fields): 15 | self.logs_store.append({**fields}) 16 | 17 | def _specific_processing(self, fields): 18 | fields['lol'] = 'kek' 19 | 20 | 21 | def test_inherit_is_working(): 22 | """ 23 | Проверяем, что у отнаследованного от базового класса класса работает базовый механизм. 24 | А именно, что вызывается сначала ._specific_processing(), затем ._push(), которые были переопределены у наследника. 25 | """ 26 | logger = HandleLogger() 27 | logger('kek') 28 | time.sleep(0.0001) 29 | log = logger.logs_store[0] 30 | assert log['message'] == 'kek' 31 | assert log['lol'] == 'kek' 32 | 33 | def test_hidden_fields(): 34 | """ 35 | Проверяем, что у ручного логгера нет ни одного поля, не начинающегося на "_" (за исключением того, что мы сами определили уже в наследнике базового класса). 36 | """ 37 | logger = HandleLogger() 38 | for name in dir(logger): 39 | if name != 'logs_store': 40 | assert name.startswith('_') 41 | 42 | def test_maybe_raise(): 43 | """ 44 | Проверяем, что внутренние исключения поднимаются в зависимости от установленных настроек. 45 | """ 46 | config.set(silent_internal_exceptions=False) 47 | with pytest.raises(ValueError): 48 | HandleLogger._maybe_raise(ValueError, 'kek') 49 | config.set(silent_internal_exceptions=True) 50 | HandleLogger._maybe_raise(ValueError, 'kek') 51 | -------------------------------------------------------------------------------- /polog/tests/utils/test_json_vars.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from polog.utils.json_vars import json_vars, get_item 6 | 7 | 8 | class NotBaseType: 9 | def __repr__(self): 10 | return 'hello' 11 | 12 | def test_base(): 13 | """ 14 | Проверка базовой функциональности json_vars(). 15 | """ 16 | to_compare = json_vars(1, 2, 3, 'test', test='lol') 17 | to_compare_2 = json.dumps({'args': [{'value': 1, 'type': 'int'}, {'value': 2, 'type': 'int'}, {'value': 3, 'type': 'int'}, {'value': 'test', 'type': 'str'}], 'kwargs': {'test': {'value': 'lol', 'type': 'str'}}}) 18 | to_compare = json.loads(to_compare) 19 | to_compare_2 = json.loads(to_compare_2) 20 | assert to_compare == to_compare_2 21 | 22 | def test_base_bool(): 23 | """ 24 | Проверка, что json_vars() корректно отрабатывает с типом bool. 25 | """ 26 | to_compare = json_vars(False) 27 | to_compare_2 = json.dumps({'args': [{'value': False, 'type': 'bool'}]}) 28 | to_compare = json.loads(to_compare) 29 | to_compare_2 = json.loads(to_compare_2) 30 | assert to_compare == to_compare_2 31 | 32 | def test_get_item_base_types(): 33 | """ 34 | Проверка, что get_item() корректно отрабатывает с типами данных, стандартными для формата json. 35 | """ 36 | assert get_item(1) == {'value': 1, 'type': 'int'} 37 | assert get_item(0.25) == {'value': 0.25, 'type': 'float'} 38 | assert get_item(False) == {'value': False, 'type': 'bool'} 39 | assert get_item('hello') == {'value': 'hello', 'type': 'str'} 40 | 41 | def test_get_item_non_base_types(): 42 | """ 43 | Проверка, что get_item() корректно отрабатывает с типами данных, НЕ стандартными для формата json. 44 | """ 45 | assert get_item(NotBaseType()) == {'value': 'hello', 'type': 'NotBaseType'} 46 | -------------------------------------------------------------------------------- /polog/data_structures/wrappers/fields_container/container.py: -------------------------------------------------------------------------------- 1 | class FieldsContainer: 2 | """ 3 | Для конкретного лога набор дополнительных извлекаемых полей задается индивидуально, в зависимости от: 4 | 5 | 1. Локального набора полей (например, заданного в декораторе). 6 | 2. Глобального (дефолтного) набора полей. 7 | 8 | При пересечении имен полей, предпочтение должно отдаваться более локальному варианту. 9 | При добавлении новых полей в словарь с дефолтами, они должны "добавляться" и сюда, то есть "под капотом" обход делается всегда по переданным изначально экземплярам словарей (то есть они не копируются). 10 | 11 | Вызывающий код не должен знать всех этих нюансов. Ему дается интерфейс итератора, работающего как словарь, но под капотом объединяющего несколько словарей. 12 | """ 13 | def __init__(self, fields, defaults): 14 | self.fields = fields 15 | self.defaults = defaults 16 | 17 | def __iter__(self): 18 | """ 19 | Аналог итератора по ключам словаря, только по ключам двух словарей сразу. 20 | 21 | При пересечении множеств ключей двух словарей, они не дублируются. 22 | """ 23 | for key in self.fields: 24 | yield key 25 | 26 | for key in self.defaults: 27 | if key not in self.fields: 28 | yield key 29 | 30 | def items(self): 31 | """ 32 | Аналог dict.items(), итерирующийся по 2-м словарям. 33 | 34 | При пересечении множеств ключей двух словарей, предпочтение отдается значению из первого словаря. 35 | """ 36 | for key, value in self.fields.items(): 37 | yield key, value 38 | 39 | for key, value in self.defaults.items(): 40 | if key not in self.fields: 41 | yield key, value 42 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/locks/test_thread_lock.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread 3 | 4 | import pytest 5 | 6 | from polog.handlers.file.locks.thread_lock import ThreadLock 7 | 8 | 9 | def test_threads_race_condition(): 10 | """ 11 | Проверяем, что если лок включен - он работает. 12 | """ 13 | iterations = 500000 14 | number_of_threads = 4 15 | lock = ThreadLock(on=True) 16 | 17 | index = 0 18 | def incrementer(iterations): 19 | nonlocal index 20 | for _ in range(iterations): 21 | lock.acquire() 22 | index += 1 23 | lock.release() 24 | 25 | threads = [Thread(target=incrementer, args=(iterations, )) for thread_index in range(number_of_threads)] 26 | for thread in threads: 27 | thread.start() 28 | for thread in threads: 29 | thread.join() 30 | 31 | assert index == iterations * number_of_threads 32 | 33 | 34 | def test_threads_race_condition_lock_off(): 35 | """ 36 | Проверяем, что если лок выключен - он не работает. 37 | Из-за состояния гонки инкремент должен "пробуксовывать". 38 | """ 39 | iterations = 5000 40 | number_of_threads = 4 41 | lock = ThreadLock(on=False) 42 | 43 | index = 0 44 | def incrementer(iterations): 45 | nonlocal index 46 | for _ in range(iterations): 47 | lock.acquire() 48 | value = index + 1 49 | time.sleep(0.001) 50 | index = value 51 | lock.release() 52 | 53 | threads = [Thread(target=incrementer, args=(iterations, )) for thread_index in range(number_of_threads)] 54 | for thread in threads: 55 | thread.start() 56 | for thread in threads: 57 | thread.join() 58 | 59 | assert index < iterations * number_of_threads 60 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/test_size_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.size_token import SizeToken 4 | 5 | 6 | def test_content_extraction_for_size_token(): 7 | """ 8 | Проверяем, что значение из строки извлекается корректно. 9 | """ 10 | assert SizeToken('b').content == 1 11 | assert SizeToken('kb').content == 1024 12 | assert SizeToken('mb').content == 1024 * 1024 13 | assert SizeToken('gb').content == 1024 * 1024 * 1024 14 | assert SizeToken('tb').content == 1024 * 1024 * 1024 * 1024 15 | assert SizeToken('pb').content == 1024 * 1024 * 1024 * 1024 * 1024 16 | 17 | assert SizeToken('byte').content == 1 18 | assert SizeToken('kilobyte').content == 1024 19 | assert SizeToken('megabyte').content == 1024 * 1024 20 | assert SizeToken('gigabyte').content == 1024 * 1024 * 1024 21 | assert SizeToken('terabyte').content == 1024 * 1024 * 1024 * 1024 22 | assert SizeToken('petabyte').content == 1024 * 1024 * 1024 * 1024 * 1024 23 | 24 | assert SizeToken('bytes').content == 1 25 | assert SizeToken('kilobytes').content == 1024 26 | assert SizeToken('megabytes').content == 1024 * 1024 27 | assert SizeToken('gigabytes').content == 1024 * 1024 * 1024 28 | assert SizeToken('terabytes').content == 1024 * 1024 * 1024 * 1024 29 | assert SizeToken('petabytes').content == 1024 * 1024 * 1024 * 1024 * 1024 30 | 31 | def test_creating_size_token_with_error(): 32 | """ 33 | Проверяем, что, если строка не является размерностью в байтах, токен не создастся. 34 | """ 35 | with pytest.raises(ValueError): 36 | SizeToken('kek') 37 | with pytest.raises(ValueError): 38 | SizeToken('5') 39 | with pytest.raises(ValueError): 40 | SizeToken('kek') 41 | -------------------------------------------------------------------------------- /polog/tests/data_structures/wrappers/fields_container/test_container.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.data_structures.wrappers.fields_container.container import FieldsContainer 4 | 5 | 6 | def test_wrapper_items_iteration_base(): 7 | """ 8 | Проверяем базовый случай, что два словаря с непересекающимися ключами объединяются. 9 | """ 10 | assert {key: value for key, value in FieldsContainer({'lol': 'kek'}, {'cheburek': 'perekek'}).items()} == {'lol': 'kek', 'cheburek': 'perekek'} 11 | 12 | def test_wrapper_items_iteration_intersection(): 13 | """ 14 | Немного усложняем. В двух словарях есть одинаковый ключ 'pek' с разными значениями. 15 | В итоге при итерации значение должно подставиться из основного словаря. 16 | """ 17 | assert {key: value for key, value in FieldsContainer({'lol': 'kek', 'pek': 'mek'}, {'cheburek': 'perekek', 'pek': 'zek'}).items()} == {'lol': 'kek', 'pek': 'mek', 'cheburek': 'perekek'} 18 | 19 | def test_wrapper_items_iteration_intersection_len(): 20 | """ 21 | Проверяем, что при пересекающихся ключах в словарях они не дублируются при итерации. 22 | """ 23 | items = list(FieldsContainer({'lol': 'kek', 'pek': 'mek'}, {'cheburek': 'perekek', 'pek': 'zek'}).items()) 24 | assert len(items) == 3 25 | 26 | def test_wrapper_iteration_base(): 27 | """ 28 | Еще один базовый случай, что два непересекающихся множества ключей двух словарей при итерации объединяются. 29 | """ 30 | assert list(FieldsContainer({'lol': 'kek'}, {'cheburek': 'perekek'})) == ['lol', 'cheburek'] 31 | 32 | def test_wrapper_iteration_intersection(): 33 | """ 34 | Еще один базовый случай, что два непересекающихся множества ключей двух словарей при итерации объединяются. 35 | """ 36 | assert list(FieldsContainer({'lol': 'kek', 'pek': 'mek'}, {'cheburek': 'perekek', 'pek': 'zek'})) == ['lol', 'pek', 'cheburek'] 37 | -------------------------------------------------------------------------------- /polog/handlers/file/locks/file_lock.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.locks.abstract_single_lock import AbstractSingleLock 2 | 3 | 4 | class FileLock(AbstractSingleLock): 5 | """ 6 | Реализация файловой блокировки (см. https://en.wikipedia.org/wiki/File_locking). 7 | 8 | Опирается на соответствующий API операционной системы, который присутствует только в *NIX'ах. Соответственно, в других ОС использовать нельзя. 9 | 10 | Файловая блокировка защищает прежде всего от аффектов перекрестных действий со стороны разных процессов над одним файлом. 11 | То есть у нас один процесс может, к примеру, решить, что файл с логами нужно ротировать, в то время как второй продолжает в него писать. 12 | Для файловой блокировки создается отдельный специальный файл с расширением .lock. Это необходимо, поскольку сам файл с логами ненадежен - его отдельные процессы могут перемещать или удалять. 13 | """ 14 | def __init__(self, original_file_name, lock_file_extension='lock'): 15 | """ 16 | Инициализация блокировки. 17 | 18 | original_file_name - имя файла, куда идет запись логов, и который мы хотим защитить локом. Если передать вместо него None, блокировка включена не будет. 19 | lock_file_extension - расширение файла блокировки. Для реализации блокировки будет создан еще один файл, имя которого образовано из имени оригинального файла + нового расширения. 20 | """ 21 | if not original_file_name: 22 | self.off() 23 | else: 24 | self.filename = f'{original_file_name}.{lock_file_extension}' 25 | self.file = open(self.filename, 'w') 26 | 27 | def acquire(self): 28 | """ 29 | Взять лок. 30 | """ 31 | import fcntl 32 | fcntl.flock(self.file.fileno(), fcntl.LOCK_EX) 33 | 34 | def release(self): 35 | """ 36 | Отпустить лок. 37 | """ 38 | import fcntl 39 | fcntl.flock(self.file.fileno(), fcntl.LOCK_UN) 40 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokenizator.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens import SizeToken, NumberToken, DotToken 2 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.tokens_group import TokensGroup 3 | 4 | 5 | class Tokenizator: 6 | """ 7 | Разбиваем исходную строку с правилом на токены. 8 | """ 9 | def __init__(self, source, tokens_classes=[SizeToken, NumberToken, DotToken]): 10 | """ 11 | source - исходная строка с правилом. 12 | tokens_classes - все доступные классы токенов. 13 | 14 | Особое внимание на класс DotToken. Любой нераспознанный токен будет отнесен к данному классу. 15 | Именно поэтому он ОБЯЗАТЕЛЬНО должен быть последним в списке. 16 | """ 17 | self.source = source 18 | self.tokens_classes = tokens_classes 19 | self.tokens = self.generate_tokens() 20 | 21 | def generate_tokens(self): 22 | """ 23 | Делим исходную строку на подстроки (скажем, на слова). Эти подстроки скармливаем в конструкторы всех классов токенов. 24 | Если токен не узнает себя в подстроке, он поднимает исключение. То есть, если исключения нет - значит подстрока представляет именно данный токен. 25 | """ 26 | pre_tokens = self.split_text(self.source) 27 | tokens = [] 28 | for source_token in pre_tokens: 29 | full = False 30 | for cls in self.tokens_classes: 31 | try: 32 | token = cls(source_token) 33 | tokens.append(token) 34 | full = True 35 | except Exception as e: 36 | pass 37 | if full: 38 | break 39 | return TokensGroup(tokens) 40 | 41 | def split_text(self, source): 42 | """ 43 | Берем исходную строку с правилом и делим на подстроки. 44 | Каждая подстрока представляет отдельный токен. 45 | """ 46 | return source.split() 47 | -------------------------------------------------------------------------------- /polog/tests/handlers/memory/test_saver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.memory.saver import memory_saver 4 | from polog.core.log_item import LogItem 5 | 6 | 7 | handler = memory_saver() 8 | 9 | def test_singleton(): 10 | """ 11 | Проверка, что memory_saver - синглтон. 12 | """ 13 | assert memory_saver() is memory_saver() 14 | 15 | def test_add_empty_args(): 16 | """ 17 | Проверка, что запись лога в память происходит. 18 | """ 19 | handler({'message': 'hello'}) 20 | assert handler.last['message'] == 'hello' 21 | assert len(handler.all) > 0 22 | 23 | def test_add_full_args(): 24 | """ 25 | Проверка, что запись лога в память происходит. 26 | """ 27 | log = LogItem() 28 | log.set_data({'message': 'hello'}) 29 | log.set_function_input_data((1, 2, 3), {'lol': 'kek'}) 30 | 31 | handler(log) 32 | 33 | assert handler.last['message'] == 'hello' 34 | assert handler.last.function_input_data.args == (1, 2, 3) 35 | assert handler.last.function_input_data.kwargs == {'lol': 'kek'} 36 | 37 | def test_clean(): 38 | """ 39 | Проверка, что список логов очищается. 40 | """ 41 | log = LogItem() 42 | log.set_data({'message': 'hello'}) 43 | 44 | handler(log) 45 | handler.clean() 46 | 47 | assert len(handler.all) == 0 48 | assert handler.last is None 49 | 50 | def test_add_to_all(): 51 | """ 52 | Проверка, что список handler.all заполняется логами. 53 | """ 54 | log = LogItem() 55 | log.set_data({'message': 'hello'}) 56 | 57 | handler.clean() 58 | handler(log) 59 | assert len(handler.all) > 0 60 | 61 | def test_getargs(): 62 | """ 63 | Проверка, что можно получить доступ к полям лога без обращения напрямую к словарю. 64 | """ 65 | log = LogItem() 66 | log.set_data({'message': 'hello'}) 67 | 68 | handler.clean() 69 | handler(log) 70 | assert handler.last is not None 71 | assert handler.last['message'] is not None 72 | assert handler.last.fields['message'] == handler.last['message'] 73 | -------------------------------------------------------------------------------- /polog/tests/core/engine/real_engines/multithreaded/test_engine.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import active_count 3 | 4 | import pytest 5 | 6 | from polog.core.stores.settings.settings_store import SettingsStore 7 | from polog.core.engine.real_engines.multithreaded.engine import MultiThreadedRealEngine 8 | from polog.core.log_item import LogItem 9 | 10 | 11 | def test_write_and_size(settings_mock): 12 | """ 13 | Проверяем, что новые сообщения попадают в очередь. 14 | """ 15 | time.sleep(0.0001) 16 | engine = MultiThreadedRealEngine(settings_mock) 17 | assert engine.queue_size() == 0 18 | engine.write({'lol': 'kek'}) 19 | engine.write({'lol': 'kek'}) 20 | assert engine.queue_size() == 2 21 | engine.stop() 22 | 23 | def test_number_of_threads(handler): 24 | """ 25 | Проверяем, что создании объекта движка потоки в нужном количестве создаются, а с вызовом метода stop() - уничтожаются. 26 | """ 27 | store = SettingsStore() 28 | store['pool_size'] = 2 29 | before = active_count() 30 | engine = MultiThreadedRealEngine(store) 31 | after = active_count() 32 | assert after == before + store['pool_size'] 33 | engine.stop() 34 | after = active_count() 35 | assert after == before 36 | 37 | def test_lost_items_on_stop(settings_mock, handler): 38 | """ 39 | Проверяем, что при остановке движка логи не теряются. 40 | """ 41 | settings_mock.handlers['lol'] = handler 42 | engine = MultiThreadedRealEngine(settings_mock) 43 | log = LogItem() 44 | log.set_handlers([handler]) 45 | log.set_data({'lol': 'kek'}) 46 | 47 | number_of_items = 5000 48 | 49 | for index in range(number_of_items): 50 | engine.write(log) 51 | engine.stop() 52 | 53 | assert len(handler.all) == number_of_items 54 | 55 | def test_get_size(settings_mock): 56 | """ 57 | Проверяем, что счетчик числа элементов в очереди работает. 58 | """ 59 | engine = MultiThreadedRealEngine(settings_mock) 60 | engine.write({'lol': 'kek'}) 61 | assert engine.queue_size() == 1 62 | engine.write({'lol': 'kek'}) 63 | assert engine.queue_size() == 2 64 | engine.stop() 65 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/size_token.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.abstractions.abstract_token import AbstractToken 2 | 3 | 4 | class SizeToken(AbstractToken): 5 | """ 6 | Токен размера файла. Работает с базовыми обозначениями размеров: байты, мегабайты и т. д. 7 | """ 8 | regexp_letter = 's' 9 | 10 | # Сокращения названий размерностей файлов. 11 | short_sizes = { 12 | 'b': 1, 13 | 'kb': 1024, 14 | 'mb': 1024 * 1024, 15 | 'gb': 1024 * 1024 * 1024, 16 | 'tb': 1024 * 1024 * 1024 * 1024, 17 | 'pb': 1024 * 1024 * 1024 * 1024 * 1024, 18 | } 19 | # Полные названия размерностей. 20 | full_sizes = { 21 | 'byte': 1, 22 | 'kilobyte': 1024, 23 | 'megabyte': 1024 * 1024, 24 | 'gigabyte': 1024 * 1024 * 1024, 25 | 'terabyte': 1024 * 1024 * 1024 * 1024, 26 | 'petabyte': 1024 * 1024 * 1024 * 1024 * 1024, 27 | } 28 | 29 | @classmethod 30 | def its_me(cls, chunk): 31 | """ 32 | Если подстрока chunk находится в полном списке возможных названий размерностей - возвращаем True. 33 | """ 34 | return chunk in cls.get_all_keys() 35 | 36 | def parse(self): 37 | """ 38 | Берем строку self.source и достаем из словарей self.short_sizes и self.full_sizes соответствующее число байт. 39 | """ 40 | if self.source in self.short_sizes: 41 | return self.short_sizes[self.source] 42 | elif self.source in self.full_sizes: 43 | return self.full_sizes[self.source] 44 | if self.source.endswith('s'): 45 | return self.full_sizes[self.source[:-1]] 46 | 47 | @classmethod 48 | def get_all_keys(cls): 49 | """ 50 | Возвращаем список всех ключей из словарей cls.short_sizes и cls.full_sizes, а также ключей из cls.full_sizes с постфиксами 's'. 51 | """ 52 | result = [x for x in cls.short_sizes.keys()] 53 | result.extend([x for x in cls.full_sizes.keys()]) 54 | result.extend([f'{x}s' for x in cls.full_sizes.keys()]) 55 | return result 56 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_get_errors_level.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.get_errors_level import get_errors_level 4 | from polog.core.stores.settings.settings_store import SettingsStore 5 | from polog import config 6 | 7 | 8 | def test_get_base_level(): 9 | """ 10 | Проверяем, что, если локальные уровни логирования в декораторе / функции не установлены, результат берется из настроек. 11 | """ 12 | config.set(default_error_level=500, default_level=5) 13 | 14 | assert get_errors_level(None, None) == SettingsStore()['default_error_level'] 15 | 16 | def test_handle_level(): 17 | """ 18 | Проверяем, что, если локальный уровень логирования для ошибок установлен, используется он. 19 | Также проверяем, что уровни логирования, указанные текстом, преобразуются в числа. 20 | """ 21 | config.levels(lol=500) 22 | 23 | assert get_errors_level(500, None) == 500 24 | assert get_errors_level('lol', None) == 500 25 | 26 | def test_error_level(): 27 | """ 28 | Если указать идентификатор уровня, ранее не зарегистрированный в Polog, вернуться должен стандартный уровень логирования для ошибок. 29 | То же самое должно произойти, если указать невозможный идентификатор уровня (в примере - отрицательный). 30 | """ 31 | config.set(default_error_level=500, default_level=5) 32 | 33 | assert get_errors_level('lolkekololo', None) == SettingsStore()['default_error_level'] 34 | assert get_errors_level(-500, None) == SettingsStore()['default_error_level'] 35 | 36 | def test_max_level_if_not_determined(): 37 | """ 38 | Если уровень логирования для ошибок не установлен, должен использоваться максимальный из трех: локальный уровень для обычных событий (не ошибок, если установлен), глобальный дефолтный уровень для обычных событий, глобальный дефолтный уровень для ошибок. 39 | Проверяем, что это так и работает. 40 | """ 41 | config.set(default_error_level=500, default_level=5) 42 | 43 | assert get_errors_level(-5, 1000) == 1000 44 | assert get_errors_level(-5, 5) == SettingsStore()['default_error_level'] 45 | 46 | config.set(default_level=700) 47 | 48 | assert get_errors_level(-5, 5) == SettingsStore()['default_level'] 49 | assert get_errors_level(-5, -5) == SettingsStore()['default_level'] 50 | -------------------------------------------------------------------------------- /polog/core/stores/settings/actions/integration_with_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | from datetime import datetime 4 | 5 | from polog.core.stores.settings.actions.decorator import is_action 6 | 7 | 8 | def from_logging_filter_to_polog(record): 9 | """ 10 | Здесь копируется информация из объекта записи logging и передается логгеру Polog. 11 | """ 12 | from polog.loggers.handle.handle_log import simple_handle_log 13 | from polog.core.stores.settings.settings_store import SettingsStore 14 | 15 | 16 | store = SettingsStore() 17 | data = {} 18 | 19 | data['time'] = datetime.fromtimestamp(record.created) 20 | if record.msg: 21 | data['message'] = record.getMessage() 22 | data['level'] = record.levelno 23 | if record.levelno >= 30: 24 | data['success'] = False 25 | else: 26 | data['success'] = True 27 | data['module'] = record.module 28 | data['line_number'] = record.lineno 29 | if record.funcName.isidentifier(): 30 | data['function'] = record.funcName 31 | data['path_to_code'] = record.pathname 32 | if record.exc_info is not None: 33 | data['exception_type'] = record.exc_info[0].__name__ 34 | data['traceback'] = store['json_module'].dumps(traceback.format_tb(record.exc_info[2])) 35 | data['exception_message'] = str(record.exc_info[1]) 36 | data['thread'] = f'{record.threadName} ({record.thread})' 37 | data['process'] = f'{record.processName} ({record.process})' 38 | 39 | data['from_logging'] = True 40 | 41 | simple_handle_log(**data) 42 | return not store['logging_off'] 43 | 44 | @is_action 45 | def integration_with_logging(old_value, new_value, store): 46 | """ 47 | Включаем / выключаем интеграцию с модулем logging из стандартной библиотеки. 48 | 49 | Интеграция работает через навешивание фильтра на корневой регистратор, который всегда возвращает True, но при этом копирует содержимое запись в Polog. 50 | При этом не происходит никаких модификаций логики работы модуля logging. Если там настроены свои обработчики и прочая инфраструктура, они продолжат работать параллельно с Polog и независимо от него. 51 | """ 52 | if new_value: 53 | logging.root.addFilter(from_logging_filter_to_polog) 54 | else: 55 | logging.root.removeFilter(from_logging_filter_to_polog) 56 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_exception_is_suppressed.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.exception_is_suppressed import exception_is_suppressed 4 | 5 | 6 | def test_suppress_exception_subclasses_on_value_error_and_value_error(): 7 | """ 8 | Проверяем, что, при включенной настройке 'suppress_exception_subclasses', при полном совпадении типов исключений - возвращается True. 9 | """ 10 | assert exception_is_suppressed(ValueError(), [ValueError], {'suppress_exception_subclasses': True}) == True 11 | 12 | def test_suppress_exception_subclasses_on_value_error_and_other_error(): 13 | """ 14 | Проверяем, что, при включенной настройке 'suppress_exception_subclasses', при не совпадении типов исключений - возвращается False. 15 | """ 16 | assert exception_is_suppressed(ValueError(), [TypeError], {'suppress_exception_subclasses': True}) == False 17 | 18 | def test_suppress_exception_subclasses_off_value_error_and_other_error(): 19 | """ 20 | Проверяем, что, при выключенной настройке 'suppress_exception_subclasses', при не совпадении типов исключений - возвращается False. 21 | """ 22 | assert exception_is_suppressed(ValueError(), [TypeError], {'suppress_exception_subclasses': False}) == False 23 | 24 | def test_suppress_exception_subclasses_off_value_error_and_value_error(): 25 | """ 26 | Проверяем, что, при выключенной настройке 'suppress_exception_subclasses', при полном совпадении типов исключений - возвращается True. 27 | """ 28 | assert exception_is_suppressed(ValueError(), [ValueError], {'suppress_exception_subclasses': False}) == True 29 | 30 | def test_suppress_exception_subclasses_on_value_error_and_exception(): 31 | """ 32 | Проверяем, что, при включенной настройке 'suppress_exception_subclasses', если переданное исключение относится к подклассу одного из подавляемых - возвращается True. 33 | """ 34 | assert exception_is_suppressed(ValueError(), [Exception], {'suppress_exception_subclasses': True}) == True 35 | 36 | def test_suppress_exception_subclasses_off_value_error_and_exception(): 37 | """ 38 | Проверяем, что, при выключенной настройке 'suppress_exception_subclasses', если переданное исключение относится к подклассу одного из подавляемых - возвращается False. 39 | """ 40 | assert exception_is_suppressed(ValueError(), [Exception], {'suppress_exception_subclasses': False}) == False 41 | -------------------------------------------------------------------------------- /polog/loggers/handle/message.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from polog.loggers.handle.abstract import AbstractHandleLogger 4 | 5 | 6 | context = ContextVar('message') 7 | 8 | 9 | class Message(AbstractHandleLogger): 10 | """ 11 | При помощи данного класса можно редактировать сообщение и некоторые другие характеристики лога, записываемого через декоратор функций (экземпляр FunctionLogger), изнутри задекорированной функции. 12 | Для синхронизации с логирующим декоратором используется контекстная переменная. 13 | """ 14 | _forbidden_fields = { 15 | 'function', 16 | } 17 | 18 | def _push(self, fields): 19 | """ 20 | Сохраняем полученные поля в контекстную переменную. 21 | 22 | fields - словарь с данными, извлеченными из переданных пользователем аргументов. 23 | """ 24 | if fields: 25 | context.set(fields) 26 | 27 | def _specific_processing(self, fields): 28 | """ 29 | Извлекаем данные об исключении, если оно было передано. 30 | Метка success не затрагивается. 31 | Уровень логирования в случае передачи исключения также автоматически не меняется. 32 | 33 | fields - словарь с данными, извлеченными из переданных пользователем аргументов. 34 | """ 35 | self._extract_exception(fields, change_success=False, change_level=False) 36 | 37 | def _copy_context(self, old_args): 38 | """ 39 | Все поля словаря, извлеченного с помощью message, копируются в словарь с аргументами, извлеченными в декораторе автоматически. 40 | Автоматические значения перезатираются. 41 | После копирования всех полей, контекст очищается. 42 | 43 | old_args - словарь с данными, уже извлеченными в декораторе. 44 | """ 45 | new_args = self._get_context() 46 | if new_args is not None: 47 | for key, value in new_args.items(): 48 | old_args[key] = value 49 | self._clean_context() 50 | 51 | def _clean_context(self): 52 | """ 53 | Обнуляем контекстную переменную. 54 | """ 55 | context.set(None) 56 | 57 | def _get_context(self): 58 | """ 59 | Возвращаем содержимое контекстной переменной. 60 | Значение по умолчанию - None. 61 | """ 62 | return context.get(None) 63 | 64 | 65 | message = Message() 66 | -------------------------------------------------------------------------------- /polog/tests/core/stores/test_levels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.stores.levels import Levels 4 | 5 | 6 | def test_set_and_get(): 7 | """ 8 | Проверяем, что новые значения уровней по ключу устанавливаются, и потом считываются. 9 | """ 10 | Levels.set('kek', 10000) 11 | assert Levels.get('kek') == 10000 12 | Levels.set('kek', 5) 13 | assert Levels.get('kek') == 5 14 | 15 | def test_set_and_get_raises(): 16 | """ 17 | Проверяем проверки типов данных для уровней логирования и их алиасов при их чтении / изменении. 18 | """ 19 | with pytest.raises(KeyError): 20 | Levels.get(set()) # Пробуем использовать в качестве ключа объект с неподходящим типом данных. 21 | with pytest.raises(ValueError): 22 | Levels.set('lol', 'kek') # Пытаемся назначить алиас на алиас. 23 | # TODO: а почему бы, кстати, не разрешить так делать? 24 | with pytest.raises(KeyError): 25 | Levels.set(5, 'kek') # Алиас и числовое значение уровня перепутаны местами. 26 | 27 | def test_get_int(): 28 | """ 29 | Проверяем, что в случае ключа-числа возвращается это же самое число. 30 | То есть уровень в данном случае равен самому себе. 31 | """ 32 | assert Levels.get(5) == 5 33 | assert Levels.get(25) == 25 34 | 35 | def test_reverse(): 36 | """ 37 | Назначаем одному и тому же уровню разные алиасы и проверяем, что в реверсах всегда "отпечатывается" последний. 38 | """ 39 | Levels.set('kek', 777) 40 | assert Levels.levels_reverse[777] == 'kek' 41 | Levels.set('lol', 777) 42 | assert Levels.levels_reverse[777] == 'lol' 43 | 44 | def test_get_level_name(): 45 | """ 46 | Проверка, аналогичная test_reverse(). Только для метода Levels.get_level_name(). 47 | """ 48 | Levels.set('kek', 777) 49 | assert Levels.get_level_name(777) == 'kek' 50 | Levels.set('lol', 777) 51 | assert Levels.get_level_name(777) == 'lol' 52 | 53 | def test_add_get_all_names(): 54 | """ 55 | Добавляем новый алиас и проверяем, что он появляется в списке всех алиасов. 56 | """ 57 | assert 'lolkeklolkek' not in Levels.get_all_names() 58 | Levels.set('lolkeklolkek', 777) 59 | assert 'lolkeklolkek' in Levels.get_all_names() 60 | 61 | def test_get_level_name_none_request(): 62 | """ 63 | Проверяем, что если запросить имя уровня логирования передачей в качестве аргумента None - вернется None. 64 | """ 65 | assert Levels.get_level_name(None) is None 66 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/test_tokenizator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokenizator import Tokenizator 4 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens import SizeToken, NumberToken, DotToken 5 | 6 | 7 | def test_base_tokenization(): 8 | """ 9 | Проверяем, что исходная строка корректно бьется на токены. 10 | """ 11 | assert Tokenizator('20').tokens.tokens == [NumberToken('20')] 12 | assert Tokenizator('0').tokens.tokens == [NumberToken('0')] 13 | assert Tokenizator(' 0').tokens.tokens == [NumberToken('0')] 14 | assert Tokenizator('030').tokens.tokens == [NumberToken('30')] 15 | assert Tokenizator('-30').tokens.tokens == [NumberToken('-30')] 16 | 17 | assert Tokenizator('megabytes').tokens.tokens == [SizeToken('mb')] 18 | assert Tokenizator('mb').tokens.tokens == [SizeToken('mb')] 19 | assert Tokenizator('megabyte').tokens.tokens == [SizeToken('mb')] 20 | assert Tokenizator('gigabytes').tokens.tokens == [SizeToken('gb')] 21 | assert Tokenizator('gb').tokens.tokens == [SizeToken('gb')] 22 | assert Tokenizator('gigabyte').tokens.tokens == [SizeToken('gb')] 23 | assert Tokenizator('kilobytes').tokens.tokens == [SizeToken('kb')] 24 | assert Tokenizator('kb').tokens.tokens == [SizeToken('kb')] 25 | assert Tokenizator('kilobyte').tokens.tokens == [SizeToken('kb')] 26 | assert Tokenizator('bytes').tokens.tokens == [SizeToken('b')] 27 | assert Tokenizator('b').tokens.tokens == [SizeToken('b')] 28 | assert Tokenizator('byte').tokens.tokens == [SizeToken('b')] 29 | assert Tokenizator('petabytes').tokens.tokens == [SizeToken('pb')] 30 | assert Tokenizator('pb').tokens.tokens == [SizeToken('pb')] 31 | assert Tokenizator('petabyte').tokens.tokens == [SizeToken('pb')] 32 | assert Tokenizator('terabytes').tokens.tokens == [SizeToken('tb')] 33 | assert Tokenizator('tb').tokens.tokens == [SizeToken('tb')] 34 | assert Tokenizator('terabyte').tokens.tokens == [SizeToken('tb')] 35 | 36 | assert Tokenizator('kek').tokens.tokens == [DotToken('kek')] 37 | assert Tokenizator('lol').tokens.tokens == [DotToken('lol')] 38 | assert Tokenizator('12lol').tokens.tokens == [DotToken('12lol')] 39 | 40 | assert Tokenizator('lol kek cheburek 12 mb oh').tokens.tokens == [DotToken('lol'), DotToken('kek'), DotToken('cheburek'), NumberToken('12'), SizeToken('mb'), DotToken('oh')] 41 | assert Tokenizator('20 20').tokens.tokens == [NumberToken('20'), NumberToken('20')] 42 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/abstractions/meta_token.py: -------------------------------------------------------------------------------- 1 | class MetaToken(type): 2 | """ 3 | Здесь попрождаются классы токенов. 4 | При создании класса проверяем корректность набора его атрибутов. 5 | """ 6 | 7 | # Данные символы зарезервированы движком регулярных выражений. 8 | forbidden_regexp_letters = ( 9 | '*', # Пропуск любого количества токенов до тех пор, пока не начнется нужная последовательность. 10 | '[', # Квадратные скобки используются в движке регулярных выражений для обозначения подстроки, с которой нужно сравнивать токен. 11 | ']', 12 | ) 13 | 14 | # У каждого порожденного класса должен быть атрибут regexp_letter. 15 | # regexp_letter не должен быть одинаковым у двух разных классов. 16 | # Для обеспечения такой проверки в данном словаре хранятся regexp_letter'ы ранее порожденных классов (ключи) и их названия (значения). 17 | all_regexp_letters = {} 18 | 19 | def __new__(cls, name, bases, dct): 20 | """ 21 | Перед созданием класса у него проверяется корретность содержимого атрибута regexp_letter. 22 | 23 | Атрибут regexp_letter должен: 24 | 1. Быть. 25 | 2. Быть строкой. 26 | 3. Иметь длину в 1 символ. 27 | 4. Не быть одним из зарезервированных для синтаксиса токенизатора символов. 28 | 5. Не повторяться у двух разных классов. 29 | 6. Не быть определенным у абстрактного класса. 30 | 31 | При несоблюдении любого из этих правил, поднимется исключение. 32 | """ 33 | x = super().__new__(cls, name, bases, dct) 34 | if x.__name__ == 'AbstractToken': 35 | if hasattr(x, 'regexp_letter') and getattr(x, 'regexp_letter') is not None: 36 | raise AttributeError('The "regexp_letter" attribute should not be defined for the abstract token class.') 37 | return x 38 | if not hasattr(x, 'regexp_letter') or type(x.regexp_letter) is not str or len(x.regexp_letter) != 1 or x.regexp_letter in cls.forbidden_regexp_letters: 39 | raise AttributeError(f'The attribute "regexp_letter" of the class {x.__name__} must be a string of the lenth == 1, not "*" and not ".". When inheriting from an abstract class, you should correctly override this parameter. These conditions are automatically checked in the metaclass.') 40 | if x.regexp_letter in cls.all_regexp_letters: 41 | raise AttributeError(f'The {cls.all_regexp_letters[x.regexp_letter]} and {name} classes have the same value "{x.regexp_letter}" for the attribute "regexp_letter".') 42 | else: 43 | cls.all_regexp_letters[x.regexp_letter] = name 44 | return x 45 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_pony_names_generator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.pony_names_generator import PonyNamesGenerator 4 | 5 | 6 | def test_new_portion_len(): 7 | """ 8 | Проверяем, что число комбинаций имен соответствует ожидаемому. 9 | Данное число == 7*7 + 5*5. 10 | """ 11 | assert len(PonyNamesGenerator.new_names_portion()) == 74 12 | 13 | def test_new_portion_contains(): 14 | """ 15 | Проверяем, что некоторые ожидаемые комбинации присутствуют. 16 | """ 17 | assert 'Twilight Sparkle' in PonyNamesGenerator.new_names_portion() 18 | assert 'Pinkie Pie' in PonyNamesGenerator.new_names_portion() 19 | assert 'Pinkie Sparkle' in PonyNamesGenerator.new_names_portion() 20 | 21 | def test_base_combinations(): 22 | """ 23 | Проверяем, что декартово произведение работает. 24 | """ 25 | container = [] 26 | PonyNamesGenerator.halfs_combinations( 27 | container, 28 | [ 29 | 'a', 30 | 'b', 31 | ], 32 | [ 33 | 'a', 34 | 'b', 35 | ], 36 | ) 37 | assert container == ['aa', 'ab', 'ba', 'bb'] 38 | 39 | def test_prefix_combinations(): 40 | """ 41 | Проверяем, что декартово произведение работает и префиксы проставляются. 42 | """ 43 | container = [] 44 | PonyNamesGenerator.halfs_combinations( 45 | container, 46 | [ 47 | 'a', 48 | 'b', 49 | ], 50 | [ 51 | 'a', 52 | 'b', 53 | ], 54 | prefix='c', 55 | ) 56 | assert container == ['caa', 'cab', 'cba', 'cbb'] 57 | 58 | def test_roman_numerals(): 59 | """ 60 | Проверяем генератор римских цифр. 61 | """ 62 | assert PonyNamesGenerator.roman_numerals(1) == 'I' 63 | assert PonyNamesGenerator.roman_numerals(2) == 'II' 64 | assert PonyNamesGenerator.roman_numerals(5) == 'V' 65 | assert PonyNamesGenerator.roman_numerals(15) == 'XV' 66 | assert PonyNamesGenerator.roman_numerals(2134) == 'MMCXXXIV' 67 | assert PonyNamesGenerator.roman_numerals(0) is None 68 | assert PonyNamesGenerator.roman_numerals(-1) is None 69 | 70 | def test_generator_works_10000(): 71 | """ 72 | Пробуем сгенерировать много имен и проверяем, что что-то генерируется. 73 | Также проверяем, что среди сгенерированных имен есть несколько из выборки. 74 | """ 75 | names = [] 76 | for index, x in enumerate(PonyNamesGenerator().get_next_pony()): 77 | assert len(x) > 0 78 | names.append(x) 79 | if index > 10000: 80 | break 81 | assert 'Pinkie Pie' in names 82 | assert 'Pinkie Pie II' in names 83 | assert 'Pinkie Pie I' not in names 84 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_time_limit.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from polog.core.utils.time_limit import time_limit 6 | 7 | 8 | def test_integer(): 9 | """ 10 | Проверяем, что лимит устанавливается и срабатывает с числом в качестве аргумента. 11 | """ 12 | quant = 0.0001 13 | function = lambda number: [time.sleep(quant) for x in range(number)] 14 | 15 | wrapper = time_limit(quant * 10) 16 | wrapped_function = wrapper(function) 17 | with pytest.raises(TimeoutError): 18 | wrapped_function(3 * 10) 19 | 20 | wrapper = time_limit(quant * 100) 21 | wrapped_function = wrapper(function) 22 | wrapped_function(10) 23 | 24 | def test_function_as_parameter(): 25 | """ 26 | Проверяем, что лимит устанавливается и срабатывает с функцией в качестве аргумента. 27 | """ 28 | quant = 0.001 29 | function = lambda number: [time.sleep(quant) for x in range(number)] 30 | 31 | wrapper = time_limit(lambda: quant) 32 | wrapped_function = wrapper(function) 33 | with pytest.raises(TimeoutError): 34 | wrapped_function(3) 35 | 36 | wrapper = time_limit(lambda: quant * 10) 37 | wrapped_function = wrapper(function) 38 | wrapped_function(3) 39 | 40 | def test_error_signature_function(): 41 | """ 42 | Проверяем, что функция с некорректной сигнатурой не принимается. 43 | """ 44 | def test(kek): 45 | pass 46 | with pytest.raises(ValueError): 47 | wrapper = time_limit(test) 48 | 49 | test = lambda x: None 50 | with pytest.raises(ValueError): 51 | wrapper = time_limit(test) 52 | 53 | def test_wrong_numbers(): 54 | """ 55 | Проверяем, что, при попытке передать в конструктор декоратора недействительное значение, поднимается ValueError. 56 | """ 57 | with pytest.raises(ValueError): 58 | @time_limit(-1) 59 | def kek(): 60 | pass 61 | with pytest.raises(ValueError): 62 | # Нулевой таймаут тоже классифицируется как ошибка. 63 | @time_limit(0) 64 | def kek(): 65 | pass 66 | 67 | def test_wrong_object(): 68 | """ 69 | Проверяем, что, при попытке передать в конструктор декоратора не число и не функция, поднимается ValueError. 70 | """ 71 | with pytest.raises(ValueError): 72 | @time_limit('kek') 73 | def kek(): 74 | pass 75 | 76 | def test_wrong_number_in_action(): 77 | """ 78 | В случае, если переданная аргументом функция возвращает недействительное значение, она должна просто отрабатывать без таймаута. 79 | Проверяем, что это так и происходит. 80 | """ 81 | flag = False 82 | @time_limit(lambda: -1) 83 | def function(): 84 | nonlocal flag 85 | flag = True 86 | function() 87 | assert flag == True 88 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/multithreaded/pool.py: -------------------------------------------------------------------------------- 1 | import time 2 | from queue import Queue 3 | 4 | from polog.core.engine.real_engines.multithreaded.worker import Worker 5 | 6 | 7 | class ThreadPool: 8 | """ 9 | Группа потоков-воркеров, обрабатывающая запись логов. Каждый воркер смотрит в общую очередь и забирает оттуда данные. 10 | 11 | Группа поддерживает команды, относящиеся ко всей группе сразу: 12 | .put(<данные лога>) - записать лог. 13 | .stop() - дождаться записи всех логов в очереди, после чего завершить потоки-воркеры и присоединить их. 14 | """ 15 | def __init__(self, settings): 16 | self.settings = settings 17 | self.queue = Queue(maxsize=self.settings.force_get('max_queue_size')) 18 | self.workers = self.create_workers() 19 | 20 | def put(self, log_item): 21 | """ 22 | Помещение лога в очередь. 23 | """ 24 | self.queue.put(log_item) 25 | 26 | def stop(self): 27 | """ 28 | Остановка воркеров. 29 | 30 | Подразумевается, что объект, останавливающий группу потоков, проконтролирует, что в процессе остановки очередь пополняться не будет. 31 | 32 | Этапы остановки: 33 | 1. Дождаться опустошения очереди. 34 | 2. Проставить флаги остановки воркерам. Воркер нельзя просто "убить". Он должен завершить работу с текущим таском, после чего проверить флаг остановки, и, если он установлен - выйти из цикла. 35 | 3. Заджойнить потоки воркеров. 36 | """ 37 | self.wait_empty_queue() 38 | for worker in self.workers: 39 | worker.set_stop_flag() 40 | for worker in self.workers: 41 | worker.stop() 42 | 43 | def wait_empty_queue(self): 44 | """ 45 | Опрашиваем очередь с некоторой периодичностью, пока она не опустеет. Когда очередь опустела, выходим. 46 | 47 | Периодичность опроса определяется двумя пунктами настроек - 'time_quant' и 'delay_on_exit_loop_iteration_in_quants'. 48 | 'time_quant' - временной квант, константа, определяющая минимальное время выполнения любой операции (в секундах). 49 | 'delay_on_exit_loop_iteration_in_quants' - количество квантов (целое число), на которое мы засыпаем в каждой итерации цикла опроса. 50 | """ 51 | delay = self.settings['time_quant'] * self.settings['delay_on_exit_loop_iteration_in_quants'] 52 | while True: 53 | if self.queue.empty(): 54 | break 55 | time.sleep(delay) 56 | 57 | def create_workers(self): 58 | """ 59 | Создание и запуск воркеров. 60 | """ 61 | workers = [] 62 | for index in range(self.settings.force_get('pool_size')): 63 | worker = Worker(self.queue, index, self.settings) 64 | workers.append(worker) 65 | return workers 66 | -------------------------------------------------------------------------------- /polog/core/stores/levels.py: -------------------------------------------------------------------------------- 1 | class Levels: 2 | """ 3 | Класс для хранения соответствий между именами уровней логирования и их числовыми значениями. 4 | 5 | Изначально имена уровням логирования не заданы. Чтобы задать их в соответствии со схемой из стандартной библиотеки (см. https://docs.python.org/3.8/library/logging.html#logging-levels), используйте Config.standard_levels(). 6 | """ 7 | # Названия уровней - ключи, числа - значения. 8 | levels = {} 9 | # Наоборот, числа - ключи, названия уровней - значения. 10 | # Размер словаря не обязан совпадать с размером levels, т. к. пользователь может дать одному уровню 2 разных имени, и здесь из них будет фигурировать только последнее. 11 | levels_reverse = {} 12 | 13 | @classmethod 14 | def set(cls, name, value): 15 | """ 16 | Устанавливаем уровню логирования имя. 17 | """ 18 | if not isinstance(name, str): 19 | raise KeyError('The key for the logging level can only be a string.') 20 | if not isinstance(value, int): 21 | raise ValueError('The level value can only be an integer.') 22 | cls.levels[name] = value 23 | cls.levels_reverse[value] = name 24 | 25 | @classmethod 26 | def get(cls, key): 27 | """ 28 | Получаем номер уровня по названию. 29 | """ 30 | if isinstance(key, str): 31 | result = cls.levels.get(key) 32 | if result is None: 33 | raise KeyError(f'Logging level "{key}" is not exist.') 34 | else: 35 | if not (type(key) is int): 36 | raise KeyError('Expected types for level: int or str.') 37 | if key < 0: 38 | raise KeyError('The level value must not be less than zero.') 39 | result = key 40 | return result 41 | 42 | @classmethod 43 | def get_level_name(cls, level_number): 44 | """ 45 | Получаем название уровня по числовому значению. 46 | 47 | В случае, если один уровень фигурировал под двумя разными названиями, вернется последнее название. 48 | Если название уровня не зарегистрировано, вернется преобразованное в строку числовое значение уровня. 49 | """ 50 | if level_number is None: 51 | return None 52 | result = cls.levels_reverse.get(level_number, None) 53 | if result is None: 54 | result = str(level_number) 55 | return result 56 | 57 | @classmethod 58 | def get_all_names(cls): 59 | """ 60 | Метод, возвращающий список из всех зарегистрированных на данный момент имен уровней логирования. 61 | 62 | В Polog нет запрета одному и тому же уровню присвоить два или более имени. Разные имена могут ссылаться на один и тот же уровень. 63 | """ 64 | result = list(cls.levels.keys()) 65 | return result 66 | -------------------------------------------------------------------------------- /polog/loggers/partial.py: -------------------------------------------------------------------------------- 1 | from polog.loggers.finalizer import LoggerRouteFinalizer 2 | 3 | 4 | class RouterPartial: 5 | """ 6 | Объект, возвращаемый при извлечении у роутера атрибута по неизвестному имени. 7 | 8 | Его можно использовать в качестве контекстного менеджера (проксируемого на сам роутер). То есть за счет него будет срабатывать вот такой трюк: 9 | 10 | >>> from polog import log, config 11 | >>> 12 | >>> config.levels(kek=5) 13 | >>> 14 | >>> with log.kek: 15 | >>> ... pass 16 | 17 | Также можно вызывать от объектов данного класса метод .suppress(). Непосредственно от роутера: 18 | 19 | >>> with log.suppress(): 20 | >>> ... raise ValueError # Будет подавлено. 21 | 22 | После указания уровня через точку: 23 | 24 | >>> with log.kek.suppress(): 25 | >>> ... raise ValueError # Будет подавлено. 26 | """ 27 | def __init__(self, item, **kwargs): 28 | """ 29 | Сохранение аргументов для последующего отложенного вызова функции. 30 | 31 | item - объект роутера. 32 | kwargs - именованные аргументы, которые будут переданы в роутер при вызове объекта RouterPartial. 33 | """ 34 | self.item = item 35 | self.kwargs = kwargs 36 | self.to_calling_before_enter = None 37 | 38 | def __call__(self, *args, **kwargs): 39 | """ 40 | Отложенный вызов функции, аналог functools.partial. 41 | """ 42 | kwargs.update(self.kwargs) 43 | return self.item(*args, **kwargs) 44 | 45 | def __enter__(self): 46 | """ 47 | Вход в контекстный менеджер. По факту проксирует вход в контекстный менеджер от роутера. 48 | """ 49 | result = self.item.__enter__() 50 | result(**self.kwargs) 51 | if self.to_calling_before_enter is not None: 52 | method_name, args, kwargs = self.to_calling_before_enter 53 | attribute = getattr(result, method_name) 54 | attribute(*args, **kwargs) 55 | return result 56 | 57 | def __exit__(self, exception_type, exception_value, traceback_instance): 58 | """ 59 | Выход из контекста. 60 | """ 61 | return self.item.__exit__(exception_type, exception_value, traceback_instance) 62 | 63 | def aware_calling_method(self, method_name, *args, **kwargs): 64 | """ 65 | Данный метод будет вызван у роутера при входе в контекстный менеджер. 66 | 67 | method_name - имя метода. 68 | args, kwargs - аргументы, которые будут туда переданы. 69 | """ 70 | self.to_calling_before_enter = (method_name, args, kwargs) 71 | 72 | def suppress(self, *exceptions): 73 | """ 74 | Подавление исключений для контекстных менеджеров. 75 | """ 76 | result = LoggerRouteFinalizer(**self.kwargs) 77 | result.suppress(*exceptions) 78 | return result 79 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/abstract_rule.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokenizator import Tokenizator 2 | 3 | 4 | class AbstractRule: 5 | """ 6 | Абстрактный класс, все правила ротации должны быть отнаследованы от него. 7 | Благодаря такой компоновке добавлять в систему новые правила становится гораздо проще, нужно всего лишь переопределить несколько методов. 8 | 9 | Правило ротации - это некий класс, экземпляр которого перед каждой записью лога должен решать, нужна сейчас ротация или нет. 10 | Правила могут быть самыми разными. Одни будут смотреть на размер файла, другие на то, когда была произведена последняя ротация, и т. д. 11 | Интерфейс у всех правил должен быть одинаковым. Если хотя бы одно правило требует ротации, она будет произведена. Если ротации требуют 2 и более правил, она будет проведена 1 раз. 12 | 13 | Правила ротации пользователь задает в виде обычного текста в определенном формате. Этот текст "скармливается" правилу ротации, после чего оно может определить, подходит он по формату или нет. 14 | 15 | Для упрощения парсинга правил ротации из текста в Polog реализованы собственные движки для токенизации текста и обработки высокоуровневых регулярных выражений. 16 | """ 17 | def __init__(self, source, file): 18 | self.source = source 19 | self.file = file 20 | self.tokens = self.get_tokens(source) 21 | if self.prove_source(): 22 | self.extract_data_from_string() 23 | 24 | def __repr__(self): 25 | type_name = type(self).__name__ 26 | result = f'{type_name}("{self.source}")' 27 | return result 28 | 29 | def get_tokens(self, source): 30 | """ 31 | Получаем группу токенов из исходной строки. 32 | """ 33 | tokens = Tokenizator(source).generate_tokens() 34 | return tokens 35 | 36 | def extract_data_from_string(self): 37 | """ 38 | Метод, который должен извлекать нужные для работы правила данные из исходной строки. 39 | 40 | Он ничего не должен возвращать. 41 | """ 42 | raise NotImplementedError # pragma: no cover 43 | 44 | def prove_source(self): 45 | """ 46 | Здесь мы проверяем, что исходная строка в нужном нам формате, то есть описывает тот тип правил, который обрабатывается конкретным наследником данного класса. 47 | 48 | Метод должен возвращать True или False в зависимости от результата проверки. 49 | """ 50 | raise NotImplementedError # pragma: no cover 51 | 52 | def check(self): 53 | """ 54 | Эта функция будет вызываться при каждой записи лога. 55 | Ее задача - определить, должно ли данное правило сработать сейчас. 56 | 57 | Метод должен возвращать True или False в зависимости от результата проверки. 58 | """ 59 | raise NotImplementedError # pragma: no cover 60 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/multithreaded/worker.py: -------------------------------------------------------------------------------- 1 | from queue import Empty 2 | from threading import Thread 3 | 4 | 5 | class Worker: 6 | """ 7 | Экземпляр класса соответствует одному потоку. Здесь происходит непосредственно выполнение функций-обработчиков. 8 | """ 9 | def __init__(self, queue, index, settings): 10 | self.index = index 11 | self.queue = queue 12 | self.stopped = False 13 | self.settings = settings 14 | self.thread = self.start_thread() 15 | 16 | def run(self): 17 | """ 18 | В бесконечном цикле принимаем из очереди данные и что-то с ними делаем. 19 | 20 | На каждом обороте цикла, а также в случае слишком долгого ожидания блокировки очереди, необходимо проверять наличие стоп-сигнала. В случае получения стоп-сигнала, необходимо выйти из цикла, чтобы поток мог быть присоединен к основному. 21 | """ 22 | stopped_from_flag = False 23 | while True: 24 | try: 25 | while True: 26 | try: 27 | log = self.queue.get(timeout=self.settings['time_quant']) 28 | break 29 | except Empty: 30 | if self.stopped: 31 | stopped_from_flag = True 32 | break 33 | if stopped_from_flag: 34 | break 35 | self.do_anything(log) 36 | self.queue.task_done() 37 | except Exception as e: 38 | self.queue.task_done() 39 | 40 | def start_thread(self): 41 | """ 42 | Запуск отдельного потока, который будет принимать события из очереди. 43 | """ 44 | thread = Thread(target=self.run) 45 | thread.daemon = True 46 | thread.start() 47 | return thread 48 | 49 | def set_stop_flag(self): 50 | """ 51 | Устанавливаем флаг остановки. 52 | 53 | Флаг остановки проверяется внутри цикла обработки поступающих из очереди логов. Пока он не установлен, логи обрабатываются в штатном режиме. Когда флаг установлен, а очередь пуста - происходит выход из цикла. 54 | 55 | Установка флага - первый этап остановки воркера. 56 | """ 57 | self.stopped = True 58 | 59 | def stop(self): 60 | """ 61 | Останавливаем воркер. 62 | 63 | Это второй этап остановки воркера (см. также self.set_stop_flag()). Ожидается, что к моменту вызова данного метода флаг остановки уже установлен и цикл обработки логов прерван, что позволяет беспрепятственно присоединить поток воркера. 64 | """ 65 | self.thread.join() 66 | 67 | def do_anything(self, log_item): 68 | """ 69 | "Выполняем" лог, то есть запускаем все привязанные к нему действия - извлечения полей, передачу лога в обработчики и т. д. 70 | """ 71 | log_item() 72 | -------------------------------------------------------------------------------- /polog/handlers/file/locks/double_lock.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.locks.file_lock import FileLock 2 | from polog.handlers.file.locks.thread_lock import ThreadLock 3 | 4 | 5 | class DoubleLock: 6 | """ 7 | Менеджер контекста сразу для двух типов блокировок: 8 | 1. Блокировки потока. 9 | 2. Файловой блокировки. 10 | 11 | Блокировка необходима в момент какой-то чувствительной работы над файлом, когда важно, чтобы в нее не вмешивались другие процессы / потоки. 12 | """ 13 | def __init__(self, filename, lock_type, lock_file_extension='lock'): 14 | self.types = self.get_lock_types(lock_type) 15 | self.thread_lock = self.get_thread_lock() 16 | self.file_lock = self.get_file_lock(filename, lock_file_extension) 17 | self.active = bool(self.thread_lock.active + self.file_lock.active) 18 | 19 | def __enter__(self): 20 | """ 21 | Взять оба лока. 22 | """ 23 | self.thread_lock.acquire() 24 | self.file_lock.acquire() 25 | 26 | def __exit__(self, exception_type, exception_value, traceback): 27 | """ 28 | Отпустить оба лока. 29 | """ 30 | self.thread_lock.release() 31 | self.file_lock.release() 32 | 33 | @staticmethod 34 | def get_lock_types(lock_type): 35 | """ 36 | Получить коллекцию строк - видов локов, которые нужно включить. 37 | """ 38 | if lock_type is None: 39 | return [] 40 | if not isinstance(lock_type, str): 41 | raise ValueError('A set of lock types can only be specified as a string, using the "+" sign as a connector. Example: "thread+file".') 42 | if not lock_type.strip(): 43 | raise ValueError('You did not specify any locks, but you did not pass None.') 44 | 45 | allowed_types = ('file', 'thread') 46 | maybe_types = [x.strip() for x in lock_type.split('+') if x.strip()] 47 | 48 | result = [] 49 | previously_seen = set() 50 | for maybe_type in maybe_types: 51 | if maybe_type not in allowed_types: 52 | raise ValueError(f'{len(allowed_types)} types of blocking are allowed: {", ".join([x for x in allowed_types])}. You passed "{maybe_type}".') 53 | if maybe_type in previously_seen: 54 | raise ValueError(f'You have specified a type of lock "{maybe_type}" more than once. Did you mean another blocking?') 55 | result.append(maybe_type) 56 | previously_seen.add(maybe_type) 57 | 58 | result.sort() 59 | return result 60 | 61 | def get_thread_lock(self): 62 | """ 63 | Получить объект обертки над тред-локом. 64 | """ 65 | on = 'thread' in self.types 66 | return ThreadLock(on=on) 67 | 68 | def get_file_lock(self, filename, lock_file_extension): 69 | """ 70 | Получить объект обертки над файл-локом. 71 | """ 72 | if 'file' not in self.types: 73 | filename = None 74 | return FileLock(filename, lock_file_extension) 75 | -------------------------------------------------------------------------------- /polog/tests/core/utils/test_name_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.core.utils.name_manager import NameManager 4 | 5 | 6 | def test_is_possible_level_name(): 7 | """ 8 | Проверяем, что определение корректности имен уровней логирования работает корректно. 9 | """ 10 | assert NameManager.is_possible_level_name('lol kek').possibility == False 11 | assert NameManager.is_possible_level_name('_kek').possibility == False 12 | assert NameManager.is_possible_level_name('.kek').possibility == False 13 | assert NameManager.is_possible_level_name('message').possibility == False 14 | 15 | assert NameManager.is_possible_level_name('kek').possibility == True 16 | 17 | def test_is_possible_extra_field_name(): 18 | """ 19 | Проверяем, что определение корректности имен извлекаемых полей работает корректно. 20 | """ 21 | assert NameManager.is_possible_extra_field_name('lol kek').possibility == False 22 | assert NameManager.is_possible_extra_field_name('_kek').possibility == False 23 | assert NameManager.is_possible_extra_field_name('.kek').possibility == False 24 | 25 | assert NameManager.is_possible_extra_field_name('message').possibility == False 26 | assert NameManager.is_possible_extra_field_name('level').possibility == False 27 | assert NameManager.is_possible_extra_field_name('auto').possibility == False 28 | assert NameManager.is_possible_extra_field_name('time').possibility == False 29 | assert NameManager.is_possible_extra_field_name('service_name').possibility == False 30 | assert NameManager.is_possible_extra_field_name('success').possibility == False 31 | assert NameManager.is_possible_extra_field_name('function').possibility == False 32 | assert NameManager.is_possible_extra_field_name('class').possibility == False 33 | assert NameManager.is_possible_extra_field_name('exception_type').possibility == False 34 | assert NameManager.is_possible_extra_field_name('exception_message').possibility == False 35 | assert NameManager.is_possible_extra_field_name('traceback').possibility == False 36 | assert NameManager.is_possible_extra_field_name('input_variables').possibility == False 37 | assert NameManager.is_possible_extra_field_name('local_variables').possibility == False 38 | assert NameManager.is_possible_extra_field_name('result').possibility == False 39 | assert NameManager.is_possible_extra_field_name('time_of_work').possibility == False 40 | 41 | assert NameManager.is_possible_extra_field_name('kek').possibility == True 42 | 43 | def test_is_identifier_name(): 44 | """ 45 | Проверяем, что определение строки как возможного идентификатора python работает корректно. 46 | """ 47 | assert NameManager.is_identifier_name('PossibleName') == True 48 | assert NameManager.is_identifier_name('possible_name') == True 49 | assert NameManager.is_identifier_name('kek') == True 50 | assert NameManager.is_identifier_name('__kek') == True 51 | 52 | assert NameManager.is_identifier_name('.kek') == False 53 | assert NameManager.is_identifier_name('lol kek') == False 54 | -------------------------------------------------------------------------------- /polog/loggers/handle/smart_assert.py: -------------------------------------------------------------------------------- 1 | from polog.loggers.handle.abstract import AbstractHandleLogger 2 | from polog.core.stores.settings.settings_store import SettingsStore 3 | from polog import log 4 | 5 | 6 | class SmartAssert(AbstractHandleLogger): 7 | """ 8 | Класс обертки вокруг инструкции assert. 9 | 10 | Если переменная __debug__ равна True, все работает в точности как assert. 11 | Если __debug__ == False и переданное выражение эквивалентно False, записывается лог. 12 | 13 | О переменной __debug__ см. https://docs.python.org/3/using/cmdline.html#cmdoption-O и https://docs.python.org/3/reference/simple_stmts.html#assert. 14 | """ 15 | def __init__(self): 16 | self.settings = SettingsStore() 17 | 18 | def __call__(self, expression_result, *maybe_message): 19 | """ 20 | Если первый аргумент эквивалентен False - мы либо поднимаем исключение, либо пишем лог. 21 | Выбранный вариант зависит от текущей настройки debug_mode. Если debug_mode == False - поднимется исключение, иначе - пишется лог. 22 | """ 23 | message = self.get_message(expression_result, *maybe_message) 24 | try: 25 | if self.settings['smart_assert_politic'](self.settings['debug_mode'], expression_result): 26 | log(message, exception=AssertionError(message)) 27 | if self.settings['debug_mode'] and not expression_result: 28 | raise AssertionError(message) 29 | except Exception: 30 | if self.settings['debug_mode']: 31 | log(message, exception=AssertionError(message)) 32 | raise AssertionError(message) 33 | 34 | def get_message(self, expression_result, *maybe_message): 35 | """ 36 | Извлекаем сообщение из того, что было передано. 37 | 38 | Если передано сообщение как строка - используем его. Если нет - сериализуем результат выражения. 39 | Если при сериализации возникает проблема, возвращаем сообщение об ошибке. 40 | """ 41 | if len(maybe_message) > 1: 42 | self._maybe_raise(TypeError, 'The assert wrapper function accepts no more than 2 arguments: an expression and a message.') 43 | 44 | message = None 45 | default_message = 'It is impossible to extract data for the log.' 46 | 47 | try: 48 | if len(maybe_message) >= 1: 49 | message = maybe_message[0] 50 | if not isinstance(message, str): 51 | try: 52 | message = str(message) 53 | except Exception: 54 | message = expression_result 55 | if not isinstance(message, str): 56 | message = str(message) 57 | else: 58 | message = expression_result 59 | if not isinstance(message, str): 60 | message = str(message) 61 | except Exception: 62 | if not isinstance(message, str): 63 | message = default_message 64 | pass 65 | return message 66 | 67 | 68 | smart_assert = SmartAssert() 69 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/test_rotator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | import pytest 5 | 6 | from polog import log, config 7 | from polog.handlers.file.rotation.rotator import Rotator 8 | from polog.handlers.file.writer import file_writer 9 | from polog.handlers.file.file_dependency_wrapper import FileDependencyWrapper 10 | 11 | 12 | @pytest.mark.skipif('windows' in platform.system().lower(), reason="file locks don't work on windows") 13 | def test_base_behavior_rotation_file_size(number_of_strings_in_the_files, delete_files, dirname_for_test, filename_for_test): 14 | """ 15 | Проверяем, что ротация работает с набором валидных правил. 16 | 17 | Рабочая ротация означает, что: 18 | 1. Когда надо - файл с ротированными логами создается. 19 | 2. Когда файл с ротированными логами создается, в нем ровно то количество строк, которое мы записывали в исходный файл. 20 | 3. После ротации в исходном файле - 0 строк. 21 | """ 22 | variations = [ 23 | ('3 megabytes', 'kek' * 1024, 1024), 24 | ('3 kilobytes', 'kek', 1024), 25 | ('3 megabyte', 'kek' * 1024, 1024), 26 | ('3 kilobyte', 'kek', 1024), 27 | ('3 mb', 'kek' * 1024, 1024), 28 | ('3 kb', 'kek', 1024), 29 | ('1 b', 'k', 1), 30 | ] 31 | for size_limit, message, iterations in variations: 32 | 33 | handler = file_writer(filename_for_test) 34 | dirname_for_test = os.path.join(dirname_for_test, 'rotation_dir') 35 | config.add_handlers(handler) 36 | config.set(pool_size=0) 37 | 38 | for iteration in range(iterations): 39 | log(message) 40 | 41 | rotator = Rotator(f'{size_limit} >> {dirname_for_test}', FileDependencyWrapper([filename_for_test], lock_type='thread+file')) 42 | rotator.maybe_do() 43 | 44 | assert len([x for x in os.listdir(dirname_for_test) if not x.endswith('.lock')]) == 1 45 | 46 | archive_file = os.listdir(dirname_for_test)[0] 47 | archive_file = os.path.join(dirname_for_test, archive_file) 48 | 49 | assert number_of_strings_in_the_files(archive_file) == iterations 50 | assert number_of_strings_in_the_files(filename_for_test) == 0 51 | 52 | config.delete_handlers(handler) 53 | 54 | def test_wrong_rule_to_rotation(): 55 | """ 56 | Проверяем, что для неформатных правил ротации поднимается ValueError. 57 | """ 58 | with pytest.raises(ValueError): 59 | rotator = Rotator(f'lol >> kek', FileDependencyWrapper((), lock_type='thread+file')) 60 | 61 | def test_wrong_number_of_rules_to_rotation(): 62 | """ 63 | Проверяем, что, при неправильном числе элементов в выражении, поднимается исключение. Должно быть 2: справа и слева от '>>'. 64 | """ 65 | with pytest.raises(ValueError): 66 | rotator = Rotator(f'lol >>', FileDependencyWrapper((), lock_type='thread+file')) 67 | 68 | with pytest.raises(ValueError): 69 | rotator = Rotator(f'>>', FileDependencyWrapper((), lock_type='thread+file')) 70 | 71 | with pytest.raises(ValueError): 72 | rotator = Rotator(f' >> ', FileDependencyWrapper((), lock_type='thread+file')) 73 | 74 | with pytest.raises(ValueError): 75 | rotator = Rotator(f'3 kilobyte >> logs.log >> kek.log', FileDependencyWrapper((), lock_type='thread+file')) 76 | -------------------------------------------------------------------------------- /polog/core/utils/name_manager.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from polog.loggers.auto.function_logger import flog 4 | from polog.loggers.handle.handle_log import handle_log 5 | from polog.loggers.handle.message import message 6 | from polog import log 7 | 8 | 9 | @dataclass 10 | class NameEstimation: 11 | possibility: bool 12 | reason: str = None 13 | 14 | class NameManager: 15 | """ 16 | Здесь должны проверяться на допустимость все пользовательские имена. 17 | """ 18 | @classmethod 19 | def is_possible_level_name(cls, name): 20 | """ 21 | Проверяем имя на возможность использования для уровня логирования. 22 | Возвращается объект NameEstimation. 23 | """ 24 | if not cls.is_identifier_name(name): 25 | return NameEstimation( 26 | possibility=False, 27 | reason="The name of level must be python identifier compatible.", 28 | ) 29 | 30 | if name.startswith('_'): 31 | return NameEstimation( 32 | possibility=False, 33 | reason="The name of level can't start on dander symbol.", 34 | ) 35 | 36 | forbidden_names = set(dir(flog)).union(set(dir(handle_log))).union(set(dir(log))).union(set(dir(message))) 37 | if name in forbidden_names: 38 | return NameEstimation( 39 | possibility=False, 40 | reason=f'The name "{name}" cannot be used for the logging level. It is already used as a service name in the logger.', 41 | ) 42 | 43 | return NameEstimation(possibility=True) 44 | 45 | @classmethod 46 | def is_possible_extra_field_name(cls, name): 47 | """ 48 | Проверяем имя на возможность использования для извлекаемых полей. 49 | Возвращается объект NameEstimation. 50 | """ 51 | if not cls.is_identifier_name(name): 52 | return NameEstimation( 53 | possibility=False, 54 | reason="The name of field must be python identifier compatible.", 55 | ) 56 | 57 | if name.startswith('_'): 58 | return NameEstimation( 59 | possibility=False, 60 | reason="The name of field can't start on dander symbol.", 61 | ) 62 | 63 | forbidden_names = { 64 | 'level', 65 | 'auto', 66 | 'time', 67 | 'service_name', 68 | 'success', 69 | 'function', 70 | 'class', 71 | 'module', 72 | 'message', 73 | 'exception_type', 74 | 'exception_message', 75 | 'traceback', 76 | 'input_variables', 77 | 'local_variables', 78 | 'result', 79 | 'time_of_work', 80 | } 81 | if name in forbidden_names: 82 | return NameEstimation( 83 | possibility=False, 84 | reason=f'The name "{name}" occupied by built-in field, you can\'t use it.', 85 | ) 86 | 87 | return NameEstimation(possibility=True) 88 | 89 | @staticmethod 90 | def is_identifier_name(name): 91 | """ 92 | Проверка, может ли данное имя быть использовано для обозначения переменной в python. 93 | """ 94 | return name.isidentifier() 95 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/locks/test_file_lock.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process, current_process 2 | from threading import Thread 3 | import platform 4 | import shutil 5 | import os 6 | 7 | import pytest 8 | 9 | from polog.handlers.file.locks.file_lock import FileLock 10 | 11 | 12 | def process_race_condition_generator(filename, number_of_iterations, number_of_process): 13 | lock = FileLock(filename) 14 | file = open(filename, 'a', encoding='utf-8') 15 | for index in range(number_of_iterations): 16 | lock.acquire() 17 | file.write('abc\n') 18 | if index % 10: 19 | new_path = f'{filename}_{current_process()}_{index}.log' 20 | shutil.move(filename, new_path) 21 | file.close() 22 | file = open(filename, 'a', encoding='utf-8') 23 | lock.release() 24 | 25 | @pytest.mark.skipif('windows' in platform.system().lower(), reason="file locks don't work on windows") 26 | def test_file_lock_race_condition_in_processes(filename_for_test, number_of_strings_in_the_files, delete_files, dirname_for_test): 27 | """ 28 | Провоцируем состояние гонки между процессами. 29 | """ 30 | number_of_processes = 5 31 | number_of_iterations = 200 32 | 33 | processes = [Process(target=process_race_condition_generator, args=(filename_for_test, number_of_iterations, index)) for index in range(number_of_processes)] 34 | 35 | for process in processes: 36 | process.start() 37 | for process in processes: 38 | process.join() 39 | 40 | files = [filename_for_test] 41 | 42 | expected_number_of_logs = number_of_processes * number_of_iterations 43 | 44 | 45 | for filename in os.listdir(os.path.join('polog', 'tests', 'data')): 46 | if not filename.startswith('.') and not os.path.isdir(filename): 47 | files.append(os.path.join('polog', 'tests', 'data', filename)) 48 | 49 | assert number_of_strings_in_the_files(*files) == expected_number_of_logs 50 | delete_files(*[os.path.join('polog', 'tests', 'data', filename) for filename in os.listdir(os.path.join('polog', 'tests', 'data')) if not os.path.isdir(filename) and not filename == '.gitkeep']) 51 | 52 | def test_active_flag_is_working_for_file_lock(filename_for_test): 53 | """ 54 | Проверяем, что атрибут .active проставляется правильно. 55 | """ 56 | assert FileLock(filename_for_test).active == True 57 | assert FileLock(None).active == False 58 | 59 | @pytest.mark.skipif('windows' in platform.system().lower(), reason="file locks don't work on windows") 60 | def test_file_lock_race_condition_in_threads(filename_for_test): 61 | """ 62 | Проверяем, что файловый лок работает и на тредах. 63 | """ 64 | number_of_threads = 10 65 | number_of_iterations = 10000 66 | 67 | lock = FileLock(filename_for_test) 68 | counter = 0 69 | 70 | def increment(number_of_iterations): 71 | nonlocal counter 72 | 73 | for _ in range(number_of_iterations): 74 | lock.acquire() 75 | counter += 1 76 | lock.release() 77 | 78 | threads = [Thread(target=increment, args=(number_of_iterations, )) for _ in range(number_of_threads)] 79 | for thread in threads: 80 | thread.start() 81 | for thread in threads: 82 | thread.join() 83 | 84 | assert counter == number_of_threads * number_of_iterations 85 | -------------------------------------------------------------------------------- /polog/loggers/handle/handle_log.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | from polog.core.stores.settings.settings_store import SettingsStore 5 | from polog.core.stores.handlers import global_handlers 6 | from polog.loggers.handle.abstract import AbstractHandleLogger 7 | from polog.core.log_item import LogItem 8 | from polog.core.stores.fields import in_place_fields 9 | from polog.unlog import unlog 10 | 11 | 12 | class BaseLogger(AbstractHandleLogger): 13 | """ 14 | Экземпляры данного класса - вызываемые объекты, каждый вызов которых означает создание лога. 15 | """ 16 | # Ключи - названия полей, значения - функции. 17 | # Каждая из этих функций должна принимать словарь с уже ранее извлеченными значениями полей и возвращать значение поля, название которого является ключом. 18 | _default_values = { 19 | 'level': lambda fields: SettingsStore()['default_level'] if fields.get('success', True) else SettingsStore()['default_error_level'], 20 | 'time': lambda fields: datetime.datetime.fromtimestamp(time.time()), 21 | } 22 | 23 | def _specific_processing(self, fields): 24 | fields['auto'] = False 25 | service_name = self._settings['service_name'] 26 | if service_name is not None: 27 | fields['service_name'] = service_name 28 | self._extract_exception(fields, change_success=True, change_level=True) 29 | self._extract_function_data(fields) 30 | self._defaults_to_dict(fields) 31 | 32 | def _defaults_to_dict(self, fields): 33 | """ 34 | Некоторые значения являются обязательными, но от пользователя не поступили. В этом случае мы генерируем их автоматически. 35 | В словаре self._default_values по ключам в виде названий полей лога хранятся функции. Каждая такая функция должна принимать словарь с ранее уже извлеченными полями лога и возвращать содержимое обязательного поля. 36 | """ 37 | for key, get_default in self._default_values.items(): 38 | if key not in fields: 39 | fields[key] = get_default(fields) 40 | 41 | def _extract_function_data(self, fields): 42 | """ 43 | Если пользователь передал объект функции, из него извлекаются название функции и модуль, в котором она была объявлена. 44 | Также пользователь может передать название функции, в этом случае никаких преобразований с ним делаться не будет. 45 | 46 | fields - словарь с извлеченными из переданных пользователем аргументов данными. 47 | """ 48 | function = fields.get('function') 49 | if function is not None and callable(function): 50 | try: 51 | fields['function'] = function.__name__ 52 | except AttributeError: 53 | fields['function'] = str(function) 54 | try: 55 | fields['module'] = function.__module__ 56 | except AttributeError: 57 | pass 58 | 59 | def _push(self, fields): 60 | """ 61 | Создаем объект лога и передаем его в движок. 62 | Предварительно проверяем, достаточен ли уровень лога для того, чтобы это сделать, и нет ли запрета на логирование. 63 | """ 64 | if fields.get('level') >= self._settings['level']: 65 | if not unlog.get_unlog_status(): 66 | log_item = LogItem() 67 | log_item.set_data(fields) 68 | log_item.set_handlers(global_handlers) 69 | log_item.extract_extra_fields_from(in_place_fields) 70 | self._engine.write(log_item) 71 | 72 | 73 | handle_log = BaseLogger() 74 | simple_handle_log = BaseLogger(all_fields_allowed=True) 75 | -------------------------------------------------------------------------------- /polog/tests/handlers/abstract/test_base.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from polog.handlers.abstract.base import BaseHandler 6 | from polog import handle_log as log, json_vars, config 7 | 8 | 9 | class ConcreteHandler(BaseHandler): 10 | def do(self, content): 11 | log(content) 12 | 13 | def get_content(self, log): 14 | return str(log) 15 | 16 | class ErrorHandler(BaseHandler): 17 | def do(self, content): 18 | raise ValueError 19 | 20 | def get_content(self, log): 21 | return str(log) 22 | 23 | def test_filter_false(handler): 24 | """ 25 | Проверяем, что фильтр, который всегда возвращает False, блокирует запись лога. 26 | """ 27 | config.set(level=0) 28 | 29 | def false_filter(log): 30 | return False 31 | 32 | concrete = ConcreteHandler(filter=false_filter) 33 | concrete(dict(lol='kek')) 34 | 35 | time.sleep(0.0001) 36 | 37 | assert handler.last is None 38 | 39 | def test_filter_true(handler): 40 | """ 41 | Проверяем, что фильтр, который всегда возвращает True, не блокирует запись лога. 42 | """ 43 | config.set(level=0) 44 | 45 | def true_filter(log): 46 | return True 47 | 48 | concrete = ConcreteHandler(filter=true_filter) 49 | concrete(dict(message='kek')) 50 | 51 | time.sleep(0.0001) 52 | 53 | assert handler.last is not None 54 | 55 | def test_only_errors_false_true(handler): 56 | """ 57 | Проверяем, что настройка only_errors в положении False не блокирует запись логов. 58 | """ 59 | config.set(level=0) 60 | 61 | concrete = ConcreteHandler(only_errors=False) 62 | concrete(dict(message='kek', success=True)) 63 | 64 | time.sleep(0.0001) 65 | 66 | assert handler.last is not None 67 | 68 | def test_only_errors_false_false(handler): 69 | """ 70 | Проверяем, что настройка only_errors в положении False не блокирует запись логов. 71 | """ 72 | config.set(level=0) 73 | 74 | concrete = ConcreteHandler(only_errors=False) 75 | concrete(dict(message='kek', success=False)) 76 | 77 | time.sleep(0.0001) 78 | 79 | assert handler.last is not None 80 | 81 | def test_only_errors_true_false(handler): 82 | """ 83 | Проверяем, что настройка only_errors в положении True не блокирует запись логов о неуспешных операциях. 84 | """ 85 | config.set(level=0) 86 | 87 | concrete = ConcreteHandler(only_errors=True) 88 | concrete(dict(message='kek', success=False)) 89 | 90 | time.sleep(0.0001) 91 | 92 | assert handler.last is not None 93 | 94 | def test_only_errors_true_true(handler): 95 | """ 96 | Проверяем, что настройка only_errors в положении True блокирует запись логов об успешных операциях. 97 | """ 98 | config.set(level=0) 99 | 100 | concrete = ConcreteHandler(only_errors=True) 101 | concrete(dict(message='kek', success=True)) 102 | 103 | time.sleep(0.0001) 104 | 105 | assert handler.last is None 106 | 107 | def test_alt(handler): 108 | """ 109 | Проверяем, что функция alt запускается, когда в обработчике что-то пошло не так. 110 | """ 111 | config.set(level=0) 112 | 113 | def alt(log_item): 114 | log('lol') 115 | 116 | concrete = ErrorHandler(alt=alt) 117 | concrete(dict(message='kek')) 118 | 119 | time.sleep(0.0001) 120 | 121 | assert handler.last['message'] == 'lol' 122 | 123 | def test_wrong_perams(): 124 | """ 125 | Проверка, что при неправильном использовании обработчика поднимается исключение. 126 | """ 127 | with pytest.raises(ValueError): 128 | concrete = ConcreteHandler(only_errors='kek') 129 | -------------------------------------------------------------------------------- /polog/core/utils/pony_names_generator.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class PonyNamesGenerator: 5 | def get_next_pony(self, epoch=0): 6 | """ 7 | "Бесконечный" рандомный генератор имен поней из My Little Pony. 8 | Имена образуются путем рекомбинации половинок оригинальных имен из сериала. 9 | 10 | Исчерпав все варианты, повторяет их заново (но в другом порядке), добавив постфикс с римской записью поколения. К примеру: 11 | Princess Skyre II 12 | Derpy Pie II 13 | Rari Dash II 14 | 15 | Генератор реализован через рекурсию, так что технически он не совсем бесконечный, но на несколько десятков тысяч вариантов точно можно расчитывать (при проверке на MacBook 70 000 комбинаций создавались, а 80 000 уже нет). 16 | """ 17 | names = self.new_names_portion() 18 | random.shuffle(names) 19 | for name in names: 20 | if epoch == 0: 21 | yield name 22 | else: 23 | yield f'{name} {self.roman_numerals(epoch + 1)}' 24 | epoch += 1 25 | yield from self.get_next_pony(epoch=epoch) 26 | 27 | @classmethod 28 | def new_names_portion(cls): 29 | """ 30 | Данный метод возвращает список имен, причем всегда одинаковый и в одинаковом порядке. 31 | """ 32 | container = [] 33 | cls.halfs_combinations( 34 | container, 35 | first_halfs = [ 36 | 'Twilight', 37 | 'Apple', 38 | 'Flutter', 39 | 'Rari', 40 | 'Pinkie', 41 | 'Rainbow', 42 | 'Derpy', 43 | ], 44 | second_halfs = [ 45 | ' Sparkle', 46 | 'jack', 47 | 'shy', 48 | 'ty', 49 | ' Pie', 50 | ' Dash', 51 | ' Hooves', 52 | ], 53 | ) 54 | cls.halfs_combinations( 55 | container, 56 | first_halfs = [ 57 | 'Cad', 58 | 'Sky', 59 | 'Amo', 60 | 'Cele', 61 | 'Lu', 62 | ], 63 | second_halfs = [ 64 | 'ance', 65 | 'star', 66 | 're', 67 | 'stia', 68 | 'na', 69 | ], 70 | prefix='Princess ' 71 | ) 72 | return container 73 | 74 | @staticmethod 75 | def halfs_combinations(container, first_halfs, second_halfs, prefix=''): 76 | """ 77 | Берем два списка с половинками имен и кладем их декартово произведение в список container. 78 | При необходимости, добавляем префиксы. 79 | """ 80 | for half in first_halfs: 81 | for half_2 in second_halfs: 82 | new_name = f'{prefix}{half}{half_2}' 83 | container.append(new_name) 84 | 85 | @staticmethod 86 | def roman_numerals(number): 87 | """ 88 | Генератор римских цифр, взят отсюда: 89 | https://py.checkio.org/mission/roman-numerals/publications/mdeakyne/python-3/first/share/53882d47af904f942fc8daf06c0ed270/ 90 | """ 91 | if number > 0: 92 | ones = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"] 93 | tens = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"] 94 | hunds = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"] 95 | thous = ["", "M", "MM", "MMM", "MMMM"] 96 | thous = thous[number // 1000] 97 | hunds = hunds[number // 100 % 10] 98 | tens = tens[number // 10 % 10] 99 | ones = ones[number % 10] 100 | return thous + hunds + tens + ones 101 | -------------------------------------------------------------------------------- /polog/core/engine/real_engines/multithreaded/engine.py: -------------------------------------------------------------------------------- 1 | from polog.core.engine.real_engines.multithreaded.pool import ThreadPool 2 | from polog.core.engine.real_engines.abstract import AbstractRealEngine 3 | 4 | 5 | class MultiThreadedRealEngine(AbstractRealEngine): 6 | """ 7 | Многопоточная реализация движка. 8 | 9 | Состоит из очереди, куда отправляются данные о событиях, и некоторого количества "воркеров", т. е. зацикленных функций в отдельных потоках, которые запрашивают новые события из очереди, и сразу обрабатывают их. 10 | Таким образом, каждый поток берет из очереди следующее событие сразу после того, как разобрался с предыдущим. 11 | 12 | Многопоточная реализация движка подразумевает 2 главных риска: 13 | 1. Отсутствие гарантии правильной последовательности записи логов. Логи попадают в очередь обработки практически в правильном порядке, однако забирают их оттуда воркеры, каждый из которых может отрабатывать разное время, в результате чего запись логов в конкретное хранилище может осуществляться уже в неправильном порядке. Обычно это не имеет значения, однако стоит иметь ввиду. 14 | 2. Возможность потери не успевших записаться логов в случае внештатного прекращения работы программы, например при внезапном отключении электричества. В этом случае те логи, которые все еще ждали своего часа в очереди, а также те, с которыми в тот момент работали обработчики, просто теряются. К сожалению, этот риск для асинхронных движков логирования непреодолим ввиду самого принципа их работы. 15 | 16 | Стоит отметить, что при штатном завершении программы все логи должны успеть записаться. Для этого через atexit регистрируется специальный обработчик, который следит, что программа будет завершена не раньше, чем очередь опустошится, и каждый из воркеров корректно завершит свою работу. Как уже было сказано, угроза потери данных актуальна только в случае нештатного прекращения работы программы - такого, как отключение питания компьютера. 17 | 18 | Кроме того, многопоточная реализация движка подразумевает некоторые дополнительные накладные расходы и в некоторых (очень редких) случаях може приводить к замедлению работы относительно синхронной реализации. Основные расходы: 19 | 1. Создаются дополнительные потоки, а значит, появляются доп. накладные расходы на переключение между ними и работу GIL. 20 | 2. Не успевшие записаться логи хранятся в очереди, то есть забирают часть оперативной памяти. В некоторых случаях, если логов пишется очень много, и обработчики не успевают их обрабатывать с той же скоростью, с какой они попадают в очередь, это может привести к переполнению памяти и остановке работы програаммы. Чтобы убрать этот риск, следует правильно подбирать нужное количество воркеров, желательно с запасом. Также можно установить лимит на максимальное число событий в очереди. 21 | 22 | При работе с многопоточным движком важно правильно выбирать нужное количество воркеров. Если их слишком много, большинство будет простаивать, отжирая вычислительную мощность. Если слишком мало, они не будут успевать обработать все логи в очереди. Выбирать количество воркеров нужно в том числе с учетом типа обработчиков. Некоторые обработчики - например те, что подолгу ожидают ответа сети - создают мало CPU-нагрузки, и в этом случае имеет смысл увеличить число воркеров. 23 | """ 24 | 25 | def __init__(self, settings): 26 | super().__init__(settings) 27 | self.pool = ThreadPool(settings) 28 | 29 | def write(self, log_item): 30 | """ 31 | Кладем аргументы оригинальной функции и извлеченные логгером данные в очередь на запись. 32 | """ 33 | self.pool.put(log_item) 34 | 35 | def stop(self): 36 | self.pool.stop() 37 | 38 | def queue_size(self): 39 | """ 40 | ПРИМЕРНЫЙ размер очереди, см. документацию: 41 | https://docs.python.org/3/library/queue.html#queue.Queue.qsize 42 | """ 43 | return self.pool.queue.qsize() 44 | -------------------------------------------------------------------------------- /polog/handlers/file/rotation/rules/rules/tokenization/tokens/abstractions/abstract_token.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.abstractions.meta_token import MetaToken 2 | 3 | 4 | class AbstractToken(metaclass=MetaToken): 5 | """ 6 | Базовый класс, от которого должны быть унаследованы все токены. 7 | 8 | Исходная строка, подаваемая пользователем, бьется на токены. 9 | Токены не должны отличаться для вызывающего кода ничем, вызывающий код не должен как-то учитывать особенности реализации каждого конкретного токена. 10 | 11 | Разбиение текста на токены позволяет проверять его на соответствие определенным правилам. Скажем, можно проверять порядок последовательности токенов, не зная, что лежит в каждом из них. 12 | 13 | Каждый унаследованный класс токена должен гарантировать, что работает только с подстроками подходящей ему формы. 14 | """ 15 | 16 | # Движок регулярных выражений использует данное поле для проверки соответствия последовательности определенным правилам. 17 | # В унаследованных классах здесь должна быть строка длиной ровно в 1 символ. 18 | # Символ '*' (звездочка), а также '[' и ']', зарезервированы для использования внутри движка регулярных выражений. 19 | # Соблюдение данных условий будет автоматически проверено на уровне метакласса. 20 | regexp_letter = None 21 | 22 | def __init__(self, source): 23 | """ 24 | На вход подается некий кусок исходной строки. 25 | 26 | Мы делаем с ним 2 вещи: 27 | 1. Проверяем, что он подходит для генерации данного типа токена. То есть, скажем, подстрока "ekfnjfn" вряд ли годится для преобразования в число, и проверку такого рода она пройти не должна. 28 | Если проверка не пройдена - поднимаем исключение. Вызывающий код должен рассматривать исключения, происходящие при инициализации объектов данного класса, как часть логики. 29 | 2. Извлекаем из подстроки значение, которое кладем в self.content. Это может быть что угодно, в т. ч., скажем, кортеж. 30 | """ 31 | self.source = source 32 | if not self.its_me(self.source): 33 | raise ValueError(f'The substring "{self.source}" is not valid for use to generate the {type(self).__name__}.') 34 | self.content = self.parse() 35 | 36 | def __repr__(self): 37 | name = self.__class__.__name__ 38 | content = f'"{self.content}"' if type(self.content) is str else f'{self.content}' 39 | base = f'{name}({content})' 40 | return base 41 | 42 | def __eq__(self, other): 43 | """ 44 | Токены можно проверять на равенство. 45 | Если другой токен того же класса и с тем же значением, что текущий, возвращается True, иначе False. 46 | """ 47 | if self.__class__ is other.__class__: 48 | if self.content == other.content: 49 | return True 50 | return False 51 | 52 | @classmethod 53 | def its_me(cls, chunk): 54 | """ 55 | Абстрактный метод. 56 | Проверка, что поданный кусок строки является валидным для преобразования в данный тип токенов. 57 | 58 | Возвращается bool по результатам проверки. 59 | """ 60 | raise NotImplementedError() # pragma: no cover 61 | 62 | def parse(self): 63 | """ 64 | Абстрактный метод. 65 | Здесь self.source преобразовывается в self.content. 66 | 67 | Возвращаемое значение может быть произвольного типа, в зависимости от природы токена. Скажем, для числа это может быть int, а для строки - str. 68 | """ 69 | raise NotImplementedError() # pragma: no cover 70 | 71 | def equal(self, other_string): 72 | """ 73 | Движок регулярных выражений позволяет проверять токен на точное соответствие какой-то подстроке. 74 | Если класс конкретного токена поддерживает такие сравнения, следует переопределить данный метод. 75 | По умолчанию результатом таких сравнений всегда будет False. 76 | """ 77 | return False 78 | -------------------------------------------------------------------------------- /polog/loggers/auto/class_logger.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from polog.loggers.auto.function_logger import flog 4 | from polog.core.stores.registering_functions import RegisteringFunctions 5 | from polog.errors import IncorrectUseOfTheDecoratorError 6 | 7 | 8 | class ClassLogger: 9 | """ 10 | Экземпляры данного класса - готовые декораторы для других классов. 11 | """ 12 | 13 | def __call__(self, suppress_all, suppressed_exceptions, *args, methods=(), message=None, level=None, errors_level=None): 14 | """ 15 | Фабрика декораторов классов. Можно вызывать как со скобками, так и без. 16 | В задекорированном классе @flog() применяется ко всем методам, кроме тех, чье название начинается с '__'. 17 | Приоритет декорирования через класс ниже, чем при прямом декорировании метода с помощью @flog() самим пользователем. Если пользователь навесил на метод задекорированного класса еще @flog(), то применяться будет только декоратор из flog(). 18 | """ 19 | def decorator(Class): 20 | # Получаем имена методов класса. 21 | all_methods = self.get_logging_methods(Class, *methods) 22 | for method_name in all_methods: 23 | method = getattr(Class, method_name) 24 | # Конфигурируем декоратор для метода. 25 | wrapper = flog(suppress_all, suppressed_exceptions, message=message, level=level, errors_level=errors_level, is_method=True) 26 | # Применяем его. 27 | new_method = wrapper(method) 28 | setattr(Class, method_name, new_method) 29 | # Получаем кортеж с именами методов, которые логировать НЕ надо. 30 | # Если они уже залогированы - нужно вернуть оригиналы. 31 | originals = self.make_originals(Class, *methods) 32 | register = RegisteringFunctions() 33 | for method_name in originals: 34 | method = getattr(Class, method_name) 35 | original = register.get_original(method) 36 | setattr(Class, method_name, original) 37 | return Class 38 | if not len(args): 39 | return decorator 40 | elif len(args) == 1 and inspect.isclass(args[0]): 41 | return decorator(args[0]) 42 | raise IncorrectUseOfTheDecoratorError('The @clog decorator could be used only for classes.') 43 | 44 | def make_originals(self, Class, *methods): 45 | """ 46 | Возвращаем кортеж с названиями всех методов класса, за исключением возвращенных через self.get_logging_methods(). 47 | То есть, фактически, все методы, которые НЕ надо логировать. 48 | Используется для того, чтобы в дочерних классах подменить данные методы на оригиналы. 49 | """ 50 | if methods: 51 | all = self.get_logging_methods(Class) 52 | methods = set(methods) 53 | result = tuple([x for x in all if x not in methods]) 54 | return result 55 | else: 56 | return () 57 | 58 | @staticmethod 59 | def get_logging_methods(Class, *methods): 60 | """ 61 | Метод, который берет на вход объект класса и кортеж с названиями методов, и возвращает список с названиями методов. 62 | 63 | Если кортеж на входе пустой - возвращается список с названиями всех методов класса, которые не начинаются и заканчиваются на '__'. 64 | Иначе - названия из кортежа просто перекладываются в список. 65 | 66 | Семантика тут такая: если пользователь не указал конкретно имена методов, которые нужно логировать - логиируем весь класс. Иначе - только выбранные методы. 67 | """ 68 | if not len(methods) or (len(methods) == 1 and inspect.isclass(methods[0])): 69 | methods = [func for func in dir(Class) if callable(getattr(Class, func)) and (not func.startswith('__') and not func.endswith('__'))] 70 | else: 71 | methods = [x for x in methods] 72 | return methods 73 | 74 | 75 | clog = ClassLogger() 76 | -------------------------------------------------------------------------------- /polog/handlers/file/writer.py: -------------------------------------------------------------------------------- 1 | from polog.handlers.abstract.base import BaseHandler 2 | from polog.core.utils.signature_matcher import SignatureMatcher 3 | from polog.handlers.file.file_dependency_wrapper import FileDependencyWrapper 4 | from polog.handlers.file.base_formatter import BaseFormatter 5 | from polog.handlers.file.rotation.rotator import Rotator 6 | from polog.core.utils.exception_escaping import exception_escaping 7 | 8 | 9 | class file_writer(BaseHandler): 10 | """ 11 | Класс-обработчик для логов. 12 | Объект класса является вызываемым благодаря наличию метода .__call__(). 13 | При вызове экземпляра класса, происходит запись лога в файл. 14 | Поддерживаются ротация логов, фильтрация и форматирование. 15 | """ 16 | 17 | input_proves = { 18 | 'forced_flush': lambda x: isinstance(x, bool), 19 | 'separator': lambda x: isinstance(x, str), 20 | 'formatter': lambda x: x is None or SignatureMatcher.is_handler(x), 21 | 'rotation': lambda x: x is None or isinstance(x, str), 22 | } 23 | 24 | def __init__(self, *file, formatter=None, rotation=None, forced_flush=True, separator='\n', only_errors=False, filter=None, alt=None, file_wrapper=FileDependencyWrapper, base_formatter=BaseFormatter, rotator=Rotator, lock_type='thread'): 25 | super().__init__(only_errors=only_errors, filter=filter, alt=alt) 26 | self.do_input_proves(forced_flush=forced_flush, separator=separator, formatter=formatter, rotation=rotation) 27 | self.file = file_wrapper([x for x in file], lock_type) 28 | self.forced_flush = forced_flush 29 | self.base_formatter = base_formatter(separator) 30 | self.formatter = self.get_formatter(formatter) 31 | self.rotator = self.get_rotator(rotator, rotation) 32 | 33 | def do(self, content): 34 | """ 35 | Данная функция вызывается непосредственно для записи лога. 36 | Помимо записи, опционально тут вызывается ротация (в зависимости от установленных настроек ротации) и сброс буфера. 37 | """ 38 | # Проверка, нужна ли сейчас ротация логов. Если нужна - ротируем, после чего уже переходим к записи нового лога. 39 | self.maybe_rotation() 40 | # Запись лога в файл. 41 | self.file.write(content) 42 | # Сброс буфера вывода. Осуществляется по умолчанию, это можно настроить при инициализации обработчика. 43 | self.maybe_flush() 44 | 45 | def get_content(self, log): 46 | """ 47 | Стандартный метод для создания строки лога из исходных данных. Использует стандартный форматтер. 48 | """ 49 | return self.formatter(log) 50 | 51 | def maybe_flush(self): 52 | """ 53 | Проверяем, нужен ли в данном случае сброс буфера файлового вывода. 54 | Если да - сбрасываем, то есть записываем в файл весь буфер. 55 | """ 56 | if self.forced_flush: 57 | self.file.flush() 58 | 59 | @exception_escaping 60 | def maybe_rotation(self): 61 | """ 62 | Проверяем, нужна ли ротация логов при данном вызове. 63 | Если да - ротируем. 64 | """ 65 | self.rotator.maybe_do() 66 | 67 | def get_formatter(self, maybe_formatter): 68 | """ 69 | Возвращаем форматтер - некую функцию или класс. 70 | Его задача - преобразовывать "сырые" данные лога в строку для записи в файл. 71 | Пользователь может передать сюда свой форматтер. Если он этого не сделал, берем стандартный метод форматирования. 72 | """ 73 | if callable(maybe_formatter): 74 | return maybe_formatter 75 | return self.base_formatter_wrapper 76 | 77 | def get_rotator(self, rotator, rotation_rules): 78 | """ 79 | Возвращаем ротатор - объект класса, ответственного за ротацию. 80 | У ротатора обязан присутствовать метод .maybe_do(). 81 | """ 82 | return rotator(rotation_rules, self.file) 83 | 84 | def base_formatter_wrapper(self, log): 85 | """ 86 | Метод, где вызывается базовый форматтер. 87 | """ 88 | return self.base_formatter.get_formatted_string(log) 89 | -------------------------------------------------------------------------------- /polog/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import uuid 4 | from multiprocessing import set_start_method 5 | 6 | import pytest 7 | from termcolor import colored 8 | 9 | from polog import config 10 | from polog.core.stores.settings.settings_store import SettingsStore 11 | from polog.handlers.memory.saver import memory_saver 12 | 13 | 14 | set_start_method('spawn') 15 | 16 | 17 | @pytest.fixture 18 | def handler(): 19 | """ 20 | Получаем стандартный обработчик, сохраняющий логи в оперативную память. 21 | """ 22 | new_handler = memory_saver() 23 | new_handler.clean() 24 | try: 25 | config.add_handlers(new_handler) 26 | except ValueError as e: 27 | pass 28 | return new_handler 29 | 30 | @pytest.fixture 31 | def empty_class(): 32 | """ 33 | Заготовка класса. 34 | """ 35 | class EmptyClass: 36 | pass 37 | return EmptyClass 38 | 39 | @pytest.fixture 40 | def settings_mock(): 41 | """ 42 | Подмена экземпляра класса настроек. 43 | """ 44 | class SettingsMock: 45 | def __init__(self): 46 | self.points = {'started': True, 'pool_size': 2, 'max_delay_before_exit': 0.001, 'max_queue_size': 50, 'time_quant': 0.001, 'service_name': 'kek', 'delay_on_exit_loop_iteration_in_quants': 10} 47 | self.handlers = {} 48 | self.fields = {} 49 | def __getitem__(self, key): 50 | return self.points[key] 51 | def __setitem__(self, key, value): 52 | points[key] = value 53 | def force_get(self, key): 54 | return self.points[key] 55 | return SettingsMock() 56 | 57 | @pytest.fixture 58 | def delete_files(): 59 | """ 60 | Функция, удаляющая все файлы, пути к которым были переданы в качестве аргументов. 61 | """ 62 | def result(*files): 63 | for file in files: 64 | try: 65 | os.remove(file) 66 | except FileNotFoundError: 67 | pass 68 | except PermissionError as e: 69 | try: 70 | os.rmdir(file) 71 | except NotADirectoryError: 72 | pass 73 | except PermissionError: 74 | pass 75 | return result 76 | 77 | @pytest.fixture 78 | def number_of_strings_in_the_files(): 79 | """ 80 | Функция, подсчитывающая не пустые строки в файлах, пути к которым были переданы в качестве аргументов. 81 | """ 82 | def result(*paths): 83 | result = 0 84 | for path in paths: 85 | try: 86 | with open(path, 'r') as file: 87 | for line in file: 88 | if line: 89 | result += 1 90 | except FileNotFoundError: 91 | pass 92 | except IsADirectoryError: 93 | pass 94 | return result 95 | return result 96 | 97 | @pytest.fixture 98 | def filename_for_test(dirname_for_test): 99 | """ 100 | Получаем имя файла в тестовой директории и удаляем за собой файл. 101 | """ 102 | yield os.path.join(dirname_for_test, f'data_{uuid.uuid1().hex}.log') 103 | 104 | @pytest.fixture 105 | def dirname_for_test(delete_files): 106 | """ 107 | Получаем имя файла в тестовой директории и удаляем за собой файл. 108 | """ 109 | path = os.path.join('polog', 'tests', 'data') 110 | shutil.rmtree(path, ignore_errors=True) 111 | try: 112 | os.makedirs(path) 113 | except FileExistsError: 114 | pass 115 | yield path 116 | shutil.rmtree(path, ignore_errors=True) 117 | try: 118 | os.makedirs(path) 119 | except FileExistsError: 120 | pass 121 | open(os.path.join(path, '.gitkeep'), 'w').close() 122 | 123 | @pytest.hookimpl 124 | def pytest_runtest_makereport(item, call): 125 | """ 126 | Хук, добавляющий информацию о текущих настройках к каждому выводу об ошибке в тесте. 127 | """ 128 | if call.when == 'call': 129 | if call.excinfo: 130 | item.add_report_section("call", "config", colored(str(SettingsStore()), 'cyan')) 131 | -------------------------------------------------------------------------------- /polog/tests/handlers/file/rotation/rules/rules/tokenization/tokens/test_tokens_group.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens.tokens_group import TokensGroup 4 | from polog.handlers.file.rotation.rules.rules.tokenization.tokens import SizeToken, NumberToken, DotToken 5 | 6 | 7 | def test_check_regexp_base_behavior(): 8 | """ 9 | Пробуем разные паттерны. 10 | """ 11 | group = TokensGroup([NumberToken('20'), DotToken('milliwatt')]) 12 | assert group.check_regexp('..') 13 | assert group.check_regexp('**') 14 | assert group.check_regexp('*.*') 15 | assert group.check_regexp('*n*') 16 | assert group.check_regexp('*d*') 17 | assert group.check_regexp('*nd*') 18 | assert group.check_regexp('*n*d*') 19 | assert group.check_regexp('*.*d*') 20 | assert group.check_regexp('*.*.*') 21 | assert group.check_regexp('*n*.*') 22 | assert group.check_regexp('n.') 23 | assert group.check_regexp('**..*') 24 | assert group.check_regexp('n*') 25 | assert group.check_regexp('.*') 26 | assert group.check_regexp('*.') 27 | assert group.check_regexp('.*.') 28 | assert group.check_regexp('.[20]*.') 29 | assert group.check_regexp('.[20]*.[milliwatt]') 30 | assert group.check_regexp('*') 31 | assert group.check_regexp('nd[milliwatt]') 32 | assert group.check_regexp('.d[milliwatt]') 33 | assert group.check_regexp('n[20]d') 34 | assert group.check_regexp('nd') 35 | assert group.check_regexp('.*') 36 | assert group.check_regexp('.d') 37 | assert group.check_regexp('*********') 38 | assert group.check_regexp('*d[milliwatt]') 39 | assert group.check_regexp('n[20]d[milliwatt]') 40 | assert not group.check_regexp('nd[milliwatts]') 41 | assert not group.check_regexp('nd[lol]') 42 | assert not group.check_regexp('nd[kek]') 43 | assert not group.check_regexp('n[21]d') 44 | assert not group.check_regexp('.[21]*.') 45 | assert not group.check_regexp('ns') 46 | assert not group.check_regexp('.') 47 | assert not group.check_regexp('...') 48 | assert not group.check_regexp('*d[milliwatts]') 49 | assert not group.check_regexp('n[21]d[milliwatt]') 50 | assert not group.check_regexp('n[20]d[milliwatts]') 51 | 52 | group = TokensGroup([NumberToken('20'), SizeToken('mb'), DotToken('kek'), NumberToken('1')]) 53 | assert group.check_regexp('....') 54 | assert group.check_regexp('....*') 55 | assert group.check_regexp('...*') 56 | assert group.check_regexp('..*') 57 | assert group.check_regexp('.*') 58 | assert group.check_regexp('*') 59 | assert group.check_regexp('*....*') 60 | assert group.check_regexp('***....***') 61 | assert group.check_regexp('***.*.*.*.***') 62 | assert group.check_regexp('***.**.*.***') 63 | assert group.check_regexp('***.***.***') 64 | assert group.check_regexp('*****.****') 65 | assert group.check_regexp('*********') 66 | assert group.check_regexp('****.[mb]*****') 67 | assert group.check_regexp('****.[20]*****') 68 | assert group.check_regexp('****.[kek]*****') 69 | assert group.check_regexp('****.[1]*****') 70 | assert group.check_regexp('n[20]s[mb]d[kek]n[1]') 71 | assert group.check_regexp('n[20].[mb]d[kek]n[1]') 72 | assert group.check_regexp('n[20].[mb].[kek]n[1]') 73 | assert group.check_regexp('n[20].[mb].[kek].[1]') 74 | assert group.check_regexp('.[20].[mb].[kek].[1]') 75 | assert group.check_regexp('nsdn') 76 | assert not group.check_regexp('.[200].[mb].[kek].[1]') 77 | assert not group.check_regexp('.[20].[mbs].[kek].[1]') 78 | assert not group.check_regexp('.[20].[mb].[keks].[1]') 79 | assert not group.check_regexp('.[20].[mb].[kek].[11]') 80 | assert not group.check_regexp('.*.[11]') 81 | assert not group.check_regexp('.*....') 82 | assert not group.check_regexp('.....') 83 | assert not group.check_regexp('nsdn.') 84 | 85 | def test_no_content_to_wildcard(): 86 | """ 87 | Проверяем, что для символа звездочки нельзя указать точное значение. 88 | """ 89 | group = TokensGroup([NumberToken('20'), DotToken('milliwatt')]) 90 | with pytest.raises(ValueError): 91 | group.check_regexp('**[milliwatt]') 92 | -------------------------------------------------------------------------------- /polog/core/utils/time_limit.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | from functools import wraps 3 | try: 4 | import thread 5 | except ImportError: 6 | import _thread as thread 7 | 8 | from polog.core.utils.signature_matcher import SignatureMatcher 9 | 10 | 11 | class time_limit: 12 | """ 13 | Класс, предназначенный для использования в качестве декоратора для функций. 14 | Устанавливает лимит времени для их выполнения. 15 | 16 | По истечении таймаута, поднимается исключение TimeoutError. 17 | 18 | В связи с особенностями реализации, есть несколько ограничений на использование такого рода таймаутов: 19 | 1. Исключение выбрасывается всегда в основном потоке. То есть, при использовании данного декоратора не в основном потоке, там исполнение кода остановлено не будет. 20 | 2. Таймаут может не сработать в случае использования time.sleep() или бинарных расширений для Python (см. https://stackoverflow.com/a/31667005/14522393). 21 | 3. При попытке остановить программу через ctrl+c в момент, пока исполняется функция под данным декоратором, это будет интерпретировано декоратором как окончание таймаута и перехвачено. 22 | """ 23 | def __init__(self, delay): 24 | """ 25 | Установка параметра для декоратора. 26 | 27 | delay - это либо число (int или float) больше нуля, либо функция без аргументов, которая возвращает такое число. Если передана функция, она будет вызвана непосредственно перед выполнением задекорированной функции, что позволяет устанавливать таймаут отложенно или даже изменять его в процессе выполнения. 28 | Если в качестве delay была передана функция, и эта функция возвращает не число больше нуля, таймаут установлен не будет и задекорированная функция будет выполняться столько, сколько захочет. 29 | """ 30 | self.delay = self.get_delay_function(delay) 31 | 32 | def __call__(self, func): 33 | """ 34 | Непосредственно функция-декоратор. 35 | 36 | Отсчет времени реализован через threading.Timer, где регистрируется обработчик, вызывающий thread.interrupt_main() (функцию, поднимающую KeyboardInterrupt в основном потоке). KeyboardInterrupt перехватывается и преобразуется в TimeoutError. 37 | """ 38 | @wraps(func) 39 | def wrapper(*args, **kwargs): 40 | try: 41 | delay = self.delay() 42 | if (not isinstance(delay, int) and not isinstance(delay, float)) or (delay <= 0): 43 | raise ValueError 44 | except Exception as e: 45 | return func(*args, **kwargs) 46 | def raise_error(): 47 | thread.interrupt_main() 48 | timer = Timer(delay, raise_error) 49 | try: 50 | timer.start() 51 | return func(*args, **kwargs) 52 | except KeyboardInterrupt: 53 | raise TimeoutError(f'The operation time of the function "{func.__name__}" exceeded the timeout of {delay} seconds.') 54 | finally: 55 | timer.cancel() 56 | return wrapper 57 | 58 | def get_delay_function(self, delay): 59 | """ 60 | Проверка пользовательского ввода и его преобразование к единому формату. 61 | 62 | Если в качестве аргумента delay было передано число, оно запаковывается в функцию, которая вернет это число. 63 | Если передана функция, возвращается эта же функция. 64 | 65 | В случае, если передно число больше нуля, функция с неправильной сигнатурой (должна быть без аргументов) или что-то еще - будет поднято ValueError. 66 | """ 67 | base_error_message = f"You can pass as an argument to {type(self).__name__} a function that takes a single positional argument, which will return a number, or directly a number." 68 | if callable(delay): 69 | matcher = SignatureMatcher() 70 | if matcher.match(delay): 71 | return delay 72 | raise ValueError(f'{base_error_message}. You passed a function whose signature does not match the expected one.') 73 | if isinstance(delay, int) or isinstance(delay, float): 74 | if delay > 0: 75 | return lambda: delay 76 | raise ValueError(f'{base_error_message}. You have passed a number that is less than zero. A meaningful designation of the delay is only numbers greater than zero.') 77 | raise ValueError(base_error_message) 78 | -------------------------------------------------------------------------------- /polog/unlog.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | from contextvars import ContextVar 3 | from functools import wraps 4 | from dataclasses import dataclass 5 | from typing import Optional 6 | from inspect import iscoroutinefunction 7 | 8 | from polog.core.stores.registering_functions import RegisteringFunctions 9 | from polog.errors import IncorrectUseOfTheDecoratorError 10 | from polog.core.stores.settings.settings_store import SettingsStore 11 | 12 | 13 | context = ContextVar('unlog') 14 | 15 | 16 | @dataclass 17 | class UnlogOrder: 18 | full: Optional[bool] 19 | 20 | class UnlogDecorator: 21 | def __init__(self): 22 | self.store = SettingsStore() 23 | 24 | def __call__(self, *args, full=None): 25 | """ 26 | Фабрика декораторов, запрещающих логирование внутри обернутых ими функций и классов. 27 | 28 | Если декоратором оборачивается класс, в нем игнорируются дандер-методы. 29 | """ 30 | def decorator(obj): 31 | if isclass(obj): 32 | for function_name in dir(obj): 33 | if not function_name.startswith('__'): 34 | maybe_method = getattr(obj, function_name) 35 | if callable(maybe_method): 36 | setattr(obj, function_name, self.unlog_function(maybe_method, full)) 37 | return obj 38 | elif callable(obj): 39 | return self.unlog_function(obj, full) 40 | else: 41 | raise IncorrectUseOfTheDecoratorError('The unlogging decorator can only be used for functions and classes.') 42 | 43 | if len(args) == 1: 44 | return decorator(args[0]) 45 | elif not args: 46 | return decorator 47 | else: 48 | raise IncorrectUseOfTheDecoratorError('The unlogging decorator can only be used for functions and classes, without any other addictional arguments.') 49 | 50 | def __neg__(self): 51 | """ 52 | При отрицании декоратора @unlog он возвращает log(). 53 | Это нужно для симметрии, поскольку -log возвращает @unlog. 54 | """ 55 | from polog import log 56 | 57 | return log 58 | 59 | def unlog_function(self, function, full): 60 | """ 61 | Запрет логирования функции. 62 | 63 | Он достигается через внесение id функции в реестр функций, который запрещено декорировать, через класс RegisteringFunctions. 64 | Если функция уже была задекорирована логгером, возвращается ее оригинал, до декорирования. Класс RegisteringFunctions хранит ссылку на оригинальную версию каждой задекорированной функции и возвращает ее по запросу. 65 | """ 66 | if not RegisteringFunctions().is_decorator(function): 67 | RegisteringFunctions().forbid(function) 68 | result = function 69 | else: 70 | original_function = RegisteringFunctions().get_original(function) 71 | RegisteringFunctions().remove(function) 72 | RegisteringFunctions().forbid(original_function) 73 | result = original_function 74 | 75 | wrapped_result = self.set_unlogged(result, full) 76 | RegisteringFunctions().add_unlogged(wrapped_result, result) 77 | return wrapped_result 78 | 79 | def get_unlog_status(self): 80 | """ 81 | В этом методе решается, позволительно ли в данный момент логирование. 82 | """ 83 | status = context.get(None) 84 | 85 | if status is None: 86 | return False 87 | 88 | if status.full is None: 89 | return self.store['full_unlog'] 90 | 91 | return status.full 92 | 93 | def set_unlogged(self, function, full): 94 | @wraps(function) 95 | def wrapper(*args, **kwargs): 96 | context.set(UnlogOrder(full=full)) 97 | try: 98 | return function(*args, **kwargs) 99 | finally: 100 | context.set(None) 101 | @wraps(function) 102 | async def async_wrapper(*args, **kwargs): 103 | context.set(UnlogOrder(full=full)) 104 | try: 105 | return await function(*args, **kwargs) 106 | finally: 107 | context.set(None) 108 | if iscoroutinefunction(function): 109 | return async_wrapper 110 | return wrapper 111 | 112 | 113 | unlog = UnlogDecorator() 114 | -------------------------------------------------------------------------------- /polog/data_structures/wrappers/weak_linked/dictionary.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | from threading import RLock 3 | from contextlib import suppress 4 | 5 | 6 | class LockedWeakKeyValueDictionary: 7 | """ 8 | Словарь, в котором и ключи и значения являются слабыми ссылками. 9 | Он автоматически очищается от элементов, чьи счетчики ссылок оказываются равными 0. Это касается как ключей, так и значений. 10 | """ 11 | def __init__(self): 12 | self.lock = RLock() 13 | self.data = weakref.WeakKeyDictionary() 14 | 15 | def __setitem__(self, key, value): 16 | """ 17 | Сохраняем в словаре значение по заданному ключу. 18 | 19 | Ни ключом, ни значением не может быть None. 20 | Также в качестве ключей и значений нельзя использовать объекты типов данных, на которые невозможно создать слабые сылки. 21 | """ 22 | if value is None: 23 | raise TypeError("The value can't be None.") 24 | weak_key = weakref.ref(key) 25 | with self.lock: 26 | def finalizer(rf): 27 | with suppress(TypeError): 28 | del self[weak_key()] 29 | self.data[key] = weakref.ref(value, finalizer) 30 | 31 | def __getitem__(self, key): 32 | """ 33 | Получаем значение по ключу. 34 | """ 35 | with self.lock: 36 | data = self.get(key) 37 | if data is None: 38 | raise KeyError(key) 39 | return data 40 | 41 | def __delitem__(self, key): 42 | """ 43 | Удаляем значение по ключу. 44 | """ 45 | with self.lock: 46 | del self.data[key] 47 | 48 | def __str__(self): 49 | """ 50 | Строковая репрезентация словаря. 51 | """ 52 | with self.lock: 53 | local_data = ', '.join([f'{key}: ' + str(value()) if not isinstance(value(), str) else f'"{value}"' for key, value in self.data.items()]) 54 | if not local_data: 55 | return f'<{type(self).__name__} object (empty)>' 56 | return f'<{type(self).__name__} object with data: {{{local_data}}}>' 57 | 58 | def __contains__(self, key): 59 | """ 60 | Проверяем, что указанный элемент присутствует в словаре в качестве ключа. 61 | """ 62 | return key in self.data 63 | 64 | def __hash__(self): 65 | """ 66 | Словарь не является хэшируемым типом данных, при попытке взять хэш поднимается исключение. 67 | """ 68 | raise TypeError(f"unhashable type: '{type(self).__name__}'") 69 | 70 | def __len__(self): 71 | """ 72 | Количество элементов в словаре. 73 | """ 74 | return len(self.data) 75 | 76 | def __iter__(self): 77 | """ 78 | Итерируемся по ключам словаря. 79 | """ 80 | for key in self.keys(): 81 | yield key 82 | 83 | def get(self, key): 84 | """ 85 | Получаем значение по ключу. 86 | """ 87 | with self.lock: 88 | weak_data = self.data.get(key) 89 | if weak_data is not None: 90 | return weak_data() 91 | 92 | def keys(self): 93 | """ 94 | Итерируемся по ключам словаря. 95 | """ 96 | return self.data.keys() 97 | 98 | def values(self): 99 | """ 100 | Итерируемся по значениям словаря. 101 | """ 102 | for value in self.data.values(): 103 | value = value() 104 | if value is not None: 105 | yield value 106 | 107 | def items(self): 108 | """ 109 | Итерируемся по парам из ключей и значений. 110 | """ 111 | for key in self.keys(): 112 | value = self.get(key) 113 | if value is not None: 114 | yield key, value() 115 | 116 | def pop(self, key, *defaults): 117 | """ 118 | Удаляем значение по ключу из словаря, при этом удаленное значение возвращается. 119 | В качестве второго аргумента можно указать дефолтное значение, которое вернется в том случае, если ключа в словаре нет. Если дефолтное значение не задано, в этом случае поднимется исключение. 120 | """ 121 | if len(defaults) > 1: 122 | raise TypeError(f'pop expected at most 2 arguments, got {len(defaults)}') 123 | 124 | default_exist = len(defaults) == 1 125 | 126 | with self.lock: 127 | item = self.get(key) 128 | if item is None: 129 | if default_exist: 130 | return defaults[0] 131 | raise KeyError(key) 132 | del self[key] 133 | return item 134 | -------------------------------------------------------------------------------- /polog/tests/test_field.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from polog import config, log, field, log 6 | from polog.core.utils.exception_escaping import exception_escaping 7 | 8 | 9 | def ip_extractor(log): 10 | request = log.function_input_data.args[0] 11 | ip = request.META.get('REMOTE_ADDR') 12 | return ip 13 | 14 | class Request: 15 | META = {'REMOTE_ADDR': '123.456.789.010'} 16 | 17 | 18 | def test_django_example(handler): 19 | """ 20 | Проверяем, что пример кода из README.md работает. 21 | В данном случае мы проверяем, что извлекается ip-адрес из обработчика запросов Django. 22 | """ 23 | config.add_fields(ip=field(ip_extractor)) 24 | 25 | @log 26 | def django_handler_example(request): 27 | html = 'text' 28 | return html 29 | request = Request() 30 | 31 | django_handler_example(request) 32 | 33 | time.sleep(0.0001) 34 | assert handler.last['ip'] == '123.456.789.010' 35 | 36 | def test_django_example_error(handler): 37 | """ 38 | В README.md есть пример кода с извлечением ip-адреса в обработчике запроса Django. 39 | В данном теста мы проверяем, что он работает. 40 | """ 41 | config.add_fields(ip=field(ip_extractor)) 42 | 43 | @exception_escaping 44 | @log 45 | def django_handler_error(request): 46 | html = 1 / 0 47 | return html 48 | 49 | request = Request() 50 | django_handler_error(request) 51 | 52 | time.sleep(0.0001) 53 | assert handler.last['ip'] == '123.456.789.010' 54 | 55 | def test_not_called_extractor(): 56 | """ 57 | Скармливаем невызываемый объект в виде экстрактора. 58 | """ 59 | with pytest.raises(ValueError): 60 | field('kek') 61 | 62 | def test_not_called_converter(): 63 | """ 64 | Скармливаем невызываемый объект в виде конвертера. 65 | """ 66 | with pytest.raises(ValueError): 67 | field(ip_extractor, converter='kek') 68 | 69 | def test_basic_converter(handler): 70 | """ 71 | Указываем конвертер значений и проверяем, что он работает. 72 | """ 73 | @log 74 | def django_handler_example(request): 75 | html = 'text' 76 | return html 77 | request = Request() 78 | config.add_fields(ip_converted=field(ip_extractor, converter=lambda value: 'converted_' + value)) 79 | 80 | django_handler_example(request) 81 | 82 | time.sleep(0.0001) 83 | assert handler.last['ip_converted'] == 'converted_123.456.789.010' 84 | 85 | def test_not_correct_extractor_signature(): 86 | """ 87 | Пробуем передать в качестве экстрактора функцию, чья сигнатура не соответствует ожидаемой. 88 | """ 89 | def not_extractor(a, b, c): 90 | pass 91 | 92 | with pytest.raises(ValueError): 93 | config.add_fields(data=field(not_extractor)) 94 | 95 | def test_not_correct_converter_signature(): 96 | """ 97 | Пробуем передать в качестве конвертера функцию, чья сигнатура не соответствует ожидаемой. 98 | """ 99 | def extractor(log_item): 100 | pass 101 | 102 | def not_converter(a, b, c): 103 | pass 104 | 105 | with pytest.raises(ValueError): 106 | config.add_fields(data=field(extractor, converter=not_converter)) 107 | 108 | def test_field_through_data_passage_with_decorator(handler): 109 | """ 110 | Проверяем, что данные из экстрактора проходят насквозь без какой-либо конвертации (например без преобразования в строку). 111 | """ 112 | config.set(pool_size=0) 113 | def extractor(log_item): 114 | return 1 115 | config.add_fields(data=field(extractor)) 116 | 117 | @log(message='kek') 118 | def kek(): 119 | pass 120 | 121 | kek() 122 | 123 | assert handler.last['data'] == 1 124 | 125 | def test_field_through_data_passage_with_error_decorator(handler): 126 | """ 127 | Аналог теста test_field_through_data_passage(), но теперь внутри обернутой функции происходит исключение. 128 | """ 129 | config.set(pool_size=0) 130 | def extractor(log_item): 131 | return 1 132 | config.add_fields(data=field(extractor)) 133 | 134 | @exception_escaping 135 | @log(message='kek') 136 | def kek(): 137 | raise ValueError 138 | 139 | kek() 140 | 141 | assert handler.last['data'] == 1 142 | 143 | def test_field_through_data_passage_with_handle_logging(handler): 144 | """ 145 | Аналог теста test_field_through_data_passage(), но вместо декоратора проверяем ручное логирование. 146 | """ 147 | config.set(pool_size=0) 148 | def extractor(log_item): 149 | return 1 150 | config.add_fields(data=field(extractor)) 151 | 152 | log('kek') 153 | 154 | assert handler.last['data'] == 1 155 | -------------------------------------------------------------------------------- /polog/field.py: -------------------------------------------------------------------------------- 1 | from polog.core.utils.signature_matcher import SignatureMatcher 2 | 3 | 4 | class field: 5 | """ 6 | Класс, представляющий кастомное поле лога. 7 | Пользователь Polog может подключать неограниченное количество дополнительных полей. 8 | 9 | Технически работа поля состоит из 2-х вещей: 10 | 1. Извлечь какую-то информацию из исходных данных. 11 | 2. Преобразовать ее в строку. 12 | 13 | Любые обработчики Polog должны ожидать возможность наличия дополнительных полей. 14 | На момент попадания в обработчик содержимое поля уже сконвертировано в нужный формат. 15 | """ 16 | def __init__(self, extractor, converter=None): 17 | """ 18 | extractor - функция, принимающая лог в качестве аргумента, и возвращающая некий объект, представляющий контент поля. 19 | converter - функция, преобразующая извлеченный контент в строковый формат. Принимает на вход объект, возвращенный функцией extractor и возвращает строку. 20 | 21 | Конвертер отделен от экстрактора, поскольку в некоторых случаях нам не нужно тратить ресурсы на преобразование форматов. Скажем, если приоритет данного лога ниже установленного в данный момент уровня логирования. 22 | Обязательным для инициализации поля является только экстрактор. Если экстрактор по умолчанию возвращает строки, передавать конвертер не требуется. Также это не нужно делать, если для сериализации достаточно скормить промежуточный объект функции str() - это будет сделано автоматически. 23 | Конвертер требуется определять только в ситуациях, когда вам требуется какой-то специфический механизм сериализации поля. 24 | """ 25 | self.extractor = self.get_extractor(extractor) 26 | self.converter = self.get_converter(converter) 27 | 28 | def get_data(self, log): 29 | """ 30 | Берем сырые данные, извлекаем из них некое значение с помощью экстрактора и скармливаем его конвертеру, результат возвращаем. 31 | """ 32 | item = self.extract(log) 33 | converted_item = self.convert(item) 34 | return converted_item 35 | 36 | def extract(self, log): 37 | """ 38 | Принимаем на вход лог и возвращаем некий объект. 39 | Обычно - строку; если это не строка, то нужно зарегистрировать еще функцию converter для преобразования в строку. См. комментарий к методу .__init__(). 40 | 41 | По факту данный метод является прокси и вызывает другой метод, ранее зарегистрированный в качестве экстрактора. 42 | """ 43 | return self.extractor(log) 44 | 45 | def convert(self, value): 46 | """ 47 | Принимаем на вход некий объект, возвращенный экстрактором, и возвращаем строку, в которую он был сериализован. 48 | 49 | Прокси-метод. По факту вызывает либо переданный пользователем конвертер, либо стандартный, если пользователь ничего не передавал. 50 | """ 51 | return self.converter(value) 52 | 53 | def get_extractor(self, extractor): 54 | """ 55 | По сути здесь не происходит никакой работы, кроме проверки, что переданный пользователем в качестве экстрактора объект является именно функцией. 56 | Исходя из сигнатуры метода .__init__() у пользователя нет возможности не передать никакой экстрактор, поэтому здесь не происходит выбора между каким-нибудь стандартным экстрактором и переданным пользователем. 57 | """ 58 | if not callable(extractor): 59 | raise ValueError('Extractor must be called.') 60 | if not SignatureMatcher.is_handler(extractor): 61 | raise ValueError('The signature of the function passed as a extractor does not match the expected one. It should be the same as the standard handler.') 62 | return extractor 63 | 64 | def get_converter(self, maybe_converter): 65 | """ 66 | Здесь принимается решение, каким конвертером (функцией-сериализатором) мы пользуемся. 67 | 68 | Если пользователь передал свой конвертер, используем его. Иначе - используем стандартный, который просто скармливает любые объекты функции str(). 69 | """ 70 | if maybe_converter is None: 71 | return self.standart_converter 72 | if callable(maybe_converter): 73 | if not SignatureMatcher.is_handler(maybe_converter): 74 | raise ValueError('The signature of the function passed as a converter does not match the expected one. It should be the same as the standard handler.') 75 | return maybe_converter 76 | raise ValueError('Converter must be called.') 77 | 78 | def standart_converter(self, value): 79 | """ 80 | Тупо возвращаем то, что сюда передано. 81 | 82 | Данный метод используется, если пользователь не передал в конструктор класса кастомную функцию-сериализатор. 83 | """ 84 | return value 85 | -------------------------------------------------------------------------------- /polog/core/utils/signature_matcher.py: -------------------------------------------------------------------------------- 1 | from inspect import Signature 2 | 3 | 4 | class SignatureMatcher: 5 | """ 6 | Объект данного класса содержит в себе "слепок" ожидаемой сигнатуры вызываемого объекта. 7 | Его затем можно "прикладывать" к реальным вызываемым объектам (см. метод .match()), чтобы понять, соответствуют ли их сигнатуры ожидаемой. 8 | """ 9 | def __init__(self, *args): 10 | """ 11 | Инициализация объекта - это создание "слепка" ожидаемой сигнатуры функций. 12 | 13 | В качестве аргументов принимаются 4 типа объектов (они все являются строками): 14 | 1. '.' - соответствует обыкновенному позиционному аргументу без дефолтного значения. 15 | 2. 'some_argument_name' - соответствует аргументу с дефолтным значением. Содержание строки - имя аргумента. 16 | 3. '*' - соответствует запаковке нескольких позиционных аргументов без дефолтных значений (*args). 17 | 4. '**' - соответствует запаковке нескольких именованных аргументов с дефолтными значениями (**kwargs). 18 | 19 | К примеру, функции, озаглавленной вот так: 20 | def func(a, b, c=5, *d, **e): 21 | ... 22 | 23 | ... будет соответствовать такой "слепок": 24 | SignatureMatcher('.', '.', 'c', '*', '**') 25 | """ 26 | self.is_args = '*' in args 27 | self.is_kwargs = '**' in args 28 | self.number_of_position_args = len([x for x in args if x == '.']) 29 | self.number_of_named_args = len([x for x in args if x.isidentifier()]) 30 | self.names_of_named_args = list(set([x for x in args if x.isidentifier()])) 31 | 32 | def match(self, function): 33 | """ 34 | Проверяем, что сигнатура функции, переданной в качестве аргумента, соответствует "слепку", полученному при инициализации объекта SignatureMatcher. 35 | """ 36 | if not callable(function): 37 | raise ValueError('It is impossible to determine the signature of an object that is not being callable.') 38 | signature = Signature.from_callable(function) 39 | parameters = list(signature.parameters.values()) 40 | result = True 41 | result *= self.prove_is_args(parameters) 42 | result *= self.prove_is_kwargs(parameters) 43 | result *= self.prove_number_of_position_args(parameters) 44 | result *= self.prove_number_of_named_args(parameters) 45 | result *= self.prove_names_of_named_args(parameters) 46 | return bool(result) 47 | 48 | def prove_is_args(self, parameters): 49 | """ 50 | Проверка наличия распаковки позиционных аргументов. 51 | """ 52 | return self.is_args == bool(len([parameter for parameter in parameters if parameter.kind == parameter.VAR_POSITIONAL])) 53 | 54 | def prove_is_kwargs(self, parameters): 55 | """ 56 | Проверка наличия распаковки именованных аргументов. 57 | """ 58 | return self.is_kwargs == bool(len([parameter for parameter in parameters if parameter.kind == parameter.VAR_KEYWORD])) 59 | 60 | def prove_number_of_position_args(self, parameters): 61 | """ 62 | Проверка, что количество позиционных аргументов совпадает с ожидаемым. 63 | """ 64 | return self.number_of_position_args == len([parameter for parameter in parameters if (parameter.kind == parameter.POSITIONAL_ONLY or parameter.kind == parameter.POSITIONAL_OR_KEYWORD) and parameter.default == parameter.empty]) 65 | 66 | def prove_number_of_named_args(self, parameters): 67 | """ 68 | Проверка количества именованных аргументов. 69 | """ 70 | return self.number_of_named_args == len([parameter for parameter in parameters if (parameter.kind == parameter.KEYWORD_ONLY or parameter.kind == parameter.POSITIONAL_OR_KEYWORD) and parameter.default != parameter.empty]) 71 | 72 | def prove_names_of_named_args(self, parameters): 73 | """ 74 | Проверка, что имена именованных аргументов совпадают с ожидаемыми. 75 | """ 76 | names_of_parameters = [parameter.name for parameter in parameters if (parameter.kind == parameter.KEYWORD_ONLY or parameter.kind == parameter.POSITIONAL_OR_KEYWORD) and parameter.default != parameter.empty] 77 | result = True 78 | for name in self.names_of_named_args: 79 | result *= (name in names_of_parameters) 80 | return result 81 | 82 | @classmethod 83 | def is_handler(cls, function, raise_exception=True): 84 | """ 85 | Проверка сигнатуры функции на предмет того, может она быть обработчиком Polog или нет. 86 | 87 | Обработчик - это вызываемый объект со следующей сигнатурой (названия аргументов не обязаны совпадать): 88 | 89 | handler(function_input, **fields) 90 | """ 91 | matcher = cls('.') 92 | try: 93 | return matcher.match(function) 94 | except ValueError as e: 95 | if not raise_exception: 96 | return False 97 | raise 98 | --------------------------------------------------------------------------------