В своей работе столкнулся с такой проблемой: мне нужно было разделить логирование на несколько потоков, сохранив динамичность в количестве потоков. Сейчас, когда уже разобрался с этим, все не так уж и сложно.
В общем и целом, штудировал я и интернет, и исследовал на практике этот вопрос. Странным оказалось то, что готового решения я не нашел, неужели никто не испытывает проблем с этим? Никто в python не пишет логи?
Начну по порядку и сразу к делу, без дифирамбов.
logging
В python есть замечательный инструмент для работы с логами, ниже листинг простейшей программы:
import logging import sys logger = logging.getLogger(__name__) if not logger.hasHandlers(): logger.setLevel(logging.DEBUG) stream = logging.StreamHandler(stream=sys.stdout) file = logging.FileHandler('logs.log') stream.setLevel(logging.DEBUG) file.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') stream.setFormatter(formatter) file.setFormatter(formatter) logger.addHandler(stream) logger.addHandler(file) logger.set_handler = True logger.info('This is a log message')
В этом коде я собрал всё, что мне удалось нарыть в интернете по моей проблеме:
-
Как одновременно стримить логи в консоль и в файл.
-
Как ограничить количество хэндлеров для одного экземпляра logger.
-
Как задать формат для логов.
threading
В python есть возможность исполнять программу в многопоточном режиме (ну, или в псевдомногопоточном, кому как нравится), ниже листинг простейшего применения этого модуля:
import threading import time def main(): time.sleep(5) thread = threading.Thread(target=main) thread.start() thread.join()
Выполнение функции main() в многопоточном режиме в контексте логирования будет выглядеть так:
import logging import sys import threading import time def main(n: int): logger = logging.getLogger(threading.current_thread().name) logger.setLevel(logging.DEBUG) stream = logging.StreamHandler(stream=sys.stdout) file = logging.FileHandler(f'logs_{threading.current_thread().name}.log') stream.setLevel(logging.DEBUG) file.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') stream.setFormatter(formatter) file.setFormatter(formatter) logger.addHandler(stream) logger.addHandler(file) logger.info(f'This is a log message from {n}') time.sleep(1) threads = [] for i in range(5): thread = threading.Thread(target=main, args=(i,)) with open(f'logs_{thread.name}.log', 'w') as f: f.write('') thread.start() threads.append(thread) for thread in threads: thread.join() # wait for all threads to finish
Казалось бы, что еще нужно? Все работает прекрасно и на потоки делится. Но не все так гладко становится, когда нужно логировать работу экземпляров класса.
threading в контексте logging и ООП
Главная проблема заключалась в том, что экземпляры класса создаются до применения над атрибутами экземпляра или класса каких либо действий, то есть методы класса мы вызываем уже после инициализации.
Есть два пути решения проблемы. Первый — передавать в метод ссылку на экземпляр logging.Logger, а второй — переместить код с настройкой logger в родительский класс.
Листинг классов, которые будут использоваться для логирования и многопоточности:
import logging import sys import threading import time class YoungPerson: def __init__(self): self.age = 35 class User(YoungPerson): def __init__(self): super().__init__() self.name = None def set_name(self, new_name, logger): logger = logging.getLogger(threading.current_thread().name) logger.setLevel(logging.DEBUG) stream = logging.StreamHandler(stream=sys.stdout) file = logging.FileHandler(f'logs_{threading.current_thread().name}.log') stream.setLevel(logging.DEBUG) file.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') stream.setFormatter(formatter) file.setFormatter(formatter) logger.addHandler(stream) logger.addHandler(file) logger.info(f"{self.name} changed name to {new_name}") self.name = new_name time.sleep(1)
Давайте попробуем передать в метод set_name() ссылку на экземпляр логгера и получим следующий код:
import logging import threading import youngPersons young_persons = [youngPersons.User() for _ in range(10)] threads = [] for user in young_persons: thread = threading.Thread(target=user.set_name, args=('John',)) logger = logging.getLogger(thread.name) thread.start() threads.append(thread) for thread in threads: thread.join() # wait for all threads to finish
Но здесь возникает проблема: мы не можем передать название потока еще до того, как поток был проинициализирован (см. строки кода 10-11).
Значит первый вариант решения проблемы не подходит, остается второй. А так будет выглядеть код с классами, если перенести настройку логов в родительский класс:
import logging import sys import time class YoungPerson: def __init__(self): self.age = 35 self.logger = logging.getLogger(__name__) def set_logger_name(self, logger_name): self.logger = logger_name if not self.logger.hasHandlers(): self.logger.setLevel(logging.DEBUG) stream = logging.StreamHandler(stream=sys.stdout) file = logging.FileHandler(f'logs_{self.logger.name}.log') stream.setLevel(logging.DEBUG) file.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') stream.setFormatter(formatter) file.setFormatter(formatter) self.logger.addHandler(stream) self.logger.addHandler(file) self.logger.set_handler = True class User(YoungPerson): def __init__(self): super().__init__() self.name = None def set_name(self, new_name): time.sleep(1) self.logger.info(f"{self.name} changed name to {new_name}") self.name = new_name
Теперь можно задавать логирование в нужном потоке в нужный файл сразу после инициализации самого потока, смотрите, как это выглядит в коде основного файла:
import logging import threading import youngPersons young_persons = [youngPersons.User() for _ in range(10)] threads = [] for user in young_persons: thread = threading.Thread(target=user.set_name, args=('John',)) logger = logging.getLogger(thread.name) user.set_logger_name(logger_name=logger) with open(f'logs_{thread.name}.log', 'w') as f: f.write('') thread.start() threads.append(thread) for thread in threads: thread.join() # wait for all threads to finish
Теперь для каждого потока создается свой логгер, а хендлеры добавляются уже позже, в родительском классе.
Плюсы такого решения в том, что есть возможность задавать произвольное число потоков. Для этого нужно дополнительно создать класс, который будет работать с группами экземпляров класса User, и алгоритм, который будет делить объекты по группам.
Но это уже другая история.. Если хотите, чтобы я ее раскрыл, то поддержите меня, как автора на Хабр. Очень буду рад вашим комментариям и с большой охотой на них отвечу.
Спасибо за внимание!!!
ссылка на оригинал статьи https://habr.com/ru/articles/849030/
Добавить комментарий