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

 

 

版权声明:本文为ydf0509原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/ydf0509/p/10502683.html