1. 程式人生 > >找到合適的方案記錄服務端日誌

找到合適的方案記錄服務端日誌

  做過服務端開發的同學都清楚日誌是多麼的重要,你要分析應用當天的 PV/UV,你需要對日誌進行統計分析; 你需要排查程式 BUG, 你需要尋找日誌中的異常資訊等等, 所以, 建立一套合適的日誌體系是非常有必要的.
  
  日誌體系一般都會遵循這麼幾個原則 :

  • 根據應用的需要記錄對應的資訊

  • 用於後期離線統計的日誌資訊與記錄程式執行問題的日誌分開存放

  • 選擇合適的日誌結構和日誌記錄工具

本文介紹的日誌記錄環境 :

  • Spring/Rose Web 框架

  • SLF4J 日誌類

  • JSON格式的日誌

      後端開發的時候往往在系統中都存在不只一套日誌體系,這篇文章介紹的日誌方案用於後期離線統計分析, 對於其他不同的情況需要根據服務的需求而定.
      Json格式的資訊易於儲存和分析,對於規模不是很大的應用服務而言,使用Json格式用於日誌記錄是個非常不錯的選擇,由於日誌一般都是按行儲存,後期根據需要利用普通的Java程式或者Hadoop MapReduce 工具處理都特別的方便;而且Json格式其內部儲存類似於map結構,以Key/Value的形式表達資訊,基本能夠滿足實際的需求.

1. 日誌示例

  本文介紹的日誌記錄方法儲存的日誌資訊就類似與下面這樣 :

{"Url":"http://localhost:8081/RoseStudy/hello/showHowToRecordLog","Uri":"/RoseStudy/hello/showHowToRecordLog","RemoteIp":"127.0.0.1","HostIp":"127.0.0.1","ActionName":"showHowToRecordLog","Time":1452233120220,"LogSource":1,"JsonResult":{"errorCode":0,"reason":null,"result"
:"test show how to record log success...","status":"success"}}

  可以看到,一行日誌包含8個資訊(只是測試使用,實際應用中需要根據自己的需求加入不同的類別資訊), 分別記錄著我們以後統計需要用到的資訊.
  那麼,我們首先需要定義的就是這8個型別資訊的常量字串,以方便後期使用 :

/**
 * 日誌常量
 * Created by zhanghu on 12/24/15.
 */
public class Constants_ {

    /**
     *  日誌中包含的屬性欄位
     * */
    public static
final String Url = "Url"; public static final String Uri = "Uri"; public static final String RemoteIp = "RemoteIp"; public static final String HostIp = "HostIp"; public static final String ActionName = "ActionName"; public static final String Time = "Time"; public static final String LogSource = "LogSource"; public static final String JsonResult = "JsonResult"; }

2. 服務端記錄日誌的過程

  服務端在處理任務的時候(Rose中的Action,或者 Servlet中的service)就需要把處理的結果,過程之類的資訊記錄在日誌裡.即外部的一個HTTP請求過來,服務端就需要打一/多條日誌,就好像這樣 :

/**
     *  url : http://localhost:8081/RoseStudy/hello/showHowToRecordLog
     * */
    @Get("showHowToRecordLog")
    @Post("showHowToRecordLog")
    public String showHowToRecordLog(Invocation inv) {

        try {
            JSonResult jSonResult = JSonResult.newInstance();
            jSonResult.errorCode(0L).reason(null).result("test show how to record log success...").status("success");

            String logStr = LogGenerator_.getJsonLog(inv.getRequest().getRequestURL().toString(), inv.getRequest().getRequestURI(),
                    inv.getRequest().getRemoteAddr(), inv.getRequest().getLocalAddr(), "showHowToRecordLog",
                    LogSource_.ServerSide, jSonResult.toString());
            LogOutputer.Instance.outputLogFromServer(logStr);

        inv.getResponse().setContentType("application/json;charset=utf-8");
        inv.getResponse().setStatus(HttpServletResponse.SC_OK);

            inv.addModel("resultJsonString", jSonResult.toString());
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
        return "resultJson";
    }

  這裡使用的是Rose框架,我們不用過多的關注,在各種框架或者技術中我們只需要關注怎樣記錄日誌就可以了.
  所以,我們分析 try catch 中的日誌記錄過程 :

  • JsonResult
      這個類當然不是JDK中提供的,它是為了我們在給客戶端返回結果的時候簡化一些步驟而構造的,其內部實現僅僅就是一個 JSONObject, 包含了 errorCode, result 這樣的幾個 key ,實現程式碼如下 :
import net.sf.json.JSONObject;

public class JSonResult {

    private long errorCode;
    private Object reason;
    private Object result;
    private Object status;

    public static JSonResult newInstance() {
        return new JSonResult();
    }

    public JSonResult() {
        errorCode = -1;
    }

    public long getErrorCode() {
        return errorCode;
    }

    public JSonResult errorCode(long errorCode) {
        this.errorCode = errorCode;
        return this;
    }

    public Object getReason() {
        return reason;
    }

    public JSonResult reason(Object reason) {
        this.reason = reason;
        return this;
    }

    public Object getStatus() {
        return status;
    }

    public JSonResult status(Object status) {
        this.status = status;
        return this;
    }

    public Object getResult() {
        return result;
    }

    public JSonResult result(Object result) {
        this.result = result;
        return this;
    }

    @Override
    public String toString() {
        return toJson().toString();
    }

    public JSONObject toJson() {
        return JSONObject.fromObject(this);
    }
}
  • LogGenerator_.getJsonLog(…)
      根據名字我們可以看出我們用到這個介面獲取一條日誌資訊, 而這個日誌資訊我們可以猜出它就是一個JSONObject, 其中包含了上面 Constants_ 類中列出的那8個日誌類別, 那麼, 我們只需要把這些傳入介面的資訊 put 到JSONObject 中就OK了,實現程式碼如下 :
import net.sf.json.JSONObject;

/**
 * 生成日誌的服務
 * Created by zhanghu on 12/24/15.
 */
public class LogGenerator_ {

    /**
     *  可解析日誌包含這樣幾個點 :
     *      - Url           客戶端請求的地址
     *      - Uri           伺服器資源的地址
     *      - RemoteIp      客戶端的IP地址
     *      - HostIp        服務端的IP地址
     *      - ActionName    請求函式的名字
     *      - source_       日誌源
     *      - JsonResult    伺服器返回的結果
     * */
    public static String getJsonLog(String Url, String Uri,
                                    String RemoteIp, String HostIp,
                                    String ActionName,
                                    LogSource_ source_, String JsonResult) {

        JSONObject object = new JSONObject();

        object.put(Constants_.Url, Url);
        object.put(Constants_.Uri, Uri);
        object.put(Constants_.RemoteIp, RemoteIp);
        object.put(Constants_.HostIp, HostIp);
        object.put(Constants_.ActionName, ActionName);
        object.put(Constants_.Time, System.currentTimeMillis());
        object.put(Constants_.LogSource, LogSource_.getValue(source_));
        object.put(Constants_.JsonResult, JsonResult);

        return object.toString();
    }
}
  • LogSource_
      這個類的作用是區分日誌源的, 日誌源也是後期統計分析的一個重要的組成部分, 比如,這條日誌是來自服務端, 客戶端, 還是 未知屬性, 我們用一個列舉來實現 :
/**
 * 日誌源列舉類
 * Created by zhanghu on 12/24/15.
 */
public enum LogSource_ {

    ServerSide(1, "服務端"),
    Unknown(2, "未知");

    private int value;
    private String description;

    LogSource_(int value, String description) {
        this.value = value;
        this.description = description;
    }

    public int getValue() {
        return value;
    }

    public String getDescription() {
        return description;
    }

    public static int getValue(LogSource_ source_) {
        if (source_ == null) {
            return Unknown.getValue();
        }

        return source_.getValue();
    }

    public static String getDescription(LogSource_ source_) {
        if (source_ == null) {
            return Unknown.getDescription();
        }

        return source_.getDescription();
    }
}
  • LogOutputer.Instance.outputLogFromServer(logStr)
      這個介面用於序列化日誌到某一個儲存位置, 從 Instance 這個詞可以猜到,這是一個單例的實現, 由於,介面比較簡單,不做過多的解釋了,直接給出實現程式碼 :
/**
 * 日誌輸出類
 * Created by zhanghu on 12/24/15.
 */
public class LogOutputer {

    public static LogOutputer Instance = new LogOutputer();

    private ILogSerializer logSerializer = null;

    private LogOutputer() {
        this.logSerializer = new LogSerializerImpl();
    }

    /**
     *  這個是真正寫日誌的介面
     * */
    public void outputLogFromServer(String jsonObjStr) {
        logSerializer.serializerLog(jsonObjStr);
    }
}
/**
 * 序列化日誌介面
 * Created by zhanghu on 12/24/15.
 */
public interface ILogSerializer {

    void serializerLog(String logStr);
}
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 序列化日誌的實現類
 * Created by zhanghu on 12/24/15.
 */
public class LogSerializerImpl implements ILogSerializer {

    /**
     *  這裡需要定義兩套日誌系統 :
     *      一類是定義用作統計的日誌系統  logger
     *      另一類是記錄性的日誌系統,一般不用做解析    allLogger
     * */
    private static final Logger logger = LoggerFactory.getLogger("roseLog");
    private static final Logger allLogger = LoggerFactory.getLogger(LogSerializerImpl.class);

    @Override
    public void serializerLog(String logStr) {

        try {
            JSONObject object = JSONObject.fromObject(logStr);
            object.put(Constants_.Time, System.currentTimeMillis());
            logger.info(object.toString());
        } catch (Exception e) {
            allLogger.error("Write Rose Log Error : {}", e.getMessage());
        }
    }
}

  好了, 到這裡, 我們已經在我們的系統中構造了一套方便解析的日誌系統, 接下來, 埋到我們的應用系統中然後進行統計分析吧 !