使用NLog通过Kafka实现日志收集
使用NLog通过Kafka实现日志收集,最终在Kibana展示
NuGet包引用
<PackageReference Include="NLog.Kafka" Version="0.2.1" /> <PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" />
Logstash配置
input {
kafka {
bootstrap_servers => "127.0.0.1:9092"
group_id => "logstash"
topics => "loges"
codec => "json"
}
}
output{
elasticsearch {
hosts => ["127.0.0.1:9002"]
index => "log_{[appname]}_%{+YYYY.MM.dd}"
}
}
项目NLog配置 nlog.config
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwConfigExceptions="true" internalLogLevel="info" internalLogFile="App_Data/logs/internal-nlog.txt"> <!--autoReload:修改后自动加载,可能会有延迟--> <!--throwConfigExceptions:NLog日志系统抛出异常--> <!--internalLogLevel:内部日志的级别--> <!--internalLogFile:内部日志保存路径,日志的内容大概就是NLog的版本信息,配置文件的地址等等--> <!--输出日志的配置,用于rules读取--> <!--扩展的程序集,官方文档:https://github.com/nlog/NLog/wiki/Extending%20NLog--> <extensions> <add assembly="NLog.Web.AspNetCore"/> <add assembly="WebApplication1"/> <add assembly="NLog.Kafka"/> <add assembly="Sentry.NLog"/> </extensions> <!-- Layout布局 ${var:basePath} basePath是前面自定义的变量 ${longdate} 日期格式 2017-01-17 16:58:03.8667 ${shortdate}日期格式 2017-01-17 ${date:yyyyMMddHHmmssFFF} 日期 20170117165803866 ${message} 输出内容 ${guid} guid ${level}日志记录的等级 ${logger} 配置的logger --> <!-- NLog记录等级 Trace - 最常见的记录信息,一般用于普通输出 Debug - 同样是记录信息,不过出现的频率要比Trace少一些,一般用来调试程序 Info - 信息类型的消息 Warn - 警告信息,一般用于比较重要的场合 Error - 错误信息 Fatal - 致命异常信息。一般来讲,发生致命异常之后程序将无法继续执行。 自上而下,等级递增。 --> <variable name="consoleLayout" value="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" /> <variable name="fileLayout" value="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" /> <targets> <!--控制台日志--> <target name="console" xsi:type="ColoredConsole" layout="${consoleLayout}" /> <!--文件日志,archive相关参数:文件拆分,archiveAboveSize="104857600":每100M拆分一个新文件 --> <target name="logFile" xsi:type="File" layout="${fileLayout}" fileName="C:/logs/${shortdate}/${hostname}/${logName}${shortdate}.log" archiveEvery="Day" archiveFileName="C:/logs/${shortdate}/${hostname}/${logName}${shortdate}.{####}.log" archiveNumbering="DateAndSequence" archiveDateFormat="yyyy-MM-dd" archiveAboveSize="104857600"/> <target name="kafka" xsi:type="Kafka" topic="logs.mbridge.taskapi" bootstrapServers="192.168.100.100:9092"> <property name="security.protocol" value="Plaintext" /> <property name="sasl.mechanism" value="PLAIN" /> <property name="acks" value="0" /> <layout xsi:type="JsonLayout"> <attribute name="Time" layout="${longdate}" /> <attribute name="EventId" layout="${event-properties:item=EventId_Id}" /> <attribute name="Level" layout="${uppercase:${level}}"/> <attribute name="Logger" layout="${logger}" /> <attribute name="Hostname" layout="${hostname}" /> <attribute name="LogName" layout="${logName}" /> <attribute name="Message" layout="${message}" /> <attribute name="Exception" layout="${exception:format=tostring}" /> </layout> </target> </targets> <rules> <!--路由顺序会对日志打印产生影响。路由匹配逻辑为顺序匹配。--> <!--路由映射关系 writeTo - 有writeTo才会写日志,没有此attr则忽略;writeTo的值对应<target>里面的 name="xxx" final - final="true"表示路由结束 --> <!-- NLog等级使用 指定特定等级 如:level="Warn" 指定多个等级 如:levels=“Warn,Debug“ 以逗号隔开 指定等级范围 如:minlevel="Warn" maxlevel="Error" --> <!--跳过microsoft的日志,不记录--> <logger name="Microsoft.AspNetCore.*" maxLevel="Warn" final="true" /> <!--跳过EasyNetQ的日志,不记录--> <logger name="EasyNetQ.*" maxLevel="Warn" final="true" /> <!--所有的日志, 包含以Microsoft开头的--> <logger name="*" maxlevel="Debug" writeTo="console" /> <logger name="*" minlevel="Debug" writeTo="logFile" /> <logger name="*" minlevel="Debug" writeTo="kafka" /> </rules> </nlog>
代码
public class LogUtlExHelper { private static bool _kafka = false; private static bool _file = false; /// <summary> /// NLog扩展的Property /// </summary> public const string NLOG_PROPERTY_LOGNAME = "logName"; /// <summary> /// NLog扩展的Property /// </summary> public const string NLOG_PROPERTY_HOSTNAME = "hostName"; public const string NLOG_TARGET_NAME_LOGFILE = "logFile"; public const string NLOG_TARGET_NAME_CONSOLE = "console"; public const string NLOG_TARGET_NAME_KAFKA = "kafka"; /// <summary> /// 文件日志的Logger,使用的name与NLog配置的target.name对应 /// </summary> public static Logger fileLogger; /// <summary> /// 控制台日志的Logger,使用的name与NLog配置的target.name对应 /// </summary> public static Logger consoleLogger; /// <summary> /// 控制台日志的Logger,使用的name与NLog配置的target.name对应 /// </summary> public static Logger kafkaLogger; static LogUtlExHelper() { InitNLog(); } public static void InitNLog() { fileLogger = LogManager.GetLogger(NLOG_TARGET_NAME_LOGFILE); consoleLogger = LogManager.GetLogger(NLOG_TARGET_NAME_CONSOLE); kafkaLogger = LogManager.GetLogger(NLOG_TARGET_NAME_KAFKA); SetNLogTargets(); } public static void SetNLogTargets() { //Kafka var target = (NLog.Targets.Kafka.KafkaTarget)kafkaLogger.Factory.Configuration.FindTargetByName(NLOG_TARGET_NAME_KAFKA); if (target != null && !string.IsNullOrEmpty(target.BootstrapServers)) { _kafka = true; } //File var targetFile = (NLog.Targets.FileTarget)kafkaLogger.Factory.Configuration.FindTargetByName(NLOG_TARGET_NAME_LOGFILE); if (targetFile != null && targetFile.FileName != null) { _file = true; } } #region File /// <summary> /// 添加日志 /// </summary> /// <param name="fileName">日志文件名称</param> /// <param name="message">日志内容</param> /// <param name="folderName">日志文件夹名称</param> public static void Info(string fileName, string message, string folderName = "") { WriteLog(LogLevel.Info, message, fileName, folderName); } /// <summary> /// 添加警告日志 /// </summary> /// <param name="fileName">日志文件名称</param> /// <param name="message">日志内容</param> /// <param name="folderName">日志文件夹名称</param> public static void Warn(string fileName, string message, string folderName = "") { WriteLog(LogLevel.Warn, message, fileName, folderName); } /// <summary> /// 添加异常日志 /// </summary> /// <param name="exception"></param> /// <param name="message">日志内容</param> /// <param name="fileName">日志文件名称</param> /// <param name="folderName">日志文件夹名称</param> public static void Error(Exception exception, string message = "", string fileName = "", string folderName = "") { Error(message, fileName, folderName, exception); } /// <summary> /// 添加异常日志 /// </summary> /// <param name="message">日志内容</param> /// <param name="fileName">日志文件名称</param> /// <param name="folderName">日志文件夹名称</param> /// <param name="exception"></param> public static void Error(string message, string fileName = "", string folderName = "", Exception exception = null) { if (string.IsNullOrEmpty(folderName)) { folderName = "Error"; } WriteLog(LogLevel.Error, message, fileName, folderName, exception); } /// <summary> /// 添加日志 /// </summary> /// <param name="logLevel">日志等级</param> /// <param name="message">日志内容</param> /// <param name="fileName">日志文件名称</param> /// <param name="folderName">日志文件夹名称</param> /// <param name="exception"></param> public static void WriteLog(LogLevel logLevel, string message, string fileName = "", string folderName = "", Exception exception = null) { string logName = string.Empty; string loggerName = string.Empty; if (_kafka) { //Kafka loggerName = kafkaLogger.Name; if (!string.IsNullOrEmpty(folderName) && !string.IsNullOrEmpty(fileName)) { logName = $"{folderName}/{fileName}"; } else if (!string.IsNullOrEmpty(folderName)) { logName = folderName ?? string.Empty; } else { logName = fileName ?? string.Empty; } LogEventInfo theEvent = new LogEventInfo(logLevel, loggerName, message); theEvent.Properties[NLOG_PROPERTY_LOGNAME] = logName; theEvent.Properties[NLOG_PROPERTY_HOSTNAME] = Environment.MachineName; theEvent.Exception = exception; kafkaLogger.Log(theEvent); } if (_file) { //本地文件日志 loggerName = fileLogger.Name; if (!string.IsNullOrEmpty(fileName)) { fileName = $"{fileName}_"; } if (!string.IsNullOrEmpty(folderName)) { logName = $"{folderName}/{fileName}"; } else { logName = fileName ?? string.Empty; } LogEventInfo theEvent = new LogEventInfo(logLevel, loggerName, message); theEvent.Properties[NLOG_PROPERTY_LOGNAME] = logName; theEvent.Properties[NLOG_PROPERTY_HOSTNAME] = Environment.MachineName; theEvent.Exception = exception; fileLogger.Log(theEvent); } } #endregion #region Console /// <summary> /// 控制台日志 /// </summary> /// <param name="message"></param> /// <param name="exception"></param> public static void ConsoleLog(string message, Exception exception = null) { ConsoleLog(LogLevel.Debug, message, exception); } /// <summary> /// 控制台日志 /// </summary> /// <param name="logLevel"></param> /// <param name="message"></param> /// <param name="exception"></param> public static void ConsoleLog(LogLevel logLevel, string message, Exception exception = null) { LogEventInfo theEvent = new LogEventInfo(logLevel, consoleLogger.Name, message); //theEvent.Properties["EventId"] = new Microsoft.Extensions.Logging.EventId(2, "eventId2"); theEvent.Exception = exception; consoleLogger.Log(theEvent); } #endregion }
/// <summary> /// custom layout renderer /// </summary> [LayoutRenderer(LogUtlExHelper.NLOG_PROPERTY_LOGNAME)] [ThreadSafe] public class NLogLayoutRender : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { if (!logEvent.Properties.ContainsKey(LogUtlExHelper.NLOG_PROPERTY_LOGNAME)) { return; } string logName = logEvent.Properties[LogUtlExHelper.NLOG_PROPERTY_LOGNAME] as string; if (!string.IsNullOrEmpty(logName)) { builder.Append(logName); } } } /// <summary> /// custom layout renderer /// </summary> [LayoutRenderer(LogUtlExHelper.NLOG_PROPERTY_HOSTNAME)] [ThreadSafe] public class NLogLayoutRender2 : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { if (!logEvent.Properties.ContainsKey(LogUtlExHelper.NLOG_PROPERTY_HOSTNAME)) { return; } string hostName = logEvent.Properties[LogUtlExHelper.NLOG_PROPERTY_HOSTNAME] as string; if (!string.IsNullOrEmpty(hostName)) { builder.Append(hostName); } } }
Kibana配置
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构