基於.net core webapi和mongodb的日誌系統
開發環境vs2017,資料寫入到mongodb。思路就是1.提供介面寫入日誌,2.基於介面封裝類庫。3.引入類庫使用
為什麼要寫它
很多開源專案像nlog、log4net、elk、exceptionless等都挺好的。就是除了引入所需類庫,還要在專案中新增配置,不喜歡。elk在分散式海量資料收集和檢索方面可能更能發揮它的優勢,單純記日誌也可以,exceptionless就是基於elk的。就想著寫一個簡單易用的、可以發郵件報警的,直接引入類庫就能用的一個記日誌工具,所有的配置資訊和入庫都交給web api。這是當時問的問題,https://q.cnblogs.com/q/109489/。乾脆就實現了先
接下里的程式碼可能有很多可以優化的地方,如果有些地方覺得不妥或者可以用更好的方式實現或組織程式碼,請告訴說,我改。另外實現完的介面沒有加訪問限制,先預設內網使用,當然有熱心網友給出實現的話就更好了,像ip限制或者簽名等等
一、實現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 { getView Code; 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; } }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>
配置檔案資訊獲取算是準備完了
- 建立日誌資訊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();
擴充套件了"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了。
二、實現類庫
類庫整體目錄結構如下
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>