.NET 的 ELK 监控方案

背景就不多说了,谁家没有个几个十系统在跑啊。如何监控这几十个系统的运行状况,对于非运营人员来说,太TM五花八门了。。。

名词

  • ELK = ElashticSearch + LogStash + Kibana
  • Lucene 是搜索引擎,搜索引擎的特点就不用说了吧。但是使用起来不是太直观。
  • ElashticSearch (简称 ES) 是基于 Luncene 的。它提供了一套易于使用的语法,关键一点:它可以很方便的透过 http 来操作。
  • Logstash 主要是用来分析(处理)日志的(不知道这样讲妥不妥)。通过指定 Logstash 的 Output ,可以把处理的结果写到 ES 中。
  • Kibana 是用于制定各种报表的。

也就是说ELK中的: E(存储),L(处理), K(展示)

ELK 需要 JAVA 运行环境,但不代表它是 JAVA世界的专用工具。

由来已久的门派对立

做为.NET开发人员,对 JAVA工具 多多少是有点抵触的,能不用就不用,能少用就少用,实在没办法在查资料。。。我也是这样过来的。

log4net 相信大家都在用,所以我的最开始的方案是写个 log4net 的Appender 扩展, 从 AppenderSkeleton 派生一个 ESAppender , 代码很简单,不在这里展示了。

但是写日志的速度有点快(每天生产1.5G左右的文本日志,还是简化过的。。。), ES 的状态不确定,可能会导致数据丢失,或是ES处理不及时,拖程序的后腿等。搜集日志是小事,拖程序后腿就是大事了。。。

所以,最终还是老老实的使用 ELK 这一套完整的方案:
扩展log4net 写 json 格式的日志, logstash 搜集这些日志。。。

如何整合 ELK 到.NET 项目中

正如上面所说的原因,此处用 log4net 写json 格式的文本日志,因为 logstash 的配置语法是我们这些“基于界面”的,“头脑简单”的程序员不能理解的(太麻烦,真心疼JAVA程序员,每天面对那么多天书一样的配置); json 格式的日志,在 logstash 中,是会被按原样写入到 ES中的,省去那一堆不能理解的 filter 的 配置。

扩展 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,//发布后,获取不到该参数
            //File = info.FileName,//发布后,获取不到该参数
            //Line = info.LineNumber,//发布后,获取不到该参数
            //Method = info.MethodName,//发布后,获取不到该参数
            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, 还定义了一个很简单的扩展函数,使用自定义的参数: AnalyzeLogItem, 这个 AnalyzeLogItem 就是要用于分析的数据,比如执行时间,执行是成功还是失败,响应请求还是发送请求等等,依自己的需求而定。

然后修改一下 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 引发 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(每启动一次创建一个文件),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 生产的UTF-8 日志文件默认是带BOM 的,logstash 这种JAVA世界的工具,太理想化,好像压根就没有考虑过 BOM 的问题,从而导至数据丢失严重(有多严重?几百万日记只分析出来个零头)。。。
如果logstash 控制台中出现以下这样的字眼,那就八九不离十了:

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 格式的日志记录,就是为了避免复杂的 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 节点中的两行即是要分析的日志路径,多条用逗号分开。
  • hosts 即 ES 的地址(用内网地址比外网地址快不止一个数量级)
  • index 即动态的 index 名称, 其中的 i (%{i}) 即产生的 json log 中的 i (也就是上文中的 JsonMsg 中的 ESIndexPrefix). 这样做的好处是可以将不同的系统的日志数据按 index 分类。

Kibana

kibana 的配置就不说了,太简单, 这里只上一张最终的日志分析出来的效果图:

效果图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容