关于通过web api记录日志的问题讨论
一般接入日志框架需要添加各种配置信息,也免不了引入相关类库。
如果写个Web Api,提供接口记日志,然后针对不同平台封装相应的类库,这样就不用在每个记录日志的项目加配置信息了。
打个比方:
1.有个Web Api 利用NLog.Mongo将日志记录到数据库,向外提供不同业务日志或者日志等级的接口,这都是业务实现,先不讨论。
2.根据web api提供的接口封装相应的方法提供给客户端,可以理解成工具类,针对web api的,比如日志等级,发邮件等等。异步post
3.项目引用封装类库,就可以调用相应方法记日志了
4.提供界面查看日志
之所以这么干就是想避免项目里配置节点或者添加日志配置文件,还是我这种处理方式把问题想复杂了?但我真的很讨厌配置,虽然有的并不麻烦,web api也可以进行分布式部署
问题是这么干有什么缺点吗,如果高并发采用异步post会有什么隐患吗。像mongodb的数据写入性能我觉得可以忽略.
我认为你的想法是对的,当项目变多时,日志在每个项目中各自配置使得总体成本增加的较快,
于是便想着将这些重复的逻辑挪到一个地方,比如你说的api,这是符合srp的原则。
但是我认为 异步post到MongoDB ,这个地方异步的创建(或使用)可能会造成性能瓶颈。
因为我看过一篇文,在写日志的时候使用了同步语句,而且解释为什么没有使用异步,
他认为写日志的实现消耗应该小于异步创建的消耗。
总体我认为这是一个相对平衡的问题,就是 写日志消耗的时间代价要大于集中管理日志收获的价值。
异步开启线程会有消耗,这一块得考虑值不值当。可以考虑用线程池来减少创建和销毁线程的开销。
先按这思路试试看吧。
性能问题感觉还是异步post上。接口并发大的话可以有其他解决方案。真正写web api的话内部可以随便整其他日志框架,写文件 数据库 es都可以
总之还是希望尽量减少各种依赖和配置吧。
其实不用异步也行,完全可以理解成秒杀接口,秒杀后台那一套逻辑可以搬到记日志的api里面
1 async 默认是从线程池中取线程,
2 调用api写日志,应该是走网络的,走网络就若干问题,即便是内网,由于写日志的成功与否不影响业务的执行,这是它可以异步的出发点。
我感觉并发实在要求的话,先扔到 MQ 中,接着再持久化(同你说的秒杀应该是相同的原理,这个地方我不确认)。处理高要求的一般思路CPU-内存-存储器。
开发环境
vs2017,
.NET Core2.1,
数据写入到mongodb。
实现思路
1.服务端提供接口写入日志web api,
2.客户端基于接口封装类库LogApiHandler,负责调用web api。
3.引入类库使用
源码在最后
背景
很多开源项目像nlog、log4net、elk、exceptionless等都挺好的。就是除了引入所需类库,还要在项目中添加配置。
elk 在分布式海量数据收集和检索方面可能更能发挥它的优势,单纯记日志也可以.
exceptionless就是基于elk的。
就想着写一个简单易用的、可以发邮件报警的,直接引入类库就能用的一个记日志工具,所有的配置信息和入库都交给web api。这是当时问的问题,https://q.cnblogs.com/q/109489/。干脆就实现了先
接口没有加访问限制,先默认内网使用,当然有热心网友给出实现的话就更好了,像ip限制或者签名等等(已改,http basic Authorization进行认证)
一、实现Web Api
- 新建.net core web api项目 【LogWebApi】
因为要发邮件和写入mongodb,先改配置文件appsettings.json
{ "ConnectionStrings": { "ConnectionString": "mongodb://yourmongoserver", "Database": "logdb", "LogCollection": "logdata" }, "AllowedHosts": "*", "AppSettings": { "SendMailInfo": { "SMTPServerName": "smtp.qiye.163.com", "SendEmailAdress": "发送人邮箱", "SendEmailPwd": "", "SiteName": "邮件主题", "SendEmailPort": "123" } } }
- 实现依赖注入获取配置文件信息
创建目录结构如下图
AppSettings类

public class AppSettings { public SendMailInfo SendMailInfo { get; set; } } public class SendMailInfo { public string SMTPServerName { get; set; } public string SendEmailAdress { get; set; } public string SendEmailPwd { get; set; } public string SiteName { get; set; } public string SendEmailPort { get; set; } }
DBSettings类

/// <summary> /// 数据库配置信息 /// </summary> public class DBSettings { /// <summary> /// mongodb connectionstring /// </summary> public string ConnectionString { get; set; } /// <summary> /// mongodb database /// </summary> public string Database { get; set; } /// <summary> /// 日志collection /// </summary> public string LogCollection { get; set; } }
接下来Here is how we modify Startup.cs to inject Settings in the Options accessor model:

public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.Configure<DBSettings>(Configuration.GetSection("ConnectionStrings"));//数据库连接信息 services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));//其他配置信息 }
在项目中将通过IOptions 接口来获取配置信息,后面看代码吧
IOptions<AppSettings>
IOptions<DBSettings>
配置文件信息获取算是准备完了
- 创建日志信息Model
在Model文件夹下创建类LogEventData,也就是存到mongodb的信息

public class LogEventData { [BsonId] public ObjectId Id { get; set; } /// <summary> /// 时间 /// </summary> [BsonDateTimeOptions(Representation = BsonType.DateTime, Kind = DateTimeKind.Local)] public DateTime Date { get; set; } /// <summary> /// 错误级别 /// </summary> public string Level { get; set; } /// <summary> /// 日志来源 /// </summary> public string LogSource { get; set; } /// <summary> /// 日志信息 /// </summary> public string Message { get; set; } /// <summary> /// 类名 /// </summary> public string ClassName { get; set; } /// <summary> /// 方法名 /// </summary> public string MethodName { get; set; } /// <summary> /// 完整信息 /// </summary> public string FullInfo { get; set; } /// <summary> /// 行号 /// </summary> public string LineNumber { get; set; } /// <summary> /// 文件名 /// </summary> public string FileName { get; set; } /// <summary> /// ip /// </summary> public string IP { get; set; } /// <summary> /// 是否发送邮件,不为空则发送邮件,多个接收人用英文逗号隔开 /// </summary> [JsonIgnore] public string Emails { get; set; } public override string ToString() { return JsonConvert.SerializeObject(this); } }
- 定义database Context
站点根目录新建文件夹Context和类,别忘了引用 MongoDB.Driver nuget包

public class MongoContext { private readonly IMongoDatabase _database = null; private readonly string _logCollection; public MongoContext(IOptions<DBSettings> settings) { var client = new MongoClient(settings.Value.ConnectionString); if (client != null) _database = client.GetDatabase(settings.Value.Database); _logCollection = settings.Value.LogCollection; } public IMongoCollection<LogEventData> LogEventDatas { get { return _database.GetCollection<LogEventData>(_logCollection); } } }
- 添加Repository
别纠结为什么叫这个名了,就是数据访问类,像是常用的DAL,创建目录如下,之后可以通过依赖注入来访问具体实现
IRepository类

public interface IRepository<T> where T:class { Task<IEnumerable<T>> GetAll(); Task<T> Get(string id); Task Add(T item); Task<bool> Remove(string id); Task<bool> Update(string id, string body); }
LogRepository类

public class LogRepository : IRepository<LogEventData> { private readonly MongoContext _context = null; public LogRepository(IOptions<DBSettings> settings) { _context = new MongoContext(settings); } public async Task Add(LogEventData item) { await _context.LogEventDatas.InsertOneAsync(item); } public async Task<IEnumerable<LogEventData>> GetList(QueryLogModel model) { var builder = Builders<LogEventData>.Filter; FilterDefinition<LogEventData> filter = builder.Empty; if (!string.IsNullOrEmpty(model.Level)) { filter = builder.Eq("Level", model.Level); } if (!string.IsNullOrEmpty(model.LogSource)) { filter = filter & builder.Eq("LogSource", model.LogSource); } if (!string.IsNullOrEmpty(model.Message)) { filter = filter & builder.Regex("Message", new BsonRegularExpression(new Regex(model.Message))); } if (DateTime.MinValue != model.StartTime) { filter = filter & builder.Gte("Date", model.StartTime); } if(DateTime.MinValue != model.EndTime) { filter = filter & builder.Lte("Date", model.EndTime); } return await _context.LogEventDatas.Find(filter) .SortByDescending(log => log.Date) .Skip((model.PageIndex - 1) * model.PageSize) .Limit(model.PageSize).ToListAsync(); } #region 未实现方法 public async Task<LogEventData> Get(string id) { throw new NotImplementedException(); } public async Task<IEnumerable<LogEventData>> GetAll() { throw new NotImplementedException(); } public Task<bool> Remove(string id) { throw new NotImplementedException(); } public Task<bool> Update(string id, string body) { throw new NotImplementedException(); } #endregion }
为了通过DI model来访问LogRepository,修改Startup.cs ,ConfigureServices添加如下代码
services.AddTransient<IRepository<LogEventData>, LogRepository>();//数据访问
到这基本的数据写入和查询算是写完了,下面来实现Controller
- 创建LogController

[Route("api/[controller]")] [ApiController] public class LogController : ControllerBase { private readonly LogRepository _logRepository; IOptions<AppSettings> _appsettings; public LogController(IRepository<LogEventData> logRepository,IOptions<AppSettings> appsettings) { _logRepository = (LogRepository)logRepository; _appsettings = appsettings; } [Route("trace")] [HttpPost] public void Trace([FromBody] LogEventData value) { Add(value); } [Route("debug")] [HttpPost] public void Debug([FromBody] LogEventData value) { Add(value); } [Route("info")] [HttpPost] public void Info([FromBody] LogEventData value) { Add(value); } [Route("warn")] [HttpPost] public void Warn([FromBody] LogEventData value) { Add(value); } [Route("error")] [HttpPost] public void Error([FromBody] LogEventData value) { Add(value); } [Route("fatal")] [HttpPost] public void Fatal([FromBody] LogEventData value) { Add(value); } private async void Add(LogEventData data) { if (data != null) { await _logRepository.Add(data); if (!string.IsNullOrEmpty(data.Emails)) { new EmailHelpers(_appsettings).SendMailAsync(data.Emails, "监测邮件", data.ToString()); } } } [HttpGet("getlist")] public async Task<ResponseModel<IEnumerable<LogEventData>>> GetList([FromQuery] QueryLogModel model) { ResponseModel<IEnumerable<LogEventData>> resp = new ResponseModel<IEnumerable<LogEventData>>(); resp.Data = await _logRepository.GetList(model); return resp; } }
控制器里整个逻辑很简单,除了向外提供不同日志级别的写入接口,也实现了日志查询接口给日志查看站点用,基本上够用了。到这编译的话会报错,有一些类还没加上,稍后加上。在Add方法内部,用到了new EmailHelpers。讲道理按.net core 对依赖注入的使用 ,这个 new是不应该出现在这的,就先这么着吧,下面补类:
先创建Model文件夹下的两个类,很简单就不解释了
QueryLogModel类

public class QueryLogModel { private int _pageindex = 1; private int _pagesize = 20; public int PageIndex { get { return _pageindex; } set { _pageindex = value; } } public int PageSize { get { return _pagesize; } set { _pagesize = value; } } public string Level { get; set; } public string LogSource { get; set; } public string Message { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } }
ResponseModel类

public class ResponseModel<T> { private HttpStatusCode _resultCode = HttpStatusCode.OK; private string _message = "请求成功"; private T _data = default(T); /// <summary> /// 返回码 /// </summary> public HttpStatusCode ResultCode { get { return this._resultCode; } set { this._resultCode = value; } } /// <summary> /// 结果说明 /// </summary> public string Message { get { return this._message; } set { this._message = value; } } /// <summary> /// 返回的数据 /// </summary> public T Data { get { return this._data; } set { this._data = value; } } }
创建EmailHelpers类

public class EmailHelpers { private SendMailInfo _mailinfo; public EmailHelpers(IOptions<AppSettings> appsettings) { _mailinfo = appsettings.Value.SendMailInfo; } /// <summary> /// 异步发送邮件 /// </summary> /// <param name="emails">email地址</param> /// <param name="subject">邮件标题</param> /// <param name="content">邮件内容</param> public void SendMailAsync(string emails, string subject, string content) { Task.Factory.StartNew(() => { SendEmail(emails, subject, content); }); } /// <summary> /// 邮件发送方法 /// </summary> /// <param name="emails">email地址</param> /// <param name="subject">邮件标题</param> /// <param name="content">邮件内容</param> /// <returns></returns> public void SendEmail(string emails, string subject, string content) { string[] emailArray = emails.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); string fromSMTP = _mailinfo.SMTPServerName; //邮件服务器 string fromEmail = _mailinfo.SendEmailAdress; //发送方邮件地址 string fromEmailPwd = _mailinfo.SendEmailPwd;//发送方邮件地址密码 string fromEmailName = _mailinfo.SiteName; //发送方称呼 try { //新建一个MailMessage对象 MailMessage aMessage = new MailMessage(); aMessage.From = new MailAddress(fromEmail, fromEmailName); foreach (var item in emailArray) { aMessage.To.Add(item); } aMessage.Subject = subject; aMessage.Body = content; System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); aMessage.BodyEncoding = Encoding.GetEncoding("utf-8"); aMessage.IsBodyHtml = true; aMessage.Priority = MailPriority.High; aMessage.ReplyToList.Add(new MailAddress(fromEmail, fromEmailName)); SmtpClient smtp = new SmtpClient(); smtp.Host = fromSMTP; smtp.Timeout = 20000; smtp.UseDefaultCredentials = false; smtp.EnableSsl = true; smtp.DeliveryMethod = SmtpDeliveryMethod.Network; smtp.Credentials = new NetworkCredential(fromEmail, fromEmailPwd); //发邮件的EMIAL和密码 smtp.Port = int.Parse(_mailinfo.SendEmailPort); smtp.Send(aMessage); } catch (Exception ex) { throw ex; } } }
此类里需要引用nuget:System.Text.Encoding.CodePages, 那行报错的代码如果不想引用删掉就行
到这接口基本上就可以用了。
但是再加三个东西
- 扩展
添加全局异常捕获服务
ExceptionMiddlewareExtensions类

/// <summary> /// 全局异常处理中间件 /// </summary> public static class ExceptionMiddlewareExtensions { public static void ConfigureExceptionHandler(this IApplicationBuilder app, IOptions<DBSettings> settings) { LogRepository _repository = new LogRepository(settings); app.UseExceptionHandler(appError => { appError.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.ContentType = "application/json"; var contextFeature = context.Features.Get<IExceptionHandlerFeature>(); if (contextFeature != null) { await _repository.Add(new LogEventData { Message= contextFeature.Error.ToString(), Date=DateTime.Now, Level="Fatal", LogSource= "LogWebApi" }); await context.Response.WriteAsync(context.Response.StatusCode + "-Internal Server Error."); } }); }); } }
修改Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IOptions<DBSettings> settings) { app.ConfigureExceptionHandler(settings); }
添加MessagePack扩展
messagepack可以让我们在post数据的时候序列化数据,“压缩”数据传输大小,这个会结合针对接口封装的类库配合使用。
引用nuget: MessagePack和WebApiContrib.Core.Formatter.MessagePack
在ConfigureServices添加代码
services.AddMvcCore().AddMessagePackFormatters();
services.AddMvc().AddMessagePackFormatters();
扩展了"application/x-msgpack", "application/msgpack",在接下来封装的类库中会使用"application/x-msgpack",在web api来引入这个东西就是为了能解析从客户端传过来的数据
,用以支持添加Swagger支持
引用nuget:Swashbuckle.AspNetCore
修改ConfigureServices
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
修改Configure

// Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = string.Empty;//在应用的根 (http://localhost:<port>/) 处提供 Swagger UI });
到这整个web api站点算是写完了,编译不出错就ok了。
二、实现类库
类库整体目录结构如下
1.新建类库LogApiHandler
2.实现
- 创建日志信息类,和WebApi那个对应,LogEventData

/// <summary> /// 日志数据 /// post到日志接口的数据 /// </summary> public class LogEventData { /// <summary> /// 时间 /// </summary> public DateTime Date { get; set; } /// <summary> /// 错误级别 /// </summary> public string Level { get; set; } /// <summary> /// 日志来源 /// </summary> public string LogSource { get; set; } /// <summary> /// 日志信息 /// </summary> public string Message { get; set; } /// <summary> /// 类名 /// </summary> public string ClassName { get; set; } /// <summary> /// 方法名 /// </summary> public string MethodName { get; set; } /// <summary> /// 完整信息 /// </summary> public string FullInfo { get; set; } /// <summary> /// 行号 /// </summary> public string LineNumber { get; set; } /// <summary> /// 文件名 /// </summary> public string FileName { get; set; } /// <summary> /// ip /// </summary> public string IP { get; set; } /// <summary> /// 不为空则发送邮件,多个接收人用英文分号 ; 隔开 /// </summary> public string Emails { get; set; } }
- 创建日志级别类,就是其他开源项目常见的Level

internal class LogLevel { /// <summary> /// Trace log level. /// </summary> public static readonly LogLevel Trace = new LogLevel("Trace", 0); /// <summary> /// Debug log level. /// </summary> public static readonly LogLevel Debug = new LogLevel("Debug", 1); /// <summary> /// Info log level. /// </summary> public static readonly LogLevel Info = new LogLevel("Info", 2); /// <summary> /// Warn log level. /// </summary> public static readonly LogLevel Warn = new LogLevel("Warn", 3); /// <summary> /// Error log level. /// </summary> public static readonly LogLevel Error = new LogLevel("Error", 4); /// <summary> /// Fatal log level. /// </summary> public static readonly LogLevel Fatal = new LogLevel("Fatal", 5); private readonly int _ordinal; private readonly string _name; /// <summary> /// Initializes a new instance of <see cref="LogLevel"/>. /// </summary> /// <param name="name">The log level name.</param> /// <param name="ordinal">The log level ordinal number.</param> private LogLevel(string name, int ordinal) { _name = name; _ordinal = ordinal; } /// <summary> /// Gets the name of the log level. /// </summary> public string Name => _name; /// <summary> /// Gets the ordinal of the log level. /// </summary> public int Ordinal => _ordinal; /// <summary> /// 请求地址 /// </summary> public string LogApi { get { switch (_name) { case "Trace": return "http://localhost:56503/api/log/trace"; case "Debug": return "http://localhost:56503/api/log/debug"; case "Info": return "http://localhost:56503/api/log/info"; case "Warn": return "http://localhost:56503/api/log/warn"; case "Error": return "http://localhost:56503/api/log/error"; case "Fatal": return "http://localhost:56503/api/log/fatal"; default: return ""; } } } /// <summary> /// Returns the <see cref="T:NLog.LogLevel"/> that corresponds to the supplied <see langword="string" />. /// </summary> /// <param name="levelName">The textual representation of the log level.</param> /// <returns>The enumeration value.</returns> public static LogLevel FromString(string levelName) { if (levelName == null) { throw new ArgumentNullException(nameof(levelName)); } if (levelName.Equals("Trace", StringComparison.OrdinalIgnoreCase)) { return Trace; } if (levelName.Equals("Debug", StringComparison.OrdinalIgnoreCase)) { return Debug; } if (levelName.Equals("Info", StringComparison.OrdinalIgnoreCase)) { return Info; } if (levelName.Equals("Warn", StringComparison.OrdinalIgnoreCase)) { return Warn; } if (levelName.Equals("Error", StringComparison.OrdinalIgnoreCase)) { return Error; } if (levelName.Equals("Fatal", StringComparison.OrdinalIgnoreCase)) { return Fatal; } throw new ArgumentException($"Unknown log level: {levelName}"); } }
上面代码是NLog Level源码,修改了一下,因为这个项目并不复杂,也不需要读配置。类里面有个LogApi属性会根据级别返回相应的日志接口,生产环境得在部署完WebApi站点以后,将里面的接口信息替换掉然后编译发布再用。
- 创建日志追踪信息类,行号、类名、文件等等
LogEventData类中有几个属性如下图,接下来创建的类就是为了获取它们
在项目中创建文件夹Core,结构如下,创建顺序为MethodItem、StackFrameItem、LocationInfo,它们层层相扣,最终由LocationInfo提供所需信息
MethodItem类

internal class MethodItem { #region Public Instance Constructors /// <summary> /// constructs a method item for an unknown method. /// </summary> public MethodItem() { m_name = NA; m_parameters = new string[0]; } /// <summary> /// constructs a method item from the name of the method. /// </summary> /// <param name="name"></param> public MethodItem(string name) : this() { m_name = name; } /// <summary> /// constructs a method item from the name of the method and its parameters. /// </summary> /// <param name="name"></param> /// <param name="parameters"></param> public MethodItem(string name, string[] parameters) : this(name) { m_parameters = parameters; } /// <summary> /// constructs a method item from a method base by determining the method name and its parameters. /// </summary> /// <param name="methodBase"></param> public MethodItem(System.Reflection.MethodBase methodBase) : this(methodBase.Name, GetMethodParameterNames(methodBase)) { } #endregion private static string[] GetMethodParameterNames(System.Reflection.MethodBase methodBase) { ArrayList methodParameterNames = new ArrayList(); try { System.Reflection.ParameterInfo[] methodBaseGetParameters = methodBase.GetParameters(); int methodBaseGetParametersCount = methodBaseGetParameters.GetUpperBound(0); for (int i = 0; i <= methodBaseGetParametersCount; i++) { methodParameterNames.Add(methodBaseGetParameters[i].ParameterType + " " + methodBaseGetParameters[i].Name); } } catch (Exception ex) { //LogLog.Error(declaringType, "An exception ocurred while retreiving method parameters.", ex); } return (string[])methodParameterNames.ToArray(typeof(string)); } #region Public Instance Properties /// <summary> /// Gets the method name of the caller making the logging /// request. /// </summary> /// <value> /// The method name of the caller making the logging /// request. /// </value> /// <remarks> /// <para> /// Gets the method name of the caller making the logging /// request. /// </para> /// </remarks> public string Name { get { return m_name; } } /// <summary> /// Gets the method parameters of the caller making /// the logging request. /// </summary> /// <value> /// The method parameters of the caller making /// the logging request /// </value> /// <remarks> /// <para> /// Gets the method parameters of the caller making /// the logging request. /// </para> /// </remarks> public string[] Parameters { get { return m_parameters; } } #endregion #region Private Instance Fields private readonly string m_name; private readonly string[] m_parameters; #endregion #region Private Static Fields /// <summary> /// The fully qualified type of the StackFrameItem class. /// </summary> /// <remarks> /// Used by the internal logger to record the Type of the /// log message. /// </remarks> private readonly static Type declaringType = typeof(MethodItem); /// <summary> /// When location information is not available the constant /// <c>NA</c> is returned. Current value of this string /// constant is <b>?</b>. /// </summary> private const string NA = "?"; #endregion Private Static Fields }
StackFrameItem类

internal class StackFrameItem { #region Public Instance Constructors /// <summary> /// returns a stack frame item from a stack frame. This /// </summary> /// <param name="frame"></param> /// <returns></returns> public StackFrameItem(StackFrame frame) { // set default values m_lineNumber = NA; m_fileName = NA; m_method = new MethodItem(); m_className = NA; try { // get frame values m_lineNumber = frame.GetFileLineNumber().ToString(System.Globalization.NumberFormatInfo.InvariantInfo); m_fileName = frame.GetFileName(); // get method values MethodBase method = frame.GetMethod(); if (method != null) { if (method.DeclaringType != null) m_className = method.DeclaringType.FullName; m_method = new MethodItem(method); } } catch (Exception ex) { } // set full info m_fullInfo = m_className + '.' + m_method.Name + '(' + m_fileName + ':' + m_lineNumber + ')'; } #endregion #region Public Instance Properties /// <summary> /// Gets the fully qualified class name of the caller making the logging /// request. /// </summary> /// <value> /// The fully qualified class name of the caller making the logging /// request. /// </value> /// <remarks> /// <para> /// Gets the fully qualified class name of the caller making the logging /// request. /// </para> /// </remarks> public string ClassName { get { return m_className; } } /// <summary> /// Gets the file name of the caller. /// </summary> /// <value> /// The file name of the caller. /// </value> /// <remarks> /// <para> /// Gets the file name of the caller. /// </para> /// </remarks> public string FileName { get { return m_fileName; } } /// <summary> /// Gets the line number of the caller. /// </summary> /// <value> /// The line number of the caller. /// </value> /// <remarks> /// <para> /// Gets the line number of the caller. /// </para> /// </remarks> public string LineNumber { get { return m_lineNumber; } } /// <summary> /// Gets the method name of the caller. /// </summary> /// <value> /// The method name of the caller. /// </value> /// <remarks> /// <para> /// Gets the method name of the caller. /// </para> /// </remarks> public MethodItem Method { get { return m_method; } } /// <summary> /// Gets all available caller information /// </summary> /// <value> /// All available caller information, in the format /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c> /// </value> /// <remarks> /// <para> /// Gets all available caller information, in the format /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c> /// </para> /// </remarks> public string FullInfo { get { return m_fullInfo; } } #endregion Public Instance Properties #region Private Instance Fields private readonly string m_lineNumber; private readonly string m_fileName; private readonly string m_className; private readonly string m_fullInfo; private readonly MethodItem m_method; #endregion #region Private Static Fields /// <summary> /// The fully qualified type of the StackFrameItem class. /// </summary> /// <remarks> /// Used by the internal logger to record the Type of the /// log message. /// </remarks> private readonly static Type declaringType = typeof(StackFrameItem); /// <summary> /// When location information is not available the constant /// <c>NA</c> is returned. Current value of this string /// constant is <b>?</b>. /// </summary> private const string NA = "?"; #endregion Private Static Fields
LocationInfo类

internal class LocationInfo { #region Public Instance Constructors /// <summary> /// Constructor /// </summary> /// <param name="callerStackBoundaryDeclaringType">The declaring type of the method that is /// the stack boundary into the logging system for this call.</param> /// <remarks> /// <para> /// Initializes a new instance of the <see cref="LocationInfo" /> /// class based on the current thread. /// </para> /// </remarks> public LocationInfo(Type callerStackBoundaryDeclaringType) { // Initialize all fields m_className = NA; m_fileName = NA; m_lineNumber = NA; m_methodName = NA; m_fullInfo = NA; #if !(NETCF || NETSTANDARD1_3) // StackTrace isn't fully implemented for NETSTANDARD1_3 https://github.com/dotnet/corefx/issues/1797 if (callerStackBoundaryDeclaringType != null) { try { StackTrace st = new StackTrace(true); int frameIndex = 0; // skip frames not from fqnOfCallingClass while (frameIndex < st.FrameCount) { StackFrame frame = st.GetFrame(frameIndex); if (frame != null && frame.GetMethod().DeclaringType == callerStackBoundaryDeclaringType) { break; } frameIndex++; } // skip frames from fqnOfCallingClass while (frameIndex < st.FrameCount) { StackFrame frame = st.GetFrame(frameIndex); if (frame != null && frame.GetMethod().DeclaringType != callerStackBoundaryDeclaringType) { break; } frameIndex++; } if (frameIndex < st.FrameCount) { // take into account the frames we skip above int adjustedFrameCount = st.FrameCount - frameIndex; ArrayList stackFramesList = new ArrayList(adjustedFrameCount); m_stackFrames = new StackFrameItem[adjustedFrameCount]; for (int i = frameIndex; i < st.FrameCount; i++) { stackFramesList.Add(new StackFrameItem(st.GetFrame(i))); } stackFramesList.CopyTo(m_stackFrames, 0); // now frameIndex is the first 'user' caller frame StackFrame locationFrame = st.GetFrame(frameIndex); if (locationFrame != null) { System.Reflection.MethodBase method = locationFrame.GetMethod(); if (method != null) { m_methodName = method.Name; if (method.DeclaringType != null) { m_className = method.DeclaringType.FullName; } } m_fileName = locationFrame.GetFileName(); m_lineNumber = locationFrame.GetFileLineNumber().ToString(System.Globalization.NumberFormatInfo.InvariantInfo); // Combine all location info m_fullInfo = m_className + '.' + m_methodName + '(' + m_fileName + ':' + m_lineNumber + ')'; } } } catch (System.Security.SecurityException) { // This security exception will occur if the caller does not have // some undefined set of SecurityPermission flags. //LogLog.Debug(declaringType, "Security exception while trying to get caller stack frame. Error Ignored. Location Information Not Available."); } } #endif } /// <summary> /// 自定义获取位置信息,异步线程内获取期望值 /// </summary> /// <param name="callerStackBoundaryDeclaringType"></param> /// <param name="st"></param> public LocationInfo(Type callerStackBoundaryDeclaringType,StackTrace st) { // Initialize all fields m_className = NA; m_fileName = NA; m_lineNumber = NA; m_methodName = NA; m_fullInfo = NA; #if !(NETCF || NETSTANDARD1_3) // StackTrace isn't fully implemented for NETSTANDARD1_3 https://github.com/dotnet/corefx/issues/1797 if (callerStackBoundaryDeclaringType != null) { try { //StackTrace st = new StackTrace(true); int frameIndex = 0; // skip frames not from fqnOfCallingClass while (frameIndex < st.FrameCount) { StackFrame frame = st.GetFrame(frameIndex); if (frame != null && frame.GetMethod().DeclaringType == callerStackBoundaryDeclaringType) { break; } frameIndex++; } // skip frames from fqnOfCallingClass while (frameIndex < st.FrameCount) { StackFrame frame = st.GetFrame(frameIndex); if (frame != null && frame.GetMethod().DeclaringType != callerStackBoundaryDeclaringType) { break; } frameIndex++; } if (frameIndex < st.FrameCount) { // take into account the frames we skip above int adjustedFrameCount = st.FrameCount - frameIndex; ArrayList stackFramesList = new ArrayList(adjustedFrameCount); m_stackFrames = new StackFrameItem[adjustedFrameCount]; for (int i = frameIndex; i < st.FrameCount; i++) { stackFramesList.Add(new StackFrameItem(st.GetFrame(i))); } stackFramesList.CopyTo(m_stackFrames, 0); // now frameIndex is the first 'user' caller frame StackFrame locationFrame = st.GetFrame(frameIndex); if (locationFrame != null) { System.Reflection.MethodBase method = locationFrame.GetMethod(); if (method != null) { m_methodName = method.Name; if (method.DeclaringType != null) { m_className = method.DeclaringType.FullName; } } m_fileName = locationFrame.GetFileName(); m_lineNumber = locationFrame.GetFileLineNumber().ToString(System.Globalization.NumberFormatInfo.InvariantInfo); // Combine all location info m_fullInfo = m_className + '.' + m_methodName + '(' + m_fileName + ':' + m_lineNumber + ')'; } } } catch (System.Security.SecurityException) { // This security exception will occur if the caller does not have // some undefined set of SecurityPermission flags. //LogLog.Debug(declaringType, "Security exception while trying to get caller stack frame. Error Ignored. Location Information Not Available."); } } #endif } /// <summary> /// Constructor /// </summary> /// <param name="className">The fully qualified class name.</param> /// <param name="methodName">The method name.</param> /// <param name="fileName">The file name.</param> /// <param name="lineNumber">The line number of the method within the file.</param> /// <remarks> /// <para> /// Initializes a new instance of the <see cref="LocationInfo" /> /// class with the specified data. /// </para> /// </remarks> public LocationInfo(string className, string methodName, string fileName, string lineNumber) { m_className = className; m_fileName = fileName; m_lineNumber = lineNumber; m_methodName = methodName; m_fullInfo = m_className + '.' + m_methodName + '(' + m_fileName + ':' + m_lineNumber + ')'; } #endregion Public Instance Constructors #region Public Instance Properties /// <summary> /// Gets the fully qualified class name of the caller making the logging /// request. /// </summary> /// <value> /// The fully qualified class name of the caller making the logging /// request. /// </value> /// <remarks> /// <para> /// Gets the fully qualified class name of the caller making the logging /// request. /// </para> /// </remarks> public string ClassName { get { return m_className; } } /// <summary> /// Gets the file name of the caller. /// </summary> /// <value> /// The file name of the caller. /// </value> /// <remarks> /// <para> /// Gets the file name of the caller. /// </para> /// </remarks> public string FileName { get { return m_fileName; } } /// <summary> /// Gets the line number of the caller. /// </summary> /// <value> /// The line number of the caller. /// </value> /// <remarks> /// <para> /// Gets the line number of the caller. /// </para> /// </remarks> public string LineNumber { get { return m_lineNumber; } } /// <summary> /// Gets the method name of the caller. /// </summary> /// <value> /// The method name of the caller. /// </value> /// <remarks> /// <para> /// Gets the method name of the caller. /// </para> /// </remarks> public string MethodName { get { return m_methodName; } } /// <summary> /// Gets all available caller information /// </summary> /// <value> /// All available caller information, in the format /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c> /// </value> /// <remarks> /// <para> /// Gets all available caller information, in the format /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c> /// </para> /// </remarks> public string FullInfo { get { return m_fullInfo; } } #if !(NETCF || NETSTANDARD1_3) /// <summary> /// Gets the stack frames from the stack trace of the caller making the log request /// </summary> public StackFrameItem[] StackFrames { get { return m_stackFrames; } } #endif #endregion Public Instance Properties #region Private Instance Fields private readonly string m_className; private readonly string m_fileName; private readonly string m_lineNumber; private readonly string m_methodName; private readonly string m_fullInfo; #if !(NETCF || NETSTANDARD1_3) private readonly StackFrameItem[] m_stackFrames; #endif #endregion Private Instance Fields #region Private Static Fields /// <summary> /// The fully qualified type of the LocationInfo class. /// </summary> /// <remarks> /// Used by the internal logger to record the Type of the /// log message. /// </remarks> private readonly static Type declaringType = typeof(LocationInfo); /// <summary> /// When location information is not available the constant /// <c>NA</c> is returned. Current value of this string /// constant is <b>?</b>. /// </summary> private const string NA = "?"; #endregion Private Static Fields
为什么会有这么多英文注释呢,因为这是copy的log4net源码。。。但是LocationInfo中重载了构造函数
我需要先获取调用日志方法的StackTrace,然后传入构造方法,主要因为写入日志用到异步,如果在异步线程内用LocationInfo(Type callerStackBoundaryDeclaringType),会导致获取不到我们期望的那几个追踪信息,因为StackTrace是在它内部new的,这会导致获取的是异步线程的信息。所以我要在进入异步线程前将StackTrace获取到。
所以也就有了下面这个类(LogEventDataAsync),一个传入异步线程的数据对象,里面保存着一些基本的初始日志信息
- 创建LogEventDataAsync

/// <summary> /// 日志数据,传入异步执行方法的数据 /// 主要为提前获取CallerStackBoundaryDeclaringType和CallerStackTrace,避免Core(log4net源码)下追踪信息在异步线程内与期望不一致 /// </summary> internal class LogEventDataAsync { public string Message { get; set; } /// <summary> /// 错误级别 /// </summary> public string Level { get; set; } /// <summary> /// 日志来源 /// </summary> public string LogSource { get; set; } /// <summary> /// 调用日志方法实例类型 /// </summary> public Type CallerStackBoundaryDeclaringType { get; set; } /// <summary> /// StackTrace /// </summary> public StackTrace CallerStackTrace { get; set; } /// <summary> /// 不为空则发送邮件,多个接收人用英文逗号隔开 /// </summary> public string Emails { get; set; } }
到目前为止日志信息的准备工作就做完了,下面来赋值写入mongodb
先来创建两个帮助类吧,一个用于异步,一个是比较常见的请求类,都很简单
创建Common文件夹,在下面创建两个类
AsyncHelpers(NLog源码)

internal static class AsyncHelpers { internal static int GetManagedThreadId() { #if NETSTANDARD1_3 return System.Environment.CurrentManagedThreadId; #else return Thread.CurrentThread.ManagedThreadId; #endif } internal static void StartAsyncTask(Action<object> action, object state) { #if NET4_0 || NET4_5 || NETSTANDARD System.Threading.Tasks.Task.Factory.StartNew(action, state, CancellationToken.None, System.Threading.Tasks.TaskCreationOptions.None, System.Threading.Tasks.TaskScheduler.Default); #else ThreadPool.QueueUserWorkItem(new WaitCallback(action), state); #endif } }
RequestHelpers

internal class RequestHelpers { /// <summary> /// 组装普通文本请求参数。 /// </summary> /// <param name="parameters">Key-Value形式请求参数字典</param> /// <returns>URL编码后的请求数据</returns> public static String BuildQuery(IDictionary<String, String> parameters) { StringBuilder postData = new StringBuilder(); bool hasParam = false; IEnumerator<KeyValuePair<String, String>> dem = parameters.GetEnumerator(); while (dem.MoveNext()) { String name = dem.Current.Key; String value = dem.Current.Value; // 忽略参数名或参数值为空的参数 if (!String.IsNullOrEmpty(name) && !String.IsNullOrEmpty(value)) { if (hasParam) { postData.Append("&"); } postData.Append(name); postData.Append("="); postData.Append(HttpUtility.UrlEncode(value)); hasParam = true; } } return postData.ToString(); } /// <summary> /// 执行HTTP POST请求。 /// 对参数值执行UrlEncode /// </summary> /// <param name="url">请求地址</param> /// <param name="parameters">请求参数</param> /// <returns>HTTP响应</returns> public static String DoPost(String url, IDictionary<String, String> parameters) { HttpWebRequest req = GetWebRequest(url, "POST"); req.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; Byte[] postData = Encoding.UTF8.GetBytes(BuildQuery(parameters)); Stream reqStream = req.GetRequestStream(); reqStream.Write(postData, 0, postData.Length); reqStream.Close(); HttpWebResponse rsp = null; rsp = (HttpWebResponse)req.GetResponse(); Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet); return GetResponseAsString(rsp, encoding); } /// <summary> /// 执行HTTP POST请求。 /// 该方法在执行post时不对请求数据进行任何编码(UrlEncode) /// </summary> /// <param name="url">请求地址</param> /// <param name="data">请求数据</param> /// <returns>HTTP响应</returns> public static String DoPost(String url, string data) { HttpWebRequest req = GetWebRequest(url, "POST"); req.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; Byte[] postData = Encoding.UTF8.GetBytes(data); Stream reqStream = req.GetRequestStream(); reqStream.Write(postData, 0, postData.Length); reqStream.Close(); HttpWebResponse rsp = null; rsp = (HttpWebResponse)req.GetResponse(); Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet); return GetResponseAsString(rsp, encoding); } /// <summary> /// post数据 T messagepack序列化格式 减少传输数据大小 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url"></param> /// <param name="model"></param> public static void DoPost<T>(String url, T model) { var client = new HttpClient(); //MessagePack.Resolvers.CompositeResolver.RegisterAndSetAsDefault( // NativeDateTimeResolver.Instance, // ContractlessStandardResolver.Instance); var messagePackMediaTypeFormatter = new MessagePackMediaTypeFormatter(ContractlessStandardResolver.Instance); var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = new ObjectContent<T>( model,messagePackMediaTypeFormatter); request.Content.Headers.ContentType.MediaType = "application/x-msgpack"; //client.Timeout = new TimeSpan(0,0,5); client.SendAsync(request); } /// <summary> /// 执行HTTP POST请求。 /// 该方法在执行post时不对请求数据进行任何编码(UrlEncode) /// </summary> /// <param name="url">请求地址</param> /// <param name="data">请求数据</param> /// <returns>HTTP响应</returns> public static String DoPostJson(String url, string data) { HttpWebRequest req = GetWebRequest(url, "POST"); req.ContentType = "application/json;charset=UTF-8"; req.Accept = "application/json"; Byte[] postData = Encoding.UTF8.GetBytes(data); Stream reqStream = req.GetRequestStream(); reqStream.Write(postData, 0, postData.Length); reqStream.Close(); HttpWebResponse rsp = null; rsp = (HttpWebResponse)req.GetResponse(); Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet); return GetResponseAsString(rsp, encoding); } /// <summary> /// 执行HTTP GET请求。 /// </summary> /// <param name="url">请求地址</param> /// <param name="parameters">请求参数</param> /// <returns>HTTP响应</returns> public static String DoGet(String url, IDictionary<String, String> parameters) { if (parameters != null && parameters.Count > 0) { if (url.Contains("?")) { url = url + "&" + BuildQuery(parameters); } else { url = url + "?" + BuildQuery(parameters); } } HttpWebRequest req = GetWebRequest(url, "GET"); req.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; HttpWebResponse rsp = null; rsp = (HttpWebResponse)req.GetResponse(); Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet); return GetResponseAsString(rsp, encoding); } public static HttpWebRequest GetWebRequest(String url, String method) { HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = method; return req; } /// <summary> /// 把响应流转换为文本。 /// </summary> /// <param name="rsp">响应流对象</param> /// <param name="encoding">编码方式</param> /// <returns>响应文本</returns> public static String GetResponseAsString(HttpWebResponse rsp, Encoding encoding) { Stream stream = null; StreamReader reader = null; try { // 以字符流的方式读取HTTP响应 stream = rsp.GetResponseStream(); reader = new StreamReader(stream, encoding); return reader.ReadToEnd(); } finally { // 释放资源 if (reader != null) reader.Close(); if (stream != null) stream.Close(); if (rsp != null) rsp.Close(); } } public static string GetUrlData(string url, string encoding, out long logSize) { logSize = 0; string return_value = string.Empty; try { HttpWebRequest wq = WebRequest.Create(url) as HttpWebRequest; if (wq == null) { return return_value; } wq.Credentials = CredentialCache.DefaultCredentials; wq.CookieContainer = new CookieContainer(); wq.ContentType = "text/html"; wq.Method = "GET"; wq.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0"; wq.Host = new Uri(url).Host; wq.Timeout = 10000; try { HttpWebResponse rep = wq.GetResponse() as HttpWebResponse; logSize = rep.ContentLength; Stream responseStream = rep.GetResponseStream(); if (rep.ContentEncoding.ToLower().Contains("gzip")) { responseStream = new GZipStream(responseStream, CompressionMode.Decompress); } else if (rep.ContentEncoding.ToLower().Contains("deflate")) { responseStream = new DeflateStream(responseStream, CompressionMode.Decompress); } StreamReader reader = new StreamReader(responseStream, Encoding.GetEncoding(encoding)); return_value = reader.ReadToEnd(); responseStream.Close(); reader.Close(); rep.Close(); } catch (Exception) { return "nolog"; } } catch (WebException ex) { return_value = "error_error"; } return return_value; } }
在RequestHelpers中真正用到的是public static void DoPost<T>(String url, T model)方法,里面用到了MessagePack以及扩展的MediaType "application/x-msgpack",所以这里要引用两个nuget包
MessagePack和Sketch7.MessagePack.MediaTypeFormatter
- 创建LogWriter
LogWriter为调用Post方法的类,里面主要提供了异步调用的方法以及创建完整的日志信息功能

internal class LogWriter { /// <summary> /// 私有构造函数 /// </summary> private LogWriter() { } /// <summary> /// 获取LogWriter实例 /// </summary> /// <returns></returns> public static LogWriter GetLogWriter() { return new LogWriter(); } public void Writer(object logEventDataAsync) { var led = GetLoggingEventData((LogEventDataAsync)logEventDataAsync); var level = LogLevel.FromString(led.Level); string logapi = level.LogApi; RequestHelpers.DoPost<LogEventData>(logapi, led);//MessagePack进行数据压缩,减小传输数据 } /// <summary> /// 获取日志数据 /// </summary> /// <param name="logEventDataAsync"></param> /// <returns></returns> private LogEventData GetLoggingEventData(LogEventDataAsync logEventDataAsync) { LocationInfo locationInfo = new LocationInfo(logEventDataAsync.CallerStackBoundaryDeclaringType, logEventDataAsync.CallerStackTrace); LogEventData logData = new LogEventData { Message = logEventDataAsync.Message, Date = DateTime.Now, Level = logEventDataAsync.Level, LogSource = string.IsNullOrEmpty(logEventDataAsync.LogSource) ? locationInfo.ClassName : logEventDataAsync.LogSource, ClassName = locationInfo.ClassName, MethodName = locationInfo.MethodName, LineNumber = locationInfo.LineNumber, FileName = locationInfo.FileName, IP = "NA", Emails = logEventDataAsync.Emails, FullInfo=locationInfo.FullInfo }; return logData; } }
GetLoggingEventData方法中就包含了LocationInfo的实例化,之所以放这里是因为这里异步执行,而获取追踪信息需要各种反射比较耗时,不然也就没有LogEventDataAsync这个类了,获取IP的方法没实现。。。
- 创建Logger类
这个类是真正暴露给开发者使用的类,就像开源项目log.debug这样的方式

public class Logger { private readonly static Type declaringType = typeof(Logger); /// <summary> /// 日志写入实例 /// </summary> private LogWriter _logWriter = null; /// <summary> /// 日志来源 /// 默认为调用方法所在类 /// </summary> private string _logSource = string.Empty; /// <summary> /// 私有构造函数 /// </summary> private Logger() { _logWriter = LogWriter.GetLogWriter(); } /// <summary> /// 私有构造函数 /// </summary> /// <param name="logSource">日志来源</param> private Logger(string logSource):this() { _logSource = logSource; } /// <summary> /// 获取Logger实例 /// 默认日志来源为调用方法所在类:namespace.classname /// </summary> /// <param name="logSource">日志来源</param> /// <returns></returns> public static Logger GetLogger(string logSource=null) { return new Logger(logSource); } /// <summary> /// Trace /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Trace(string message, string emails = null) { WriterToTargets(message, LogLevel.Trace, emails); } /// <summary> /// Trace /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Trace(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Trace, emails); } /// <summary> /// Debug /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Debug(string message, string emails = null) { WriterToTargets(message, LogLevel.Debug, emails); } /// <summary> /// Debug /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Debug(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Debug, emails); } /// <summary> /// Info /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Info(string message, string emails = null) { WriterToTargets(message, LogLevel.Info, emails); } /// <summary> /// Info /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Info(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Info, emails); } /// <summary> /// Warn /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Warn(string message, string emails = null) { WriterToTargets(message, LogLevel.Warn, emails); } /// <summary> /// Warn /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Warn(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Warn, emails); } /// <summary> /// Error /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Error(string message, string emails = null) { WriterToTargets(message, LogLevel.Error, emails); } /// <summary> /// Error /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Error(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Error, emails); } /// <summary> /// Fatal /// </summary> /// <param name="message">日志内容</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Fatal(string message, string emails = null) { WriterToTargets(message, LogLevel.Fatal, emails); } /// <summary> /// Fatal /// </summary> /// <param name="ex">异常信息</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> public void Fatal(Exception ex, string emails = null) { WriterToTargets(ex.ToString(), LogLevel.Fatal, emails); } /// <summary> /// 写日志 /// </summary> /// <param name="message">日志信息</param> /// <param name="level">级别</param> /// <param name="emails">是否发送邮件,不为空则发送邮件,多个接收人用英文分号;隔开</param> private void WriterToTargets(string message, LogLevel level,string emails=null) { try { LogEventDataAsync leda = new LogEventDataAsync { LogSource = _logSource, Level = level.Name, CallerStackBoundaryDeclaringType = GetType(),//获取当前实例 CallerStackTrace = new StackTrace(true),//获取当前StackTrace Message = message, Emails = emails }; AsyncHelpers.StartAsyncTask(_logWriter.Writer, leda);//执行异步写日志 } catch { } }
代码非常简单,重载了最常用的自定义信息和exception信息,以及邮件联系人。日志来源主要为了分类日志,像不同的服务、站点等等,可以方便入库后查询。到这已经可以编译使用了,但是为了在.net core中可以依赖注入这个Logger,最后添加一个扩展方法
- 创建Extensions文件夹及LoggerServiceExtension类

/// <summary> /// 日志服务注入扩展类 /// </summary> public static class LoggerServiceExtension { /// <summary> /// 注入日志服务 /// </summary> /// <param name="service">IServiceCollection</param> /// <param name="logSource">日志来源,默认日志来源为调用方法所在类:namespace.classname</param> /// <returns></returns> public static IServiceCollection AddLoggerService(this IServiceCollection service, string logSource=null) { return service.AddTransient(factory => Logger.GetLogger(logSource)); } }
三、使用(引入类库)
一般项目使用可以这样,比如控制台

class Program { static Logger logger = LogApiHandler.Logger.GetLogger("logSource"); static void Main(string[] args) { logger.Debug("text"); Console.ReadLine(); } }
.net core的话比如web项目可以这样用依赖注入
在Startup中,ConfigureServices添加一行代码
services.AddLoggerService("TestApi");
然后在其他类中就可以这么用了,当然也可以用new的方式使用
public class ValuesController : ControllerBase { private Logger _logger; public ValuesController(Logger logger) { _logger = logger; } // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> Get() { _logger.Error("测试依赖注入logger", "[email protected]"); return new string[] { "value1", "value2" }; } }
大体使用方式就是这样了。
最后得需要自己写一个页面来调用web api的api/log/getlist接口查询显示日志
mongodb客户端用的Robo 3T,安装完记得修改Options--Display Dates in--Local Timezone,不然默认utc,存入时的时间少8小时。mongodb存入数据就是下图
更新:
1.LogWriter类Writer方法加try catch,因为异步线程内异常不会被主线程捕获
2.添加请求认证
- LogApiHandler类库修改
LogWriter类新增两个字段:
private readonly string UserName = "UserName"; private readonly string Password = "Password";
RequestHelpers类重载一个请求方法:

/// <summary> /// Authorization 认证 /// post数据 T messagepack序列化格式 减少传输数据大小 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url"></param> /// <param name="model"></param> /// <param name="username">账户</param> /// <param name="password">密码</param> public static void DoPost<T>(string url, T model,string username,string password) { var client = new HttpClient(); var messagePackMediaTypeFormatter = new MessagePackMediaTypeFormatter(ContractlessStandardResolver.Instance); var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = new ObjectContent<T>( model, messagePackMediaTypeFormatter); request.Content.Headers.ContentType.MediaType = "application/x-msgpack"; string encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password)); request.Headers.Add("Authorization", "Basic " + encoded); client.SendAsync(request); }
修改LogWriter类Writer方法,将请求改为上面重载的新方法:
RequestHelpers.DoPost<LogEventData>(logapi, led, UserName, Password);
- LogWebApi站点修改
appsettings.json新增配置:
"AppSettings": { "RequestAuth": { "UserName": "UserName", "Password": "Password" } }
Model--AppSettings下修改AppSettings类

public class AppSettings { public SendMailInfo SendMailInfo { get; set; } public RequestAuth RequestAuth { get; set; } } public class SendMailInfo { public string SMTPServerName { get; set; } public string SendEmailAdress { get; set; } public string SendEmailPwd { get; set; } public string SiteName { get; set; } public string SendEmailPort { get; set; } } public class RequestAuth { public string UserName { get; set; } public string Password { get; set; } }
Extensions文件夹下新建RequestAuthorizeMiddleware类:

public class RequestAuthorizeMiddleware { private readonly RequestDelegate _next; private readonly IOptions<AppSettings> _appsettings; public RequestAuthorizeMiddleware(RequestDelegate next, IOptions<AppSettings> appsettings) { _next = next; _appsettings = appsettings; } public async Task InvokeAsync(HttpContext context) { var authHeader = context.Request.Headers["Authorization"].ToString(); if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) { var token = authHeader.Substring("Basic ".Length).Trim(); var credentialstring = Encoding.GetEncoding("ISO-8859-1").GetString(Convert.FromBase64String(token)); var credentials = credentialstring.Split(':'); if (credentials[0] == _appsettings.Value.RequestAuth.UserName && credentials[1] == _appsettings.Value.RequestAuth.Password) { await _next(context); } else { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } } else { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } } }
Startup.cs Configure添加代码:
app.UseMiddleware(typeof(RequestAuthorizeMiddleware));
Useful links
Using MongoDB .NET Driver with .NET Core WebAPI
Create a web API with ASP.NET Core and MongoDB
https://qappdesign.com/code/using-mongodb-with-net-core-webapi/