开发环境vs2017,.NET Core2.1, 数据写入到mongodb。思路就是1.提供接口写入日志,2.基于接口封装类库。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();
扩展了media type,用以支持"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)){}#endregionprivate 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 Fieldsprivate 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 valuesm_lineNumber = NA;m_fileName = NA;m_method = new MethodItem();m_className = NA;try{// get frame valuesm_lineNumber = frame.GetFileLineNumber().ToString(System.Globalization.NumberFormatInfo.InvariantInfo);m_fileName = frame.GetFileName();// get method valuesMethodBase 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 infom_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 Fieldsprivate 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 fieldsm_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/1797if (callerStackBoundaryDeclaringType != null){try{StackTrace st = new StackTrace(true);int frameIndex = 0;// skip frames not from fqnOfCallingClasswhile (frameIndex < st.FrameCount){StackFrame frame = st.GetFrame(frameIndex);if (frame != null && frame.GetMethod().DeclaringType == callerStackBoundaryDeclaringType){break;}frameIndex++;}// skip frames from fqnOfCallingClasswhile (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 aboveint 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 frameStackFrame 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 infom_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 fieldsm_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/1797if (callerStackBoundaryDeclaringType != null){try{//StackTrace st = new StackTrace(true);int frameIndex = 0;// skip frames not from fqnOfCallingClasswhile (frameIndex < st.FrameCount){StackFrame frame = st.GetFrame(frameIndex);if (frame != null && frame.GetMethod().DeclaringType == callerStackBoundaryDeclaringType){break;}frameIndex++;}// skip frames from fqnOfCallingClasswhile (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 aboveint 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 frameStackFrame 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 infom_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 Fieldsprivate 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_3return System.Environment.CurrentManagedThreadId; #elsereturn Thread.CurrentThread.ManagedThreadId; #endif}internal static void StartAsyncTask(Action<object> action, object state){ #if NET4_0 || NET4_5 || NETSTANDARDSystem.Threading.Tasks.Task.Factory.StartNew(action, state, CancellationToken.None, System.Threading.Tasks.TaskCreationOptions.None, System.Threading.Tasks.TaskScheduler.Default); #elseThreadPool.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),//获取当前StackTraceMessage = 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", "ddd@ddd.com");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));