1. 程式人生 > >基於.net core webapi的日誌系統——介面實現

基於.net core webapi的日誌系統——介面實現

開發環境vs2017,資料寫入到mongodb。思路就是1.提供介面寫入日誌,2.基於介面封裝類庫。

1.為什麼要寫它

很多開源專案像nlog、log4net、elk、exceptionless等都挺好的。就是除了引入所需類庫,還要在專案中新增配置,不喜歡。elk在分散式海量資料收集和檢索方面可能更能發揮它的優勢,單純記日誌也可以,exceptionless就是基於elk的。就想著寫一個簡單易用的、可以發郵件報警的,直接引入類庫就能用的一個記日誌工具,所有的配置資訊和入庫都交給web api。這是當時問的問題,https://q.cnblogs.com/q/109489/。乾脆就實現了先

接下里的程式碼可能有很多可以優化的地方,如果有些地方覺得不妥或者可以用更好的方式實現或組織程式碼,請告訴說,我改。另外實現完的介面沒有加訪問限制,先預設內網使用,當然有熱心網友給出實現的話就更好了,像ip限制或者簽名等等,接下來是web api的實現

2.實現Web Api

2.1 新建.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"
    }
  }
}

2.2 實現寫入mongodb

  • 實現依賴注入獲取配置檔案資訊

建立目錄結構如下圖

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; } }
View Code

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; }
    }
View Code

 接下來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"));//其他配置資訊            

        }
View Code

在專案中將通過IOptions 介面來獲取配置資訊,後面看程式碼吧

IOptions<AppSettings>
IOptions<DBSettings>
View Code

配置檔案資訊獲取算是準備完了

  • 建立日誌資訊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);
        }
    }
View Code
  • 定義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);
            }
        }
    }
View Code
  • 新增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);
    }
View Code

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
    }
View Code

 為了通過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;
        }
    }
View Code

控制器裡整個邏輯很簡單,除了向外提供不同日誌級別的寫入介面,也實現了日誌查詢介面給日誌檢視站點用,基本上夠用了。到這編譯的話會報錯,有一些類還沒加上,像傳送郵件的類,稍後加上。在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; }
    }
View Code

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; }
        }
    }
View Code

 建立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;
            }
        }
    }
View Code

到這介面基本上就可以用了。

但是再加三個東西

  • 擴充套件

 新增全域性異常捕獲服務

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.");
                    }
                });
            });
        }
    }
View Code

修改Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IOptions<DBSettings> settings)
        {            
            app.ConfigureExceptionHandler(settings);
        }
View Code

messagepack可以讓我們在post資料的時候序列化資料,“壓縮”資料傳輸大小,這個會結合針對介面封裝的類庫配合使用。

引用nuget: 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
            });
View Code

到這整個web api站點算是寫完了,編譯不出錯就ok了。思路上應該很簡單清晰了吧

接下來會寫針對這個web api的類庫實現,其實一開始是先寫的類庫,然後針對類庫寫的這個介面。。。