背景就不多說了菩颖,誰家沒有個幾個十系統(tǒng)在跑啊样漆。如何監(jiān)控這幾十個系統(tǒng)的運行狀況,對于非運營人員來說晦闰,太TM五花八門了放祟。。呻右。
名詞
- ELK = ElashticSearch + LogStash + Kibana
- Lucene 是搜索引擎跪妥,搜索引擎的特點就不用說了吧。但是使用起來不是太直觀声滥。
- ElashticSearch (簡稱 ES) 是基于 Luncene 的眉撵。它提供了一套易于使用的語法,關(guān)鍵一點:它可以很方便的透過 http 來操作落塑。
- Logstash 主要是用來分析(處理)日志的(不知道這樣講妥不妥)纽疟。通過指定 Logstash 的 Output ,可以把處理的結(jié)果寫到 ES 中憾赁。
- Kibana 是用于制定各種報表的污朽。
也就是說ELK中的: E(存儲),L(處理), K(展示)
ELK 需要 JAVA 運行環(huán)境龙考,但不代表它是 JAVA世界的專用工具蟆肆。
由來已久的門派對立
做為.NET開發(fā)人員矾睦,對 JAVA工具 多多少是有點抵觸的,能不用就不用炎功,能少用就少用枚冗,實在沒辦法在查資料。蛇损。官紫。我也是這樣過來的。
log4net 相信大家都在用州藕,所以我的最開始的方案是寫個 log4net 的Appender 擴(kuò)展束世, 從 AppenderSkeleton 派生一個 ESAppender , 代碼很簡單,不在這里展示了床玻。
但是寫日志的速度有點快(每天生產(chǎn)1.5G左右的文本日志毁涉,還是簡化過的。锈死。贫堰。), ES 的狀態(tài)不確定,可能會導(dǎo)致數(shù)據(jù)丟失待牵,或是ES處理不及時其屏,拖程序的后腿等。搜集日志是小事缨该,拖程序后腿就是大事了偎行。。贰拿。
所以蛤袒,最終還是老老實的使用 ELK 這一套完整的方案:
擴(kuò)展log4net 寫 json 格式的日志, logstash 搜集這些日志。膨更。妙真。
如何整合 ELK 到.NET 項目中
正如上面所說的原因,此處用 log4net 寫json 格式的文本日志荚守,因為 logstash 的配置語法是我們這些“基于界面”的珍德,“頭腦簡單”的程序員不能理解的(太麻煩,真心疼JAVA程序員矗漾,每天面對那么多天書一樣的配置)锈候; json 格式的日志,在 logstash 中缩功,是會被按原樣寫入到 ES中的晴及,省去那一堆不能理解的 filter 的 配置都办。
擴(kuò)展 log4net 嫡锌,從 LayoutSkeleton 派生一個 JsonLayout
/// <summary>
///
/// </summary>
public class JsonLayout : LayoutSkeleton
{
public override string ContentType
{
get
{
return "application/json";
}
}
public JsonLayout()
{
this.IgnoresException = false;
}
public override void ActivateOptions()
{
//
}
public override void Format(TextWriter writer, LoggingEvent evt)
{
if (!evt.Level.DisplayName.Equals("ES"))
return;
var info = evt.LocationInformation;
var exTitle = "";
var exStack = "";
if (evt.ExceptionObject != null)
{
exTitle = evt.ExceptionObject.Message;
exStack = evt.ExceptionObject.StackTrace;
}
var msg = new JsonMsg()
{
ESIndexPrefix = ESIndex.ESIndexPrefix,
Logger = evt.LoggerName,
//@Class = info.ClassName,//發(fā)布后虑稼,獲取不到該參數(shù)
//File = info.FileName,//發(fā)布后,獲取不到該參數(shù)
//Line = info.LineNumber,//發(fā)布后势木,獲取不到該參數(shù)
//Method = info.MethodName,//發(fā)布后蛛倦,獲取不到該參數(shù)
CreatedOn = evt.TimeStamp,
App = evt.Domain,
//Level = evt.Level.Name, 無用,點硬盤
Data = evt.MessageObject,
ExTitle = exTitle,
ExStack = exStack
};
var json = JsonConvert.SerializeObject(msg);
writer.WriteLine(json);
}
}
IgnoresException = false 是忽略 Exception 的輸出啦桌,否則溯壶,會在 json 字符串后面追加一串字符串用于描述異常信息。
JsonMsg.cs
internal class JsonMsg
{
[JsonProperty("i")]
public string ESIndexPrefix
{
get;
set;
}
[JsonProperty("L")]
public string Logger
{
get;
set;
}
[JsonProperty("On")]
public DateTime CreatedOn
{
get;
set;
}
[JsonProperty("D")]
public object Data
{
get;
set;
}
[JsonProperty("Ex")]
public string ExStack
{
get;
set;
}
[JsonProperty("ExT")]
public string ExTitle
{
get;
set;
}
public string App
{
get;
set;
}
}
添加一個 helper
public static class LogHelper
{
private static readonly Type DeclareType = typeof(LogHelper);
private static readonly Level Level = new Level(130000, "ES");
public static void ES(this ILog logger, AnalyzeLogItem data, Exception ex = null)
{
logger.Logger.Log(DeclareType, Level, data, ex);
}
}
這段代碼中自定義了一個叫 "ES" 的 LEVEL, 還定義了一個很簡單的擴(kuò)展函數(shù)甫男,使用自定義的參數(shù): AnalyzeLogItem且改, 這個 AnalyzeLogItem 就是要用于分析的數(shù)據(jù),比如執(zhí)行時間板驳,執(zhí)行是成功還是失敗又跛,響應(yīng)請求還是發(fā)送請求等等,依自己的需求而定若治。
然后修改一下 log4net.config
<log4net>
<appender name="ESAppender" type="log4net.Appender.RollingFileAppender">
<file value="logES/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<!--UTF-8 帶的 BOM 引發(fā) LOGSTASH 解析JSON失敗-->
<!--<Encoding value="UTF-8" />-->
<layout type="XXX.JsonLayout,XXX" />
</appender>
<logger name="ESLog" additivity="false">
<level value="ES" />
<appender-ref ref="ESAppender" />
</logger>
<appender name="InfoFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" />
<file value="logInfo/" />
<param name="AppendToFile" value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<!--可選為Size(按文件大锌丁),Date(按日期)端幼,Once(每啟動一次創(chuàng)建一個文件)礼烈,Composite(按日期及文件大小)婆跑,-->
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="INFO" />
<param name="LevelMax" value="INFO" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="ErrorFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logError/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="ERROR" />
<param name="LevelMax" value="ERROR" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="DebugFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logDebug/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="DEBUG" />
<param name="LevelMax" value="DEBUG" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="FatalFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logFatal/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="FATAL" />
<param name="LevelMax" value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="WARNFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logWARN/" />
<appendToFile value="true" />
<param name="DatePattern" value="yyyyMMddHH".txt"" />
<rollingStyle value="Composite" />
<CountDirection value="1" />
<maximumFileSize value="2MB" />
<staticLogFileName value="false" />
<Encoding value="UTF-8" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="FATAL" />
<param name="LevelMax" value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%d [%t] %-5p %l - %m%n" />
</layout>
</appender>
<root>
<!--all priority options: OFF此熬、FATAL、ERROR, WARN, INFO, DEBUG, ALL-->
<level value="DEBUG" />
<appender-ref ref="ConsoleAppender" />
<appender-ref ref="InfoFileAppender" />
<appender-ref ref="ErrorFileAppender" />
<appender-ref ref="FatalFileAppender" />
<appender-ref ref="DebugFileAppender" />
<appender-ref ref="WARNFileAppender" />
<appender-ref ref="ESAppender" />
</root>
</log4net>
注意第一段(ESAppender)中的 layout type="XXX.JsonLayout,XXX"滑进, 修改為自己的包名摹迷。
另外,不能使用 UTF-8 郊供。
因為在 WINDOWS 下峡碉,log4net 生產(chǎn)的UTF-8 日志文件默認(rèn)是帶BOM 的,logstash 這種JAVA世界的工具驮审,太理想化鲫寄,好像壓根就沒有考慮過 BOM 的問題,從而導(dǎo)至數(shù)據(jù)丟失嚴(yán)重(有多嚴(yán)重疯淫?幾百萬日記只分析出來個零頭)地来。。熙掺。
如果logstash 控制臺中出現(xiàn)以下這樣的字眼未斑,那就八九不離十了:
11:17:54.244 [[main]<file] ERROR logstash.codecs.json - JSON parse error, original data now in message field {:error=>#<LogStash::Json::ParserError: Unexpected character ('???' (code 65279 / 0xfeff)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')
最后,在你的 AssemblyInfo 中添加:
[assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]
配置 logstash
上面說了币绩,我們直接生成 json 格式的日志記錄蜡秽,就是為了避免復(fù)雜的 logstash 配置府阀。 所以這里的配置很簡單:
input{
file {
path => [
"D:/Web/Api1/W1/logES/*.*",
"D:/Web/Api1/W2/logES/*.*"
]
codec => "json"
}
}
output {
elasticsearch {
hosts => ["10.89.70.70:9600"]
index => "%{i}-%{+YYYY.MM.dd}"
}
}
- path 節(jié)點中的兩行即是要分析的日志路徑,多條用逗號分開芽突。
- hosts 即 ES 的地址(用內(nèi)網(wǎng)地址比外網(wǎng)地址快不止一個數(shù)量級)
- index 即動態(tài)的 index 名稱试浙, 其中的 i (%{i}) 即產(chǎn)生的 json log 中的 i (也就是上文中的 JsonMsg 中的 ESIndexPrefix). 這樣做的好處是可以將不同的系統(tǒng)的日志數(shù)據(jù)按 index 分類。
Kibana
kibana 的配置就不說了寞蚌,太簡單, 這里只上一張最終的日志分析出來的效果圖: