.NET 的 ELK 監(jiān)控方案

背景就不多說了菩颖,誰家沒有個幾個十系統(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&quot;.txt&quot;" />
    <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&quot;.txt&quot;" />
    <!--可選為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&quot;.txt&quot;" />
    <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&quot;.txt&quot;" />
    <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&quot;.txt&quot;" />
    <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&quot;.txt&quot;" />
    <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 的配置就不說了寞蚌,太簡單, 這里只上一張最終的日志分析出來的效果圖:

效果圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末田巴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挟秤,更是在濱河造成了極大的恐慌壹哺,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艘刚,死亡現(xiàn)場離奇詭異斗躏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)昔脯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門啄糙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人云稚,你說我怎么就攤上這事隧饼。” “怎么了静陈?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵燕雁,是天一觀的道長。 經(jīng)常有香客問我鲸拥,道長拐格,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任刑赶,我火速辦了婚禮捏浊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撞叨。我一直安慰自己金踪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布牵敷。 她就那樣靜靜地躺著胡岔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枷餐。 梳的紋絲不亂的頭發(fā)上靶瘸,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼怨咪。 笑死屋剑,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惊暴。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼趁桃,長吁一口氣:“原來是場噩夢啊……” “哼辽话!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起卫病,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤油啤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蟀苛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體益咬,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年帜平,在試婚紗的時候發(fā)現(xiàn)自己被綠了幽告。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡裆甩,死狀恐怖冗锁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嗤栓,我是刑警寧澤冻河,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站茉帅,受9級特大地震影響叨叙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜堪澎,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一擂错、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧樱蛤,春花似錦马昙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至土匀,卻和暖如春子房,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工证杭, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留田度,地道東北人。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓解愤,卻偏偏與公主長得像镇饺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子送讲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容