如何將Log4Net 日志保存到mongodb數據庫之實踐,log4netmongodb
log4net的大名早有耳聞,一直沒真正用過,這次開發APP項目准備在服務端使用log4net。 日志的數據量較大,頻繁的寫數據庫容易影響系統整體性能,所以獨立將日志寫到mongodb數據庫是不錯的選擇。---經過2天的摸索,總結出本文檔。
github有個開源項目log4mongo-net,另一位斯克迪亞作者根據開源項目又做了修改http://skyd.sinaapp.com/archives/1282。
所以直接拿斯克迪亞的代碼來使用。
1、將log4net和mongodb驅動升級為最新版本。log2net: 1.2.15 mongodb: 2.2.3.3
2、新加了一個LogHelper類(單件模式),所有的日志通過LogHelper的靜態方法來寫。
public class LogHelper
{
private static readonly LogHelper instance=new LogHelper();
private static ILog log = null;
private Logger()
{
// XmlConfigurator.Configure();
LogLog.InternalDebugging = true;
}
public static LogHelper getInstance()
{
if (log == null)
{
log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
}
return instance;
}
public void Debug(Object message, Exception exception)
{
log.Debug(message);
}
public void Debug(Object message)
{
log.Debug(message);
}
}
結果發現會有問題,log4net本來可以記錄日志發生所在的類名,和具體的行號。如果使用了單獨的類來寫日志,那麼記錄下來的類和行號都是LogHelper的內容,沒法定位到錯誤具體發生的位置。原因就在ILog是通過LogManger.GetLogger(Type)方法來獲的,要想獲取錯誤的類和行號,那麼必須在每個類裡單獨調用LogManger.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType)。(自己的理解,不一定准確)
3、自定義日志內容字段。網上能找到很多Log4net添加字段的方法,但因為我用的是修改過的log4mongo。而且簡化了配置,默認就是顯示所有字段。所以基本上通過改配置的都不適用。
開始准備使用 LogicalThreadContext.Properties["CustomColumn"] = "Custom value" ,可以將更多自定義的內容寫到日志。
執行寫日志log.Debug(object)之前,先把各種自定義內容通過LogicalThreadContext設置好。然後再執行log.Debug(object)。
這種方式雖然可以達到目的,但感覺不是很方便。另一個問題是設置LogicalThreadContext後,這些值會一直存在,在同一個線程裡再次執行 log.Debug(object)時,任然會記錄這些自定義的屬性值,不太好控制,容易寫錯內容。
另一種辦法是通過 log.Debug(object)裡的object來實現,一般情況下都是直接 log.Debug("this is message")這種方式來寫日志,也可以將一個對象傳遞給 log.Debug。在往數據庫寫數據前,可以通過loggingEvent.RenderedMessage來讀取傳遞過來的對象,
public class LogInfo
{
public string AppKey { set; get; }
public string UserID { set; get; }
public string HostName { set; get; }
public string IPAddress { set; get; }
public string Message { set; get; }
public override string ToString()
{
var bsonDoc = this.ToBsonDocument();
return bsonDoc.ToString();
}
}
默認loggingEvent.RenderedMessage是獲取object 的ToString()的值,這裡需要重寫一下ToString(),將類轉化為BsonDocument。
loggingEvent.RenderedMessage獲取BsonDocument後再拆分為具體的屬性。
private static void BuildCustomMessage(string message,ref Log log)
{
try
{
var bson = BsonDocument.Parse(message);
foreach (var item in bson.Elements)
{
string value = item.Value.ToString();
if (item.Value.IsBsonNull)
{
value = string.Empty;
}
log.Properties.Add(item.Name, value);
}
}
catch (Exception)
{
log.RenderedMessage = message;
}
}
為了方便,直接用 BsonDocument.Parse(message)來判斷是復雜對象還是String。直接用try catch來處理,對性能會有一點影響。做了測試相比直接傳遞String不需要BuildCustomMessage ,傳遞復雜對象時性能相差6%左右。這6%主要是對象ToString()轉化為BsonDocument,和BuildCustomMessage兩部份。
4、在測試時發生一個奇怪的問題,單獨一個語句log.Debug(string)可以輸出到控制台,但死活寫不到數據庫。而用一個循環來執行log.Debug(string),卻能正常寫到數據庫。 log4net有輸出Shutdown called on Hierarchy的提示,但不明白是什麼意思,也不知道是否有相關。
解決辦法是將寫數據庫的操作由異步改為同步。具體原因沒找到。log4net自身問題、 mongodb驅動 、 log4net緩存等等都沒法排除。
//collection.InsertOneAsync(BuildBsonDocument(loggingEvent));
collection.InsertOne(BuildBsonDocument(loggingEvent));
5、為了方便日後的維護,將數據庫名和Collection(表名)設置到配置文件。
<appender name="MongoDBAppender" type="Log4Mongo.MongoDBAppender, Log4Mongo">
<connectionString value="mongodb://root:123456@localhost:27017"/>
<DatabaseName value="log4mongo"/>
<CollectionName value="yyyyMM"/>
</appender>
配置的值在public class MongoDBAppender : AppenderSkeleton這個類裡會直接獲取,不需要做額外處理
public string ConnectionStringName { get; set; }
public string DatabaseName { get; set; }
public string CollectionName { get; set; }
<CollectionName value="yyyyMM"/>這麼寫的目的是讓日志根據日期來自動分表。通過修改配置就能按日、按月、按年來分表。
private IMongoCollection<Log> GetCollection()
{
string tableName;
tableName = CollectionName ?? "yyyyMM";
tableName = "log"+ string.Format("{0:" + tableName + "}", DateTime.Now);
return GetDatabase().GetCollection<Log>(tableName);
}