找到合適的方案記錄服務端日誌
做過服務端開發的同學都清楚日誌是多麼的重要,你要分析應用當天的 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());
}
}
}
好了, 到這裡, 我們已經在我們的系統中構造了一套方便解析的日誌系統, 接下來, 埋到我們的應用系統中然後進行統計分析吧 !