最方便最好看最好用的python日志。
这个日志没有依赖自己的其他包,复制即可运行,也可以从pypi网站上下载或者pip来安装这个日志。
1、日志内置了7种模板,其中模版4和模板5,可以实现点击日志跳转到指定文件指定行数的功能,史无前例的实现这种方式。
2、使用了ColorHandler作为默认的控制台显示日志,而不是使用官方的StramHandler,实现五颜六色的日志,在茫茫大海的日志中一眼就能看出哪些是调试日志,哪些是错误日志哪些是警告日志和严重日志。绿色代表debug,天蓝色代表info,黄色代表warning,粉红色代表错误,血红色代表严重错误,颜色符合正常逻辑,具体的颜色显示业余自己设置的pycharm主题和配色有关,建议使用红色主题,具体的颜色显示与pycahrm版本也有一些关系。
3、实现了进程安全的日志切片,引用的是第三方的Handler
4、添加了对国内邮箱 qq 163等支持的mailhandler,并且支持邮件发送控频。
5、添加了MongoHanler,可以自动拆分日志字段插入mongo
6.1、以上这些handler都不需要去手动调用添加各种handler,都是通过传参的方式,如,设置了文件名那么自动生成文件日志,添加了mongo的url登录链接,则添加mongohandler,以此类推。
6.2、要搞清楚为啥logger和各种handler,要弄清楚日志命名空间,各种handler的关系和logger的关系必须弄清楚23种设计模式的观察者模式,搞清楚这个模式了,就可以自己扩展各种各样的handler来满足自己的需求。
为什么要使用日志呢,
之前同事全部print,十分蛋疼,项目有几十万行,一运行起来,各种地方嵌套import各种模块,到处去print,十分操蛋,完全不知道哪里冒出来的日志,不好禁止,扩展不了各种handler,总之比日志差太多了。。
拿print当做日志用是属于py很初级的表现。
- 1 # coding=utf8
- 2 """
- 3 日志管理,支持日志打印到控制台或写入切片文件或mongodb或email
- 4 使用方式为 logger = LogManager('logger_name').get_and_add_handlers(log_level_int=1, is_add_stream_handler=True, log_path=None, log_filename=None, log_file_size=10,mongo_url=None,formatter_template=2)
- 5 或者 logger = LogManager('logger_name').get_without_handlers(),此种没有handlers不立即记录日志,之后可以在单独统一的总闸处对所有日志根据loggerame进行get_and_add_handlers添加相关的各种handlers
- 6 创建一个邮件日志的用法为 logger = LogManager.bulid_a_logger_with_mail_handler('mail_logger_name', mail_time_interval=10, toaddrs=('909686xxx@qq.com', 'yangxx4508@dingtalk.com',subject='你的主题)),使用了独立的创建方式
- 7 concurrent_log_handler的ConcurrentRotatingFileHandler解决了logging模块自带的RotatingFileHandler多进程切片错误,此ConcurrentRotatingFileHandler在win和linux多进程场景下log文件切片都ok.
- 8 1、根据日志级别,使用coolorhanlder代替straemhandler打印5种颜色的日志,一目了然哪里是严重的日志。
- 9 2、带有多种handler,邮件 mongo stream file的。
- 10 3、支持pycharm点击日志跳转到对应代码文件的对应行。
- 11 4、对相同命名空间的logger可以无限添加同种类型的handlers,不会重复使用同种handler记录日志。不需要用户自己去判断。
- 12
- 13 """
- 14 import sys
- 15 import os
- 16 from threading import Lock
- 17 import unittest
- 18 import time
- 19 from collections import OrderedDict
- 20 import pymongo
- 21 import logging
- 22 from logging import handlers
- 23 from concurrent_log_handler import ConcurrentRotatingFileHandler # 需要安装。ConcurrentLogHandler==0.9.1
- 24
- 25 os_name = os.name
- 26 formatter_dict = {
- 27 1: logging.Formatter(
- 28 '日志时间【%(asctime)s】 - 日志名称【%(name)s】 - 文件【%(filename)s】 - 第【%(lineno)d】行 - 日志等级【%(levelname)s】 - 日志信息【%(message)s】',
- 29 "%Y-%m-%d %H:%M:%S"),
- 30 2: logging.Formatter(
- 31 '%(asctime)s - %(name)s - %(filename)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s',
- 32 "%Y-%m-%d %H:%M:%S"),
- 33 3: logging.Formatter(
- 34 '%(asctime)s - %(name)s - 【 File "%(pathname)s", line %(lineno)d, in %(funcName)s 】 - %(levelname)s - %(message)s',
- 35 "%Y-%m-%d %H:%M:%S"), # 一个模仿traceback异常的可跳转到打印日志地方的模板
- 36 4: logging.Formatter(
- 37 '%(asctime)s - %(name)s - "%(filename)s" - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s - File "%(pathname)s", line %(lineno)d ',
- 38 "%Y-%m-%d %H:%M:%S"), # 这个也支持日志跳转
- 39 5: logging.Formatter(
- 40 '%(asctime)s - %(name)s - "%(pathname)s:%(lineno)d" - %(funcName)s - %(levelname)s - %(message)s',
- 41 "%Y-%m-%d %H:%M:%S"), # 我认为的最好的模板,推荐
- 42 6: logging.Formatter('%(name)s - %(asctime)-15s - %(filename)s - %(lineno)d - %(levelname)s: %(message)s',
- 43 "%Y-%m-%d %H:%M:%S"),
- 44 7: logging.Formatter('%(levelname)s - %(filename)s - %(lineno)d - %(message)s'), # 一个只显示简短文件名和所处行数的日志模板
- 45 }
- 46
- 47
- 48 # noinspection PyMissingOrEmptyDocstring
- 49 class LogLevelException(Exception):
- 50 def __init__(self, log_level):
- 51 err = '设置的日志级别是 {0}, 设置错误,请设置为1 2 3 4 5 范围的数字'.format(log_level)
- 52 Exception.__init__(self, err)
- 53
- 54
- 55 # noinspection PyMissingOrEmptyDocstring
- 56 class MongoHandler(logging.Handler):
- 57 """
- 58 一个mongodb的log handler,支持日志按loggername创建不同的集合写入mongodb中
- 59 """
- 60
- 61 # msg_pattern = re.compile('(\d+-\d+-\d+ \d+:\d+:\d+) - (\S*?) - (\S*?) - (\d+) - (\S*?) - ([\s\S]*)')
- 62
- 63 def __init__(self, mongo_url, mongo_database='logs'):
- 64 """
- 65 :param mongo_url: mongo连接
- 66 :param mongo_database: 保存日志的数据库,默认使用logs数据库
- 67 """
- 68 logging.Handler.__init__(self)
- 69 mongo_client = pymongo.MongoClient(mongo_url)
- 70 self.mongo_db = mongo_client.get_database(mongo_database)
- 71
- 72 def emit(self, record):
- 73 # noinspection PyBroadException,PyPep8
- 74 try:
- 75 """以下使用解析日志模板的方式提取出字段"""
- 76 # msg = self.format(record)
- 77 # logging.LogRecord
- 78 # msg_match = self.msg_pattern.search(msg)
- 79 # log_info_dict = {'time': msg_match.group(1),
- 80 # 'name': msg_match.group(2),
- 81 # 'file_name': msg_match.group(3),
- 82 # 'line_no': msg_match.group(4),
- 83 # 'log_level': msg_match.group(5),
- 84 # 'detail_msg': msg_match.group(6),
- 85 # }
- 86 level_str = None
- 87 if record.levelno == 10:
- 88 level_str = 'DEBUG'
- 89 elif record.levelno == 20:
- 90 level_str = 'INFO'
- 91 elif record.levelno == 30:
- 92 level_str = 'WARNING'
- 93 elif record.levelno == 40:
- 94 level_str = 'ERROR'
- 95 elif record.levelno == 50:
- 96 level_str = 'CRITICAL'
- 97 log_info_dict = OrderedDict()
- 98 log_info_dict['time'] = time.strftime('%Y-%m-%d %H:%M:%S')
- 99 log_info_dict['name'] = record.name
- 100 log_info_dict['file_path'] = record.pathname
- 101 log_info_dict['file_name'] = record.filename
- 102 log_info_dict['func_name'] = record.funcName
- 103 log_info_dict['line_no'] = record.lineno
- 104 log_info_dict['log_level'] = level_str
- 105 log_info_dict['detail_msg'] = record.msg
- 106 col = self.mongo_db.get_collection(record.name)
- 107 col.insert_one(log_info_dict)
- 108 except (KeyboardInterrupt, SystemExit):
- 109 raise
- 110 except Exception:
- 111 self.handleError(record)
- 112
- 113
- 114 class ColorHandler0(logging.Handler):
- 115 """彩色日志handler,根据不同级别的日志显示不同颜色"""
- 116 bule = 96 if os_name == 'nt' else 36
- 117 yellow = 93 if os_name == 'nt' else 33
- 118
- 119 def __init__(self):
- 120 logging.Handler.__init__(self)
- 121 self.formatter_new = logging.Formatter(
- 122 '%(asctime)s - %(name)s - "%(filename)s" - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s',
- 123 "%Y-%m-%d %H:%M:%S")
- 124 # 对控制台日志单独优化显示和跳转,单独对字符串某一部分使用特殊颜色,主要用于第四种模板,以免filehandler和mongohandler中带有\033
- 125
- 126 @classmethod
- 127 def _my_align(cls, string, length):
- 128 if len(string) > length * 2:
- 129 return string
- 130 custom_length = 0
- 131 for w in string:
- 132 custom_length += 1 if cls._is_ascii_word(w) else 2
- 133 if custom_length < length:
- 134 place_length = length - custom_length
- 135 string += ' ' * place_length
- 136 return string
- 137
- 138 @staticmethod
- 139 def _is_ascii_word(w):
- 140 if ord(w) < 128:
- 141 return True
- 142
- 143 def emit(self, record):
- 144 """
- 145 30 40 黑色
- 146 31 41 红色
- 147 32 42 绿色
- 148 33 43 黃色
- 149 34 44 蓝色
- 150 35 45 紫红色
- 151 36 46 青蓝色
- 152 37 47 白色
- 153 :type record:logging.LogRecord
- 154 :return:
- 155 """
- 156
- 157 if self.formatter is formatter_dict[4] or self.formatter is self.formatter_new:
- 158 self.formatter = self.formatter_new
- 159 if os.name == 'nt':
- 160 self.__emit_for_fomatter4_pycahrm(record) # 使用模板4并使用pycharm时候
- 161 else:
- 162 self.__emit_for_fomatter4_linux(record) # 使用模板4并使用linux时候
- 163 else:
- 164 self.__emit(record) # 其他模板
- 165
- 166 def __emit_for_fomatter4_linux(self, record):
- 167 """
- 168 当使用模板4针对linxu上的终端打印优化显示
- 169 :param record:
- 170 :return:
- 171 """
- 172 # noinspection PyBroadException,PyPep8
- 173 try:
- 174 msg = self.format(record)
- 175 file_formatter = ' ' * 10 + '\033[7mFile "%s", line %d\033[0m' % (record.pathname, record.lineno)
- 176 if record.levelno == 10:
- 177 print('\033[0;32m%s' % self._my_align(msg, 150) + file_formatter)
- 178 elif record.levelno == 20:
- 179 print('\033[0;34m%s' % self._my_align(msg, 150) + file_formatter)
- 180 elif record.levelno == 30:
- 181 print('\033[0;33m%s' % self._my_align(msg, 150) + file_formatter)
- 182 elif record.levelno == 40:
- 183 print('\033[0;35m%s' % self._my_align(msg, 150) + file_formatter)
- 184 elif record.levelno == 50:
- 185 print('\033[0;31m%s' % self._my_align(msg, 150) + file_formatter)
- 186 except (KeyboardInterrupt, SystemExit):
- 187 raise
- 188 except Exception:
- 189 self.handleError(record)
- 190
- 191 def __emit_for_fomatter4_pycahrm(self, record):
- 192 """
- 193 当使用模板4针对pycahrm的打印优化显示
- 194 :param record:
- 195 :return:
- 196 """
- 197 # \033[0;93;107mFile "%(pathname)s", line %(lineno)d, in %(funcName)s\033[0m
- 198 # noinspection PyBroadException
- 199 try:
- 200 msg = self.format(record)
- 201 # for_linux_formatter = ' ' * 10 + '\033[7m;File "%s", line %d\033[0m' % (record.pathname, record.lineno)
- 202 file_formatter = ' ' * 10 + '\033[0;93;107mFile "%s", line %d\033[0m' % (record.pathname, record.lineno)
- 203 if record.levelno == 10:
- 204 print('\033[0;32m%s\033[0m' % self._my_align(msg, 200) + file_formatter) # 绿色
- 205 elif record.levelno == 20:
- 206 print('\033[0;36m%s\033[0m' % self._my_align(msg, 200) + file_formatter) # 青蓝色
- 207 elif record.levelno == 30:
- 208 print('\033[0;92m%s\033[0m' % self._my_align(msg, 200) + file_formatter) # 蓝色
- 209 elif record.levelno == 40:
- 210 print('\033[0;35m%s\033[0m' % self._my_align(msg, 200) + file_formatter) # 紫红色
- 211 elif record.levelno == 50:
- 212 print('\033[0;31m%s\033[0m' % self._my_align(msg, 200) + file_formatter) # 血红色
- 213 except (KeyboardInterrupt, SystemExit):
- 214 raise
- 215 except: # NOQA
- 216 self.handleError(record)
- 217
- 218 def __emit(self, record):
- 219 # noinspection PyBroadException
- 220 try:
- 221 msg = self.format(record)
- 222 if record.levelno == 10:
- 223 print('\033[0;32m%s\033[0m' % msg) # 绿色
- 224 elif record.levelno == 20:
- 225 print('\033[0;%sm%s\033[0m' % (self.bule, msg)) # 青蓝色 36 96
- 226 elif record.levelno == 30:
- 227 print('\033[0;%sm%s\033[0m' % (self.yellow, msg))
- 228 elif record.levelno == 40:
- 229 print('\033[0;35m%s\033[0m' % msg) # 紫红色
- 230 elif record.levelno == 50:
- 231 print('\033[0;31m%s\033[0m' % msg) # 血红色
- 232 except (KeyboardInterrupt, SystemExit):
- 233 raise
- 234 except: # NOQA
- 235 self.handleError(record)
- 236
- 237
- 238 class ColorHandler(logging.Handler):
- 239 """
- 240 A handler class which writes logging records, appropriately formatted,
- 241 to a stream. Note that this class does not close the stream, as
- 242 sys.stdout or sys.stderr may be used.
- 243 """
- 244
- 245 terminator = '\n'
- 246 bule = 96 if os_name == 'nt' else 36
- 247 yellow = 93 if os_name == 'nt' else 33
- 248
- 249 def __init__(self, stream=None):
- 250 """
- 251 Initialize the handler.
- 252
- 253 If stream is not specified, sys.stderr is used.
- 254 """
- 255 logging.Handler.__init__(self)
- 256 if stream is None:
- 257 stream = sys.stdout # stderr无彩。
- 258 self.stream = stream
- 259
- 260 def flush(self):
- 261 """
- 262 Flushes the stream.
- 263 """
- 264 self.acquire()
- 265 try:
- 266 if self.stream and hasattr(self.stream, "flush"):
- 267 self.stream.flush()
- 268 finally:
- 269 self.release()
- 270
- 271 def emit(self, record):
- 272 """
- 273 Emit a record.
- 274
- 275 If a formatter is specified, it is used to format the record.
- 276 The record is then written to the stream with a trailing newline. If
- 277 exception information is present, it is formatted using
- 278 traceback.print_exception and appended to the stream. If the stream
- 279 has an 'encoding' attribute, it is used to determine how to do the
- 280 output to the stream.
- 281 """
- 282 # noinspection PyBroadException
- 283 try:
- 284 msg = self.format(record)
- 285 stream = self.stream
- 286 if record.levelno == 10:
- 287 msg_color = ('\033[0;32m%s\033[0m' % msg) # 绿色
- 288 elif record.levelno == 20:
- 289 msg_color = ('\033[0;%sm%s\033[0m' % (self.bule, msg)) # 青蓝色 36 96
- 290 elif record.levelno == 30:
- 291 msg_color = ('\033[0;%sm%s\033[0m' % (self.yellow, msg))
- 292 elif record.levelno == 40:
- 293 msg_color = ('\033[0;35m%s\033[0m' % msg) # 紫红色
- 294 elif record.levelno == 50:
- 295 msg_color = ('\033[0;31m%s\033[0m' % msg) # 血红色
- 296 else:
- 297 msg_color = msg
- 298 # print(msg_color,'***************')
- 299 stream.write(msg_color)
- 300 stream.write(self.terminator)
- 301 self.flush()
- 302 except Exception:
- 303 self.handleError(record)
- 304
- 305 def __repr__(self):
- 306 level = logging.getLevelName(self.level)
- 307 name = getattr(self.stream, 'name', '')
- 308 if name:
- 309 name += ' '
- 310 return '<%s %s(%s)>' % (self.__class__.__name__, name, level)
- 311
- 312
- 313 class CompatibleSMTPSSLHandler(handlers.SMTPHandler):
- 314 """
- 315 官方的SMTPHandler不支持SMTP_SSL的邮箱,这个可以两个都支持,并且支持邮件发送频率限制
- 316 """
- 317
- 318 def __init__(self, mailhost, fromaddr, toaddrs: tuple, subject,
- 319 credentials=None, secure=None, timeout=5.0, is_use_ssl=True, mail_time_interval=0):
- 320 """
- 321
- 322 :param mailhost:
- 323 :param fromaddr:
- 324 :param toaddrs:
- 325 :param subject:
- 326 :param credentials:
- 327 :param secure:
- 328 :param timeout:
- 329 :param is_use_ssl:
- 330 :param mail_time_interval: 发邮件的时间间隔,可以控制日志邮件的发送频率,为0不进行频率限制控制,如果为60,代表1分钟内最多发送一次邮件
- 331 """
- 332 # noinspection PyCompatibility
- 333 super().__init__(mailhost, fromaddr, toaddrs, subject,
- 334 credentials, secure, timeout)
- 335 self._is_use_ssl = is_use_ssl
- 336 self._current_time = 0
- 337 self._time_interval = mail_time_interval
- 338 self._msg_map = dict() # 是一个内容为键时间为值得映射
- 339 self._lock = Lock()
- 340
- 341 def emit0(self, record: logging.LogRecord):
- 342 """
- 343 不用这个判断内容
- 344 """
- 345 from threading import Thread
- 346 if sys.getsizeof(self._msg_map) > 10 * 1000 * 1000:
- 347 self._msg_map.clear()
- 348 if record.msg not in self._msg_map or time.time() - self._msg_map[record.msg] > self._time_interval:
- 349 self._msg_map[record.msg] = time.time()
- 350 # print('发送邮件成功')
- 351 Thread(target=self.__emit, args=(record,)).start()
- 352 else:
- 353 print(f'[log_manager.py] 邮件发送太频繁,此次不发送这个邮件内容: {record.msg} ')
- 354
- 355 def emit(self, record: logging.LogRecord):
- 356 """
- 357 Emit a record.
- 358
- 359 Format the record and send it to the specified addressees.
- 360 """
- 361 from threading import Thread
- 362 with self._lock:
- 363 if time.time() - self._current_time > self._time_interval:
- 364 self._current_time = time.time()
- 365 Thread(target=self.__emit, args=(record,)).start()
- 366 else:
- 367 print(f'[log_manager.py] 邮件发送太频繁,此次不发送这个邮件内容: {record.msg} ')
- 368
- 369 def __emit(self, record):
- 370 # noinspection PyBroadException
- 371 try:
- 372 import smtplib
- 373 from email.message import EmailMessage
- 374 import email.utils
- 375 t_start = time.time()
- 376 port = self.mailport
- 377 if not port:
- 378 port = smtplib.SMTP_PORT
- 379 smtp = smtplib.SMTP_SSL(self.mailhost, port, timeout=self.timeout) if self._is_use_ssl else smtplib.SMTP(
- 380 self.mailhost, port, timeout=self.timeout)
- 381 msg = EmailMessage()
- 382 msg['From'] = self.fromaddr
- 383 msg['To'] = ','.join(self.toaddrs)
- 384 msg['Subject'] = self.getSubject(record)
- 385 msg['Date'] = email.utils.localtime()
- 386 msg.set_content(self.format(record))
- 387 if self.username:
- 388 if self.secure is not None:
- 389 smtp.ehlo()
- 390 smtp.starttls(*self.secure)
- 391 smtp.ehlo()
- 392 smtp.login(self.username, self.password)
- 393 smtp.send_message(msg)
- 394 smtp.quit()
- 395 # noinspection PyPep8
- 396 print(
- 397 f'[log_manager.py] {time.strftime("%H:%M:%S",time.localtime())} 发送邮件给 {self.toaddrs} 成功,'
- 398 f'用时{round(time.time() - t_start,2)} ,发送的内容是--> {record.msg} \033[0;35m!!!请去邮箱检查,可能在垃圾邮件中\033[0m')
- 399 except Exception as e:
- 400 # self.handleError(record)
- 401 print(
- 402 f'[log_manager.py] {time.strftime("%H:%M:%S",time.localtime())} \033[0;31m !!!!!! 邮件发送失败,原因是: {e} \033[0m')
- 403
- 404
- 405 # noinspection PyTypeChecker
- 406 def get_logs_dir_by_folder_name(folder_name='/app/'):
- 407 """获取app文件夹的路径,如得到这个路径
- 408 D:/coding/hotel_fares/app
- 409 如果没有app文件夹,就在当前文件夹新建
- 410 """
- 411 three_parts_str_tuple = (os.path.dirname(__file__).replace('\\', '/').partition(folder_name))
- 412 # print(three_parts_str_tuple)
- 413 if three_parts_str_tuple[1]:
- 414 return three_parts_str_tuple[0] + three_parts_str_tuple[1] + 'logs/' # noqa
- 415 else:
- 416 return three_parts_str_tuple[0] + '/logs/' # NOQA
- 417
- 418
- 419 def get_logs_dir_by_disk_root():
- 420 """
- 421 返回磁盘根路径下的pythonlogs文件夹,当使用文件日志时候自动创建这个文件夹。
- 422 :return:
- 423 """
- 424 from pathlib import Path
- 425 return str(Path(Path(__file__).absolute().root) / Path('pythonlogs'))
- 426
- 427
- 428 # noinspection PyMissingOrEmptyDocstring,PyPep8
- 429 class LogManager(object):
- 430 """
- 431 一个日志管理类,用于创建logger和添加handler,支持将日志打印到控制台打印和写入日志文件和mongodb和邮件。
- 432 """
- 433 logger_name_list = []
- 434 logger_list = []
- 435
- 436 def __init__(self, logger_name=None):
- 437 """
- 438 :param logger_name: 日志名称,当为None时候创建root命名空间的日志,一般不要传None,除非你确定需要这么做
- 439 """
- 440 self._logger_name = logger_name
- 441 self.logger = logging.getLogger(logger_name)
- 442 self._logger_level = None
- 443 self._is_add_stream_handler = None
- 444 self._do_not_use_color_handler = None
- 445 self._log_path = None
- 446 self._log_filename = None
- 447 self._log_file_size = None
- 448 self._mongo_url = None
- 449 self._formatter = None
- 450
- 451 # 此处可以使用*args ,**kwargs减少很多参数,但为了pycharm更好的自动智能补全提示放弃这么做
- 452 @classmethod
- 453 def bulid_a_logger_with_mail_handler(cls, logger_name, log_level_int=10, *, is_add_stream_handler=True,
- 454 do_not_use_color_handler=False, log_path=get_logs_dir_by_disk_root(),
- 455 log_filename=None,
- 456 log_file_size=100, mongo_url=None,
- 457 formatter_template=5, mailhost: tuple = ('smtp.mxhichina.com', 465),
- 458 fromaddr: str = 'xxx@oo.com',
- 459 toaddrs: tuple = ('yy@oo.com', 460 'zzg@oo.com', 'xxxx@qq.com',
- 461 'ttttt@qq.com', 'hhh@qq.com', 'mmmm@oo.com'),
- 462 subject: str = '日志报警测试',
- 463 credentials: tuple = ('xxx@oo.com', '123456789'),
- 464 secure=None, timeout=5.0, is_use_ssl=True, mail_time_interval=60):
- 465 """
- 466 创建一个附带邮件handler的日志
- 467 :param logger_name:
- 468 :param log_level_int: 可以用1 2 3 4 5 ,用可以用官方logging模块的正规的10 20 30 40 50,兼容。
- 469 :param is_add_stream_handler:
- 470 :param do_not_use_color_handler:
- 471 :param log_path:
- 472 :param log_filename:
- 473 :param log_file_size:
- 474 :param mongo_url:
- 475 :param formatter_template:
- 476 :param mailhost:
- 477 :param fromaddr:
- 478 :param toaddrs:
- 479 :param subject:
- 480 :param credentials:
- 481 :param secure:
- 482 :param timeout:
- 483 :param is_use_ssl:
- 484 :param mail_time_interval: 邮件的频率控制,为0不限制,如果为100,代表100秒内相同内容的邮件最多发送一次邮件
- 485 :return:
- 486 """
- 487 if log_filename is None:
- 488 log_filename = f'{logger_name}.log'
- 489 logger = cls(logger_name).get_logger_and_add_handlers(log_level_int=log_level_int,
- 490 is_add_stream_handler=is_add_stream_handler,
- 491 do_not_use_color_handler=do_not_use_color_handler,
- 492 log_path=log_path, log_filename=log_filename,
- 493 log_file_size=log_file_size, mongo_url=mongo_url,
- 494 formatter_template=formatter_template, )
- 495 smtp_handler = CompatibleSMTPSSLHandler(mailhost, fromaddr,
- 496 toaddrs,
- 497 subject,
- 498 credentials,
- 499 secure,
- 500 timeout,
- 501 is_use_ssl,
- 502 mail_time_interval,
- 503 )
- 504 log_level_int = log_level_int * 10 if log_level_int < 10 else log_level_int
- 505 smtp_handler.setLevel(log_level_int)
- 506 smtp_handler.setFormatter(formatter_dict[formatter_template])
- 507 if not cls.__judge_logger_contain_handler_class(logger, CompatibleSMTPSSLHandler):
- 508 if logger.name == 'root':
- 509 for logger_x in cls.logger_list:
- 510 for hdlr in logger_x.handlers:
- 511 if isinstance(hdlr, CompatibleSMTPSSLHandler):
- 512 logger_x.removeHandler(hdlr)
- 513 logger.addHandler(smtp_handler)
- 514
- 515 return logger
- 516
- 517 # 加*是为了强制在调用此方法时候使用关键字传参,如果以位置传参强制报错,因为此方法后面的参数中间可能以后随时会增加更多参数,造成之前的使用位置传参的代码参数意义不匹配。
- 518 def get_logger_and_add_handlers(self, log_level_int: int = 10, *, is_add_stream_handler=True,
- 519 do_not_use_color_handler=False, log_path=get_logs_dir_by_disk_root(),
- 520 log_filename=None, log_file_size=100,
- 521 mongo_url=None,
- 522 formatter_template=5):
- 523 """
- 524 :param log_level_int: 日志输出级别,设置为 1 2 3 4 5,分别对应原生logging.DEBUG(10),logging.INFO(20),logging.WARNING(30),logging.ERROR(40),logging.CRITICAL(50)级别,现在可以直接用10 20 30 40 50了,兼容了。
- 525 :param is_add_stream_handler: 是否打印日志到控制台
- 526 :param do_not_use_color_handler :是否禁止使用color彩色日志
- 527 :param log_path: 设置存放日志的文件夹路径
- 528 :param log_filename: 日志的名字,仅当log_path和log_filename都不为None时候才写入到日志文件。
- 529 :param log_file_size :日志大小,单位M,默认10M
- 530 :param mongo_url : mongodb的连接,为None时候不添加mongohandler
- 531 :param formatter_template :日志模板,1为formatter_dict的详细模板,2为简要模板,5为最好模板
- 532 :type log_level_int :int
- 533 :type is_add_stream_handler :bool
- 534 :type log_path :str
- 535 :type log_filename :str
- 536 :type mongo_url :str
- 537 :type log_file_size :int
- 538 """
- 539 self._logger_level = log_level_int * 10 if log_level_int < 10 else log_level_int
- 540 self._is_add_stream_handler = is_add_stream_handler
- 541 self._do_not_use_color_handler = do_not_use_color_handler
- 542 self._log_path = log_path
- 543 self._log_filename = log_filename
- 544 self._log_file_size = log_file_size
- 545 self._mongo_url = mongo_url
- 546 self._formatter = formatter_dict[formatter_template]
- 547 self.__set_logger_level()
- 548 self.__add_handlers()
- 549 self.logger_name_list.append(self._logger_name)
- 550 self.logger_list.append(self.logger)
- 551 return self.logger
- 552
- 553 def get_logger_without_handlers(self):
- 554 """返回一个不带hanlers的logger"""
- 555 return self.logger
- 556
- 557 # noinspection PyMethodMayBeStatic,PyMissingOrEmptyDocstring
- 558 def look_over_all_handlers(self):
- 559 print(f'{self._logger_name}名字的日志的所有handlers是--> {self.logger.handlers}')
- 560
- 561 def remove_all_handlers(self):
- 562 for hd in self.logger.handlers:
- 563 self.logger.removeHandler(hd)
- 564
- 565 def remove_handler_by_handler_class(self, handler_class: type):
- 566 """
- 567 去掉指定类型的handler
- 568 :param handler_class:logging.StreamHandler,ColorHandler,MongoHandler,ConcurrentRotatingFileHandler,MongoHandler,CompatibleSMTPSSLHandler的一种
- 569 :return:
- 570 """
- 571 if handler_class not in (logging.StreamHandler, ColorHandler, MongoHandler, ConcurrentRotatingFileHandler, MongoHandler, CompatibleSMTPSSLHandler):
- 572 raise TypeError('设置的handler类型不正确')
- 573 for handler in self.logger.handlers:
- 574 if isinstance(handler, handler_class):
- 575 self.logger.removeHandler(handler)
- 576
- 577 def __set_logger_level(self):
- 578 self.logger.setLevel(self._logger_level)
- 579
- 580 def __remove_handlers_from_other_logger_when_logger_name_is_none(self, handler_class):
- 581 """
- 582 当logger name为None时候需要移出其他logger的handler,否则重复记录日志
- 583 :param handler_class: handler类型
- 584 :return:
- 585 """
- 586 if self._logger_name is None:
- 587 for logger in self.logger_list:
- 588 for hdlr in logger.handlers:
- 589 if isinstance(hdlr, handler_class):
- 590 logger.removeHandler(hdlr)
- 591
- 592 @staticmethod
- 593 def __judge_logger_contain_handler_class(logger: logging.Logger, handler_class):
- 594 for h in logger.handlers + logging.getLogger().handlers:
- 595 if isinstance(h, (handler_class,)):
- 596 return True
- 597
- 598 def __add_handlers(self):
- 599 if self._is_add_stream_handler:
- 600 if not self.__judge_logger_contain_handler_class(self.logger,
- 601 ColorHandler): # 主要是阻止给logger反复添加同种类型的handler造成重复记录
- 602 self.__remove_handlers_from_other_logger_when_logger_name_is_none(ColorHandler)
- 603 self.__add_stream_handler()
- 604
- 605 if all([self._log_path, self._log_filename]):
- 606 if not self.__judge_logger_contain_handler_class(self.logger, ConcurrentRotatingFileHandler):
- 607 self.__remove_handlers_from_other_logger_when_logger_name_is_none(ConcurrentRotatingFileHandler)
- 608 self.__add_file_handler()
- 609
- 610 if self._mongo_url:
- 611 if not self.__judge_logger_contain_handler_class(self.logger, MongoHandler):
- 612 self.__remove_handlers_from_other_logger_when_logger_name_is_none(MongoHandler)
- 613 self.__add_mongo_handler()
- 614
- 615 def __add_mongo_handler(self):
- 616 """写入日志到mongodb"""
- 617 mongo_handler = MongoHandler(self._mongo_url)
- 618 mongo_handler.setLevel(self._logger_level)
- 619 mongo_handler.setFormatter(self._formatter)
- 620 self.logger.addHandler(mongo_handler)
- 621
- 622 def __add_stream_handler(self):
- 623 """
- 624 日志显示到控制台
- 625 """
- 626 # stream_handler = logging.StreamHandler()
- 627 stream_handler = ColorHandler() if not self._do_not_use_color_handler else logging.StreamHandler() # 不使用streamhandler,使用自定义的彩色日志
- 628 stream_handler.setLevel(self._logger_level)
- 629 stream_handler.setFormatter(self._formatter)
- 630 self.logger.addHandler(stream_handler)
- 631
- 632 def __add_file_handler(self):
- 633 """
- 634 日志写入日志文件
- 635 """
- 636 if not os.path.exists(self._log_path):
- 637 os.makedirs(self._log_path)
- 638 log_file = os.path.join(self._log_path, self._log_filename)
- 639 rotate_file_handler = None
- 640 if os_name == 'nt':
- 641 # windows下用这个,非进程安全
- 642 rotate_file_handler = ConcurrentRotatingFileHandler(log_file, maxBytes=self._log_file_size * 1024 * 1024,
- 643 backupCount=3,
- 644 encoding="utf-8")
- 645 if os_name == 'posix':
- 646 # linux下可以使用ConcurrentRotatingFileHandler,进程安全的日志方式
- 647 rotate_file_handler = ConcurrentRotatingFileHandler(log_file, maxBytes=self._log_file_size * 1024 * 1024,
- 648 backupCount=3, encoding="utf-8")
- 649 rotate_file_handler.setLevel(self._logger_level)
- 650 rotate_file_handler.setFormatter(self._formatter)
- 651 self.logger.addHandler(rotate_file_handler)
- 652
- 653
- 654 class LoggerMixin(object):
- 655 subclass_logger_dict = {}
- 656
- 657 @property
- 658 def logger(self):
- 659 if self.__class__.__name__ + '1' not in self.subclass_logger_dict:
- 660 logger_var = LogManager(self.__class__.__name__).get_logger_and_add_handlers()
- 661 self.subclass_logger_dict[self.__class__.__name__ + '1'] = logger_var
- 662 return logger_var
- 663 else:
- 664 return self.subclass_logger_dict[self.__class__.__name__ + '1']
- 665
- 666 @property
- 667 def logger_with_file(self):
- 668 if self.__class__.__name__ + '2' not in self.subclass_logger_dict:
- 669 logger_var = LogManager(type(self).__name__).get_logger_and_add_handlers(log_filename=type(self).__name__ + '.log', log_file_size=50)
- 670 self.subclass_logger_dict[self.__class__.__name__ + '2'] = logger_var
- 671 return logger_var
- 672 else:
- 673 return self.subclass_logger_dict[self.__class__.__name__ + '2']
- 674
- 675 @property
- 676 def logger_with_file_mongo(self):
- 677 from app import config
- 678 if self.__class__.__name__ + '3' not in self.subclass_logger_dict:
- 679 logger_var = LogManager(type(self).__name__).get_logger_and_add_handlers(log_filename=type(self).__name__ + '.log', log_file_size=50, mongo_url=config.connect_url)
- 680 self.subclass_logger_dict[self.__class__.__name__ + '3'] = logger_var
- 681 return logger_var
- 682 else:
- 683 return self.subclass_logger_dict[self.__class__.__name__ + '3']
- 684
- 685
- 686 simple_logger = LogManager('simple').get_logger_and_add_handlers()
- 687 defaul_logger = LogManager('hotel').get_logger_and_add_handlers(do_not_use_color_handler=True, formatter_template=7)
- 688 file_logger = LogManager('hotelf').get_logger_and_add_handlers(do_not_use_color_handler=True,
- 689 log_filename='hotel_' + time.strftime("%Y-%m-%d",
- 690 time.localtime()) + ".log",
- 691 formatter_template=7)
- 692
- 693
- 694 # noinspection PyMethodMayBeStatic,PyNestedDecorators,PyArgumentEqualDefault
- 695 class _Test(unittest.TestCase):
- 696 # noinspection PyMissingOrEmptyDocstring
- 697 @classmethod
- 698 def tearDownClass(cls):
- 699 """
- 700
- 701 """
- 702 time.sleep(1)
- 703
- 704 @unittest.skip
- 705 def test_repeat_add_handlers_(self):
- 706 """测试重复添加handlers"""
- 707 LogManager('test').get_logger_and_add_handlers(log_path='../logs', log_filename='test.log')
- 708 LogManager('test').get_logger_and_add_handlers(log_path='../logs', log_filename='test.log')
- 709 LogManager('test').get_logger_and_add_handlers(log_path='../logs', log_filename='test.log')
- 710 test_log = LogManager('test').get_logger_and_add_handlers(log_path='../logs', log_filename='test.log')
- 711 print('下面这一句不会重复打印四次和写入日志四次')
- 712 time.sleep(1)
- 713 test_log.debug('这一句不会重复打印四次和写入日志四次')
- 714
- 715 @unittest.skip
- 716 def test_get_logger_without_hanlders(self):
- 717 """测试没有handlers的日志"""
- 718 log = LogManager('test2').get_logger_without_handlers()
- 719 print('下面这一句不会被打印')
- 720 time.sleep(1)
- 721 log.info('这一句不会被打印')
- 722
- 723 @unittest.skip
- 724 def test_add_handlers(self):
- 725 """这样可以在具体的地方任意写debug和info级别日志,只需要在总闸处规定级别就能过滤,很方便"""
- 726 LogManager('test3').get_logger_and_add_handlers(2)
- 727 log1 = LogManager('test3').get_logger_without_handlers()
- 728 print('下面这一句是info级别,可以被打印出来')
- 729 time.sleep(1)
- 730 log1.info('这一句是info级别,可以被打印出来')
- 731 print('下面这一句是debug级别,不能被打印出来')
- 732 time.sleep(1)
- 733 log1.debug('这一句是debug级别,不能被打印出来')
- 734
- 735 @unittest.skip
- 736 def test_only_write_log_to_file(self): # NOQA
- 737 """只写入日志文件"""
- 738 log5 = LogManager('test5').get_logger_and_add_handlers(20)
- 739 log6 = LogManager('test6').get_logger_and_add_handlers(is_add_stream_handler=False, log_filename='test6.log')
- 740 print('下面这句话只写入文件')
- 741 log5.debug('这句话只写入文件')
- 742 log6.debug('这句话只写入文件')
- 743
- 744 # @unittest.skip
- 745 def test_color_and_mongo_hanlder(self):
- 746 """测试彩色日志和日志写入mongodb"""
- 747 from app import config
- 748 logger = LogManager('helloMongo').get_logger_and_add_handlers(mongo_url=config.connect_url)
- 749 logger.debug('一个debug级别的日志')
- 750 logger.info('一个info级别的日志')
- 751 logger.warning('一个warning级别的日志')
- 752 logger.error('一个error级别的日志')
- 753 logger.critical('一个critical级别的日志')
- 754
- 755 @unittest.skip
- 756 def test_get_app_logs_dir(self): # NOQA
- 757 print(get_logs_dir_by_folder_name())
- 758 print(get_logs_dir_by_disk_root())
- 759
- 760 @unittest.skip
- 761 def test_none(self):
- 762 # noinspection PyUnusedLocal
- 763 log1 = LogManager('log1').get_logger_and_add_handlers()
- 764 LogManager().get_logger_and_add_handlers()
- 765
- 766 LogManager().get_logger_and_add_handlers()
- 767 log1 = LogManager('log1').get_logger_and_add_handlers()
- 768 LogManager().get_logger_and_add_handlers()
- 769 LogManager('log1').get_logger_and_add_handlers(log_filename='test_none.log')
- 770 log1.debug('打印几次?')
- 771
- 772 @unittest.skip
- 773 def test_formater(self):
- 774 logger2 = LogManager('test_formater2').get_logger_and_add_handlers(formatter_template=6)
- 775 logger2.debug('测试日志模板2')
- 776 logger5 = LogManager('test_formater5').get_logger_and_add_handlers(formatter_template=5)
- 777 logger5.error('测试日志模板5')
- 778 defaul_logger.debug('dddddd')
- 779 file_logger.info('ffffff')
- 780
- 781 @unittest.skip
- 782 def test_bulid_a_logger_with_mail_handler(self):
- 783 """
- 784 测试日志发送到邮箱中
- 785 :return:
- 786 """
- 787 logger = LogManager.bulid_a_logger_with_mail_handler('mail_logger_name', mail_time_interval=10, toaddrs=(
- 788 '909686719@qq.com', 'yangdefeng4508@dingtalk.com', 'defeng.yang@silknets.com'))
- 789 # LogManager.bulid_a_logger_with_mail_handler('mail_logger_name', mail_time_interval=10)
- 790 # LogManager.bulid_a_logger_with_mail_handler(None, log_filename='mail.log', mail_time_interval=10)
- 791 for _ in range(100):
- 792 logger.warning('啦啦啦啦啦')
- 793 logger.warning('测试邮件日志的内容。。。。')
- 794 time.sleep(2)
- 795
- 796 @unittest.skip
- 797 def test_remove_handler(self):
- 798 logger = LogManager('test13').get_logger_and_add_handlers()
- 799 logger.debug('去掉coloerhandler前')
- 800 LogManager('test13').remove_handler_by_handler_class(ColorHandler)
- 801 logger.debug('去掉coloerhandler后,此记录不会被打印')
- 802
- 803
- 804 if __name__ == "__main__":
- 805 unittest.main()