示例:储存日志

很多程序在运行的时候都会生成一些日志,这些日志记录了程序的运行状态以及执行过的重要操作。

比如说,以下展示的就是 Redis 服务器运行时输出的一些日志,这些日志记录了 Redis 开始运行的时间,载入数据库所耗费的时长,接收客户端连接所使用的端口号,以及进行数据持久化操作的时间点等信息:

  1. 6066:M 06 Jul 17:40:49.611 # Server started, Redis version 3.1.999
  2. 6066:M 06 Jul 17:40:49.627 * DB loaded from disk: 0.016 seconds
  3. 6066:M 06 Jul 17:40:49.627 * The server is now ready to accept connections on port 6379
  4. 6066:M 06 Jul 18:29:20.009 * DB saved on disk

为了记录程序运行的状态,又或者为了对日志进行分析,我们有时候会需要把程序生成的日志储存起来。

比如说,我们可以通过使用 SET 命令,将日志的生成时间用作键、日志的内容用作值,把上面展示的日志储存到多个字符串键里面:

  1. redis> SET "06 Jul 17:40:49.611" "# Server started, Redis version 3.1.999"
  2. OK
  3.  
  4. redis> SET "06 Jul 17:40:49.627" "* DB loaded from disk: 0.016 seconds"
  5. OK
  6.  
  7. redis> SET "06 Jul 17:40:49.627" "* The server is now ready to accept connections on port 6379"
  8. OK
  9.  
  10. redis> SET "06 Jul 18:29:20.009" "* DB saved on disk"
  11. OK

遗憾的是,这种日志储存方式并不理想,它的主要问题有两个:

  • 这种方法需要在数据库里面创建非常多的键。因为 Redis 每创建一个键就需要消耗一定的额外资源(overhead)来对键进行维护,所以键的数量越多,消耗的额外资源就会越多。

  • 这种方法将全部日志分散地储存在不同的键里面,当程序想要对特定的日志进行分析的时候,它就需要花费额外的时间和资源去查找指定的日志,这给分析操作带来了额外的麻烦和资源消耗。

代码清单 2-5 展示了另一种更为方便和高效的日志储存方式,这个程序会把同一天之内产生的所有日志都储存在同一个字符串键里面,从而使得用户可以非常高效地取得指定日期内产生的所有日志。


代码清单 2-5 使用字符串键实现高效的日志储存程序:/string/log.py

  1. LOG_SEPARATOR = "\n"
  2.  
  3. class Log:
  4.  
  5. def __init__(self, client, key):
  6. self.client = client
  7. self.key = key
  8.  
  9. def add(self, new_log):
  10. """
  11. 将给定的日志储存起来。
  12. """
  13. new_log += LOG_SEPARATOR
  14. self.client.append(self.key, new_log)
  15.  
  16. def get_all(self):
  17. """
  18. 以列表形式返回所有日志。
  19. """
  20. all_logs = self.client.get(self.key)
  21. if all_logs is not None:
  22. log_list = all_logs.split(LOG_SEPARATOR)
  23. log_list.remove("")
  24. return log_list
  25. else:
  26. return []

日志储存程序的 add() 方法负责将新日志储存起来。这个方法首先会将分隔符追加到新日志的末尾:

  1. new_log += LOG_SEPARATOR

然后调用 APPEND 命令,将新日志追加到已有日志的末尾:

  1. self.client.append(self.key, new_log)

举个例子,如果用户输入的日志是:

  1. "this is log1"

那么 add() 方法首先会把分隔符 "\n" 追加到这行日志的末尾,使之变成:

  1. "this is log1\n"

然后调用以下命令,将新日志追到已有日志的末尾:

  1. APPEND key "this is log1\n"

负责获取所有日志的 get_all() 方法比较复杂,因为它不仅需要从字符串键里面取出包含了所有日志的字符串值,还需要从这个字符串值里面分割出每一条日志。首先,这个方法使用 GET 命令从字符串键里面取出包含了所有日志的字符串值:

  1. all_logs = self.client.get(self.key)

接着,程序会检查 all_logs 这个值是否为空,如果为空则表示没有日志被储存,程序直接返回空列表 [] 作为 get_all() 方法的执行结果;另一方面,如果值不为空,那么程序将调用 Python 的 split() 方法对字符串值进行分割,并将分割结果储存到 log_list 列表里面:

  1. log_list = all_logs.split(LOG_SEPARATOR)

因为 split() 方法会在结果中包含一个空字符串,而我们并不需要这个空字符串,所以程序还会调用 remove() 方法,将空字符串从分割结果中移除,使得 log_list 列表里面只保留被分割的日志:

  1. log_list.remove("")

在此之后,程序只需要将包含了多条日志的 log_list 列表返回给调用者就可以了:

  1. return log_list

举个例子,假设我们使用 add() 方法,在一个字符串键里面储存了 "this is log1"this is log2"this is log3" 这三条日志,那么 get_all() 方法在使用 GET 命令获取字符串键的值时,将得到以下结果:

  1. "this is log1\nthis is log2\nthis is log3"

在使用 split(LOG_SEPARATOR) 方法对这个结果进行分割之后,程序将得到一个包含四个元素的列表,其中列表最后的元素为空字符串:

  1. ["this is log1", "this is log2", "this is log3", ""]

在调用 remove("") 方法移除列表中的空字符串之后,列表里面就只会包含被储存的日志:

  1. ["this is log1", "this is log2", "this is log3"]

这时 get_all() 方法只需要把这个列表返回给调用者就可以了。

以下代码展示了这个日志储存程序的使用方法:

  1. >>> from redis import Redis
  2. >>> from log import Log
  3. >>> client = Redis(decode_responses=True)
  4. >>> # 按日期归类日志
  5. >>> log = Log(client, "06 Jul")
  6. >>> # 储存日志
  7. >>> log.add("17:40:49.611 # Server started, Redis version 3.1.999")
  8. >>> log.add("17:40:49.627 * DB loaded from disk: 0.016 seconds")
  9. >>> log.add("17:40:49.627 * The server is now ready to accept connections on port 6379")
  10. >>> log.add("18:29:20.009 * DB saved on disk")
  11. >>> # 以列表形式返回所有日志
  12. >>> log.get_all()
  13. ['17:40:49.611 # Server started, Redis version 3.1.999', '17:40:49.627 * DB loaded from disk: 0.016 seconds', '17:40:49.627 * The server is now ready to accept connections on port 6379', '18:29:20.009 * DB saved on disk']
  14. >>> # 单独打印每条日志
  15. >>> for i in log.get_all():
  16. ... print(i)
  17. ...
  18. 17:40:49.611 # Server started, Redis version 3.1.999
  19. 17:40:49.627 * DB loaded from disk: 0.016 seconds
  20. 17:40:49.627 * The server is now ready to accept connections on port 6379
  21. 18:29:20.009 * DB saved on disk