使用Redis記錄系統日誌

South發表於2017-10-11

使用Redis記錄系統日誌


在構建一個系統時,我們常常需要記錄當前發生的事情,以及記錄特定訊息出現的頻率,根據出現頻率的高低來決定訊息的排列資訊,幫助我們找到重要的資訊。

常見記錄日誌的方法有兩種:

  1. 將日誌記錄在檔案中。隨時時間流逝將日誌行不斷新增到檔案裡面,並在一段時間後建立新的日誌檔案。這種方式為每個不同的服務建立不同的日誌,由於服務輪換日誌的機制不同,也缺少一種能夠方便地聚合所有日誌並對其進行處理的常見方法。
  2. syslog服務。這種服務幾乎執行在Linux伺服器和Unix伺服器的514號TCP埠和UDP埠上。syslog接受其他程式發來的日誌訊息,並將這個訊息路由至儲存在硬碟上的各個日誌檔案,並且負責舊日誌的輪換和刪除工作。甚至還可以將日誌訊息轉發給其他服務來做進一步的處理。

syslog的轉發功能可以將不同的日誌分別儲存在同一臺伺服器的多個檔案裡面,對於長時間地記錄日誌非常有幫助。我們可以使用redis來儲存與時間緊密相關的日誌,從而在功能上替代那些需要在短期內被儲存的syslog訊息。

1. 最新日誌

我們需要使用 “列表” 來儲存最新日誌檔案,使用LPUSH命令將日誌訊息推入到列表中。如果我們之後想要檢視已有日誌訊息的話,可以使用LRANGE命令來拉取列表中的訊息。

我們還要命名不同的日誌訊息佇列,根據問題的嚴重性對日誌進行分級。

import time
import logging
import unittest
import redis
from datetime import datetime

# 設定一個字典,將大部分日誌的安全級別對映為字串
SEVERITY = {
    logging.DEBUG: `debug`,
    logging.INFO: `info`,
    logging.WARNING: `warning`,
    logging.ERROR: `error`,
    logging.CRITICAL: `critical`,
}

SEVERITY.update((name, name) for name in SEVERITY.values())

"""
儲存最新日誌檔案,命名不同的日誌訊息佇列,根據問題的嚴重性對日誌進行分級

@param {object}
@param {string} name    訊息佇列名稱
@param {string} message 訊息
@param {string} severity安全級別
@param {object} pip     pipline

"""
def logRecent(conn, name, message, severity=logging.INFO, pip=None):
    # 將日誌的安全級別轉換為簡單的字串
    severity = str(SEVERITY.get(severity, severity)).lower()
    # 建立要儲存的redis列表key
    destination = `recent:%s:%s`%(name, severity)
    # 將當前時間加到訊息裡面,用於記錄訊息的傳送時間
    message = time.asctime() + ` ` + message
    # 使用流水線來將通訊往返次數降低為一次
    pipe = pip or conn.pipeline()
    # 將訊息新增到列表的最前面
    pipe.lpush(destination, message)
    # 修剪日誌列表,讓它只包含最新的100條訊息
    pipe.ltrim(destination, 0, 99)
    pipe.execute()

2. 常見日誌

我們需要記錄較高頻率出現的日誌,使用“有序集合”,將訊息作為成員,訊息出現的頻率為成員的分值。

為了確保我們看到的常見訊息都是最新的,需要以每小時一次的頻率對訊息進行輪換,並在輪換日誌的時候保留上一個小時記錄的常見訊息,從而防止沒有任何訊息儲存的情況出現。

"""
記錄較高頻率出現的日誌,每小時一次的頻率對訊息進行輪換,並在輪換日誌的時候保留上一個小時記錄的常見訊息

@param {object}
@param {string} name    訊息佇列名稱
@param {string} message 訊息
@param {string} severity安全級別
@param {int}    timeout 執行超時時間

"""
def logCommon(conn, name, message, severity=logging.INFO, timeout=5):
    # 設定日誌安全級別
    severity = str(SEVERITY.get(severity, severity)).lower()
    # 負責儲存近期的常見日誌訊息的鍵
    destination = `common:%s:%s`%(name, severity)
    # 每小時需要輪換一次日誌,需要記錄當前的小時數
    start_key = destination + `:start`
    pipe = conn.pipeline()
    end = time.time() + timeout
    while time.time() < end:
        try:
            # 對記錄當前小時數的鍵進行監聽,確保輪換操作可以正常進行
            pipe.watch(start_key)
            # 當前時間
            now = datetime.utcnow().timetuple()
            # 取得當前所處的小時數
            hour_start = datetime(*now[:4]).isoformat()

            existing = pipe.get(start_key)
            # 開始事務
            pipe.multi()
            # 如果這個常見日誌訊息記錄的是上個小時的日誌
            if existing and existing < hour_start:
                # 將這些舊的常見日誌歸檔
                pipe.rename(destination, destination + `:last`)
                pipe.rename(start_key, destination + `:pstart`)
                # 更新當前所處的小時數
                pipe.set(start_key, hour_start)
            elif not existing:
                pipe.set(start_key, hour_start)

            # 記錄日誌出現次數
            pipe.zincrby(destination, message)
            # 將日誌記錄到日誌列表中,呼叫excute
            logRecent(pipe, name, message, severity, pipe)
            return
        except redis.exceptions.WatchError:
            continue

測試
測試程式碼如下:

class TestLog(unittest.TestCase):
    def setUp(self):
        import redis
        self.conn = redis.Redis(db=15)
        self.conn.flushdb

    def tearDown(self):
        self.conn.flushdb()
        del self.conn
        print
        print

    def testLogRecent(self):
        import pprint
        conn = self.conn

        print "Let`s write a few logs to the recent log"
        for msg in xrange(5):
            logRecent(conn, `test`, `this is message %s`%msg)

        recent = conn.lrange(`recent:test:info`, 0, -1)
        print `The current recent message log has this many message:`, len(recent)
        print `Those message include:`
        pprint.pprint(recent[:10])
        self.assertTrue(len(recent) >= 5)

    def testLogCommon(self):
        import pprint
        conn = self.conn

        print "Let`s writ a few logs to the common log"
        for count in xrange(1, 6):
            for i in xrange(count):
                logCommon(conn, `test`, `message-%s`%count)

        common = conn.zrevrange(`common:test:info`, 0, -1, withscores=True)
        print `The current common message log has this many message:`, len(common)
        print `Those common message include:`
        pprint.pprint(common)
        self.assertTrue(len(common) >= 5)

if __name__ == `__main__`:
    unittest.main()

相關文章