.NET 的 ELK 监控方案

作者: gruan | 来源:发表于2017-01-03 16:07 被阅读1569次

    背景就不多说了,谁家没有个几个十系统在跑啊。如何监控这几十个系统的运行状况,对于非运营人员来说,太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 的配置就不说了,太简单, 这里只上一张最终的日志分析出来的效果图:

    效果图

    相关文章

      网友评论

      • 6684e5218d73:请问有没有更详细的博客介绍?或者介绍一点资料看一下,最好中文的

      本文标题:.NET 的 ELK 监控方案

      本文链接:https://www.haomeiwen.com/subject/arcpvttx.html