一般來說,對於何時寫日志並沒有明確的限制和約束,只要你覺得記錄的日志是有價值的,對跟蹤bug是有幫助的,你就可以去添加日志。當然一些敏感信息除外,比如你正在開發一套支付系統,不要把客戶的卡號和密碼等信息記錄在日志中,因為日志並不會被刻意保護,有可能被其他的用戶群體收集到。
另外不要擔心大量的日志會對服務器造成壓力,一般來說在產品環境都會采用消息隊列配合搜索引擎的方式存儲日志,通過定義准確的日志級別也會減少生產環境的日志數量。
一、如何記錄日志
以log4net為例,在想要添加日志的任何地方使用下列的方法添加日志:
private readonly ILog log = log4net.LogManager.GetLogger(typeof(T));
log.Info("message");
二、選擇准確的日志級別
一般來說日志組件都會有Debug,Info,Warn,Error,Fatal五種日志級別,其中Fatal用來處理Unhandled exception,因此對開發者而言在開發過程中只有四種可用的日志級別:
- Debug 用來記錄一些用於有利於調試程序的信息和系統狀態,一般來說這種級別的日志只會出現在開發環境
- Info 跟Debug類似,區別在於這種級別的日志可以上生產環境
- Warn 警告類日志,比如第三方請求返回了失敗的信息,但是開發者知道如何處理這種信息, 不會造成系統奔潰
- Error 一般可以用來記錄一些異常信息,比如配置文件丟失等
三、選擇正確的方法重載
以log.Error()舉例:
- 使用
log.Error(string message)的時機
if (string.IsNullOrEmpty(userName))
{
var message = "user name is empty";
logger.Error(message);
throw new UserNameEmptryException();
}
上面的使用方式是顯而易見的,業務代碼發現用戶名為空,記錄日志同時拋出異常,上面的場景不能使用下面的方式記錄日志:
logger.Error(new Exception(message));
選擇錯誤的重載會導致日志調用堆棧丟失,日志變得不再完整,最終會在查找日志的時候浪費時間。
- 使用
log.Error(Exeception exception)的時機
在捕獲到異常的時候使用此重載:
try
{
var response = AlipayHandler.Pay();
}
catch (AlipayRequestException e)
{
logger.Error(e);
throw;
}
- 使用
log.Error(string message, Exception exception)的時機
上面的場景也符合這個重載,區別在於你不但想記錄異常信息,還想添加自定義的信息。
四、設計全局的Unhandler exception handler
一般來說使用不同的框架,添加Unhandler exception handler的方式也不同。
- 對於ASP.NET MVC 項目,標准的方式是創建自定義ExceptionFilter:
public class UnhandledExceptionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var logger = LoggerFactory.GetLogger(typeof(UnhandledExceptionFilterAttribute));
var exception = filterContext.Exception;
logger.Fatal("Unhandled exception", exception);
base.OnActionExecuting(filterContext);
}
}
- 對於Winform則需要用另一種方式:
AppDomain.CurrentDomain.UnhandledException +=
(sender, e) =>
{
logger.Fatal("Unhandled exception", exception);;
};
- 對於asp.net core可以使用NLog來記錄unhandled exception。
總之,開發不同類型的應用程序都有相對應的方法來處理unhandled exception,使用時搜索最佳實踐即可。
五、錯誤的日志記錄方式
private void RequestAlipay()
{
try
{
var response = httpClient.Request(url);
}
catch (Exception e)
{
logger.Error(e);
}
}
上面記錄日志的方式犯了兩個錯誤:
- 不要捕獲通用的異常, 記住:只捕獲你知道如何處理的異常,如果你不知道如何處理他,請不要捕獲他。Why catch(Exception)/empty catch is bad
在上面的場景中,你可能會擔心由於網絡原因或者請求不合理等原因會導致整個請求失敗,你想在此時記錄日志,因此你的意圖是處理HttpRequestException而不是Exception。因此正確的寫法如下:
private void RequestAlipay()
{
try
{
var response = httpClient.Request(url);
}
catch (HttpRequestException e)
{
logger.Error(e);
}
}
你並不知道如何處理其他類型的異常,因此你不應該捕獲其他類型的異常。
2. 上面的Try...Catch會吞掉異常並吞掉bug,同時還有可能會將本應該在開發階段就能發現的問題推遲到了產品環境。
一般來說你之所以要捕獲異常是應為你知道如何處理該異常同時還知道一個恢復的方案Swallowing exceptions is hazardous to your health
這段代碼的意圖是向第三方請求資源,如果真的由於某些原因導致請求失敗,只寫日志是不夠的,你的應用程序還是可能會由此崩掉。但是開發人員想要查到事情的真相不再容易,因為你靜悄悄的吞掉了這個異常,看起來好像什么事情都沒有發生一樣。如果在此時你並不知道如何處理這種異常,你應該把異常拋出去,讓上層調用者做決定。
private void RequestAlipay()
{
try
{
var response = httpClient.Request(url);
}
catch (HttpRequestException e)
{
logger.Error(e);
throw;
}
}
注意: 不要通過下面的方式拋出異常,因為這樣會丟掉調用堆棧Why catch and rethrow an exception in C#?
throw ex;
六、一個正確記錄日志的例子
比如在一個ProductSellingService中:
public void Sell(string itemId)
{
Product product;
try
{
car = GetProducts().Single(x => x.ItemId.Contains(itemId));
}
catch (InvalidOperationException)
{
log.Error($"product {itemId} has been sold out.")
throw new ProductsHasBeenSoldOutException();
}
}
當用戶要購買的產品已經不存在時,除了記錄一條日志,還會拋出一個ProductsHasBeenSoldOutException異常。因為作為ProductSellingService並不知道如何處理這種異常,所以需要上層的調用者做決策。
在ProductSellingController中使用了ProductSellingService:
try
{
ProductSellingService.Sell(id);
var response = Request.CreateResponse(HttpStatusCode.OK, "successful");
}
catch (ProductsHasBeenSoldOutException e)
{
var response = Request.CreateResponse(HttpStatusCode.NotFound,
"product is been sold out");
return response;
}
ProductSellingController知道如何處理ProductsHasBeenSoldOutException,最終返回給用戶一個錯誤消息。
准確的使用日志可以方便bug追蹤,數據分析,達到事半功倍的效果,相反,錯誤的日志使用方式只會讓開發者在調查原因的過程中浪費時間,降低效率。本文將重點描述日志的代碼設計部分,在正式環境還要考慮如何通過消息隊列和搜索引擎存儲海量日志,以及日志監控等解決方案的設計,敬請期待后續文章。
