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的靜態方法來寫。
1 public class Helper 2 { 3 private static readonly Helper instance = new Helper(); 4 private static ILog apiLog = null; 5 private static ILog dbLog = null; 6 7 private Helper() 8 { 9 // XmlConfigurator.Configure(); 10 LogLog.InternalDebugging = true; 11 12 13 } 14 public static ILog ApiLog() 15 { 16 if (apiLog == null) 17 { 18 apiLog = LogManager.GetLogger("ApiLog"); 19 } 20 return apiLog; 21 22 } 23 public static ILog DbLog() 24 { 25 if (dbLog == null) 26 { 27 dbLog = LogManager.GetLogger("ApiLog"); 28 } 29 return dbLog; 30 31 } 32 }
調用
Log4Mongo.Helper.DbLog().Info("abc");
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);
}