ElasticSearch ile Farklı Indexler ile .Net Core’da Loglama
Selamlar,
Bu makalede, var olan .Net Core katmanlı bir projede farklı bussineslar altında Elasticsearch ile loglama konusuna değineceğiz. Önceden ElasticSearch hakkında hiçbir fikriniz yok ise önce şu makaleye bakmanızı tavsiye ederim. Hatta ElasticSearch üzerine yazdığım diğer makalelere, buradan göz atabilirsiniz.
Öncelikle neden ElasticSearch’e ihtiyaç duyduk?
Eğer, çok fazla data içeri alınacak, bunlar içinde Full-text search yani metin aramısı yapılacak ve bu arama sonuçları, saniyenin altında bir zamanda alınacak ise, farklı teknolojilere ihtiyaç duyulur. Bunlardan bazıları Solr veya Elasticsearch’dür. Loglama, bir çok amaç için proje genelinde kullanılabilir. En kısa tabiri ile önemli projelerin, olmaz ise olmazıdır. Hem performanslı olmalı hem de arandığında kolayca bulunabilmelidir.
“Anı yazmak, ölümün elinden bir şeyi kurtarmaktır.” ―Andre Gide
Aslında bu yazının amacı, ElasticSearch’ün ne olduğundan ziyade, nerelerde ve ne amaçlı kullanılabileceğini göstermektir.
Bu makalede 4 ihtiyaç için Elasticsearch ile Loglama yapılmıştır.
- Login olan kullanıcı bilgileri.
- İşaretlenmiş Actionları, çağıran kullanıcı logları.
- Projenin tamamında oluşan hatalar.
- Tabloların üzerinde yapılan Audit kayıtlar.
Her şeyin muntazam ve düzenli bir şekilde saklanması, iyi bir loglamanın ilk anahtarıdır.
Hazırlıklar:
1-) Öncelikle Elasticsearch’ün implemente edileceği proje, .Net Core 3.1’dir. Solution’ın Core katmanına, NuGet Package Manager ile aşağıda görüldüğü gibi NEST v7.9.0 kütüphanesi indirilir.
2-) Makinanızda ElasticSearch kurulu değil ise https://www.elastic.co/downloads/elasticsearch linkinden indirilir. Bu makale itibari ile 7.9.2 versiyonu indirilmiştir.
3-) Elasticsearch monitör amaçlı, Kibana kullanılacağı için https://www.elastic.co/downloads/kibana linkinden indirilir.
4-) İstenir ise Elasticsearch monitor amaçlı, Chrome Extensions : ElasticSearch Head de kullanılabilir.
Gelin önce var olan .Net Core bir projede, Core katmanında ElasticSearch adında bir klasör açalım ve ilk ElasticSearchClient Provider’ı oluşturalım.
Elastic Client Yaratma:
Elasticsearch’de, her işlem için bir Client’a ihtiyaç duyulur. Aslında Client, elastic sunucuya bağlanmak için kullanılan bir nesnedir. Her client, bir Index’e ihtiyaç duyar. Aslında, her Index ayrı bir Database‘dir. Type da, Table‘dır. İçerisine atılan her bir kayıt yani row da, Document‘dir. Bu projede Client yaratılırken, hem Index tanımlanmış, hem de Index tanımlanmamış versiyonları ayrı ayrı yazılmıştır.
Not: Elasticsearch’de tek fark, farklı document tipleri için farklı Indexlerin yaratılması gerektiğidir.
- “public ElasticClientProvider(Microsoft.Extensions.Options.IOptions<ElasticConnectionSettings> elasticConfig)“: Dependency Injection ile ElasticSearch’ün Url parametresi config dosya üzerinden bir class yardımı ile alınır. Bu class, “ElasticConnectionSettings“‘dir. Yazının devamında nasıl “appsettings.json” ile bir class’ı eşleştirdiğimizi göreceğiz.
- “CreateClient()“: Nest kütüphanesi yardımı ile oluşturulan ElasticClient, projenin başından sonuna kadar bir kere üretilecek ve devamında hep aynı client kullanılacaktır. Dikkat edilirse yaratılma anında, tanımlı herhangi bir index yoktur.
- “var connectionSettings = new ConnectionSettings(new Uri(ElasticSearchHost))“: Connection’ı configden aldık.
- “DisablePing()”: İlk request’den sonra, belirlenen standart sürenin üstünde bir sürede hata fırlatılması sağlanır.
- “DisableDirectStreaming(true”): Bunu elasticsearch’de hata alındığı zaman daha detaylı hatayı alabilmek adına eklenmiştir. Memoryde performans kaybına neden olabilir. Sadece ihtiyaç anında kullanılmalıdır.
- “SniffOnStartup(false)”: İlk connection’ın çekilme anında, havuzun kontrol edilmesini engeller. Amaç performanstır.
- “SniffOnConnectionFault(false)”: Bağlantı havuzu yeniden beslemeyi destekliyorsa, bir arama başarısız olduğunda ilgili connection havuzundan yeniden denetlenmesini engeller. Amaç yine performanstır.
- “var connectionSettings = new ConnectionSettings(new Uri(ElasticSearchHost))“: Connection’ı configden aldık.
- “CreateClientWithIndex(string defaultIndex)“: Bu sefer client oluşturulurken, default olarak Index’in de tanımlanması sağlanmıştır.
Core/ElasticClientProvider.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using Dashboard.Core.Configuration; using Nest; using System; namespace Dashboard.Core.ElasticSearch { public class ElasticClientProvider { public ElasticClientProvider(Microsoft.Extensions.Options.IOptions<ElasticConnectionSettings> elasticConfig) { ElasticSearchHost = elasticConfig.Value.ElasticSearchHost; ElasticClient = CreateClient(); } private ElasticClient CreateClient() { var connectionSettings = new ConnectionSettings(new Uri(ElasticSearchHost)) .DisablePing() .DisableDirectStreaming(true) .SniffOnStartup(false) .SniffOnConnectionFault(false); return new ElasticClient(connectionSettings); } public ElasticClient CreateClientWithIndex(string defaultIndex) { var connectionSettings = new ConnectionSettings(new Uri(ElasticSearchHost)) .DisablePing() .SniffOnStartup(false) .SniffOnConnectionFault(false) .DefaultIndex(defaultIndex); return new ElasticClient(connectionSettings); } public ElasticClient ElasticClient { get; } public string ElasticSearchHost { get; set; } } } |
Core/IElasticSearchService.cs: ElasticSearch ile yapılacak tüm operasyonlar burada tanımlanır.
- “CheckExistsAndInsertLog“: Yok ise ilgili Index’in oluşturulması ve istenen document’ın yani row’un atılması sağlanır.
- “SearchLoginLog“: Login olan clientların ve çağırdıkları işaretli tüm methodların logu, bu method ile filtrelenerek listelenebilir.
- “SearchErrorLog“: Tüm hata logları, gene filitrelenerek bu method ile çekilebilir.
- “SearchAuditLog“: Üzerinde değişiklik yapılmış tabloların önceki halleri, yapan kişi, ilgili tablo ismi ve zaman aralığı belirtilerek yine bu method ile filitrelenebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using Dashboard.Core.Models.Management; using Dashboard.Core.Models.Users; using System; using System.Collections.Generic; namespace Dashboard.Core.ElasticSearch { public interface IElasticSearchService<T> where T : class { public void CheckExistsAndInsertLog(T logMode, string indexName); public IReadOnlyCollection<LoginLogModel> SearchLoginLog(string userID, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", int? page = 0, int? rowCount = 10, string? indexName = "login_log"); public IReadOnlyCollection<ErrorLogModel> SearchErrorLog(int? userID, int? errorCode, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", string method = "", string services = "", int page = 0, int rowCount = 10, string indexName = "error_log"); public IReadOnlyCollection<AuditLogModel> SearchAuditLog(int? userID, DateTime? BeginDate, DateTime? EndDate, string className = "", string operation = "Update", int page = 0, int rowCount = 10, string indexName = "audit_log"); } } |
0-) CheckExistsAndInsertLog():
ElasticSearch’de tanımlanan index tipi yok ise yaratılan, var ise geçerli index’e ilgili tipdeki document’ın kaydedildiği methoddur.
ElasticSearchService.cs(1): ElasticSearch servisinin sadece “CheckExistsAndInsertLog()” methodu burada tanımlanmıştır. Makalenin devamında diğer özellikleri de tanımlanacaktır.
- “public class ElasticSearchService<T> : IElasticSearchService<T> where T : class“: ElasticSearchServis’de alıcağı document type’ı, generic olarak oluşturulmuştur. Böylece yeni bir döküman tipi için de, aynı servis kullanılabilecektir.
- “public ElasticSearchService(ElasticClientProvider provider)“: Elastic Client almak için, yukarıda oluşturulan sınıfı dependency injection ile ElasticSearchServisine dahil edilir.
- “_client = _provider.ElasticClient“: İlgili client, provider’ın ElasticClient propertysinden alınır.
- Not: ElasticClientProvider, proje ilk ayağa kaldırılırken sadece bir kere üretilir.(AddSingleton<>())
- “CheckExistsAndInsertLog(T logModel, string indexName)“: T tipinde tanımlı generic document tipidir. Yok ise, index’i yaratılıp kaydedilir.
- “indexSettings.NumberOfReplicas = 1; indexSettings.NumberOfShards = 3” : 3 sharding denilen, 3 makinada performans amaçlı çalışan ve herbir makinanın 1 yedeği olacak şekilde(Replica) güvenlik amaçlı toplam 6 makina üzerinde, Elasticsearch yapılandırılır. 6 makina 2şerli gruplar halinde toplam 3 grupa ayrılır. Herbir gruba Node denir. Herbir makina yedeğinin, farklı bir Nodeda olması elzemdir. “Bütün yumurtaları aynı sepete koymamak gerekir. :)“
- “.Aliases(a => a.Alias(indexName)))”: Elasticsearch’e aliases vermeniz çok önemlidir. İlerde index üzerinde değişiklik yapılırken, önceden kaydedilen dökümanların kaybedilmeden yenisinin kolaylıkla yaratılabilmesini sağlar.
- “IndexResponse responseIndex= _client.Index<T>(logModel, idx => idx.Index(indexName))” : Generic tipdeki döküman yani yeni kayıt, yoksa yaratılan ya da var olan index’e atılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using Dashboard.Core.Models.Management; using Dashboard.Core.Models.Users; using Nest; using ServiceStack.Web; using System; using System.Collections.Generic; namespace Dashboard.Core.ElasticSearch { public class ElasticSearchService<T> : IElasticSearchService<T> where T : class { ElasticClientProvider _provider; ElasticClient _client; public ElasticSearchService(ElasticClientProvider provider) { _provider = provider; _client = _provider.ElasticClient; } public void CheckExistsAndInsertLog(T logModel, string indexName) { if (!_client.Indices.Exists(indexName).Exists) { var newIndexName = indexName + System.DateTime.Now.Ticks; var indexSettings = new IndexSettings(); indexSettings.NumberOfReplicas = 1; indexSettings.NumberOfShards = 3; var response = _client.Indices.Create(newIndexName, index => index.Map<T>(m => m.AutoMap() ) .InitializeUsing(new IndexState() { Settings = indexSettings }) .Aliases(a => a.Alias(indexName))); } IndexResponse responseIndex= _client.Index<T>(logModel, idx => idx.Index(indexName)); int a = 0; } } } |
WebApi/Startup.cs: Aşağıda görüldüğü gibi Elasticsearch’e ait tüm tanımlamalar, .Net Core projede Startup sınıfına ait “ConfigureServices()” methodu altında yapılmaktadır.
-
- “services.Configure<ElasticConnectionSettings>(Configuration.GetSection(“ElasticConnectionSettings”))” : “appsettings.json” üzerinde “ElasticConnectionSettings” segmenti içinde tanımlı tüm alanların, “ElasticConnectionSettings” sınıfı ile maplenmesi sağlamıştır. Kısaca config’de tanımlı ElasticSearchHost field’ına erişmek istendiğinde, ElasticConnectionSettings sınıfının ilgili property’sinin yazılması yeterli olacaktır.
- “services.AddTransient(typeof(IElasticSearchService<>), typeof(ElasticSearchService<>))” : ElasticSearch servisi, her nesne çağrımında, yeni oluşturulurlar. En hızlısı ve thread safety açısından en güvenlisidir.
- “services.AddSingleton<ElasticClientProvider>()“: ElasticClient, yukarıda da bahsedildiği gibi proje ayağa kaldırılırken, sadece bir kere üretilir. Ve tüm clientlar için, aynı provider nesnesi kullanılır.
1 2 3 4 5 6 7 |
public void ConfigureServices(IServiceCollection services) { ... services.Configure<ElasticConnectionSettings>(Configuration.GetSection("ElasticConnectionSettings")); services.AddTransient(typeof(IElasticSearchService<>), typeof(ElasticSearchService<>)); services.AddSingleton<ElasticClientProvider>(); } |
WebApi/appsettings.json: Json config dosya içinde Elasticsearch tanımlamaları, aşağıdaki gibidir.
1 2 3 4 5 6 7 8 |
. . "ElasticConnectionSettings": { "ElasticSearchHost": "http://localhost:9200", "ElasticLoginIndex": "login_log", "ElasticErrorIndex": "error_log", "ElasticAuditIndex": "audit_log" }, |
Core/Configuration/ElasticConnectionSettings: Config dosyasına karşılık gelen class, aşağıdaki gibidir. İlgili config değerine, alttaki class’ın propertyleri aracılı ile ulaşılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Configuration { public class ElasticConnectionSettings { public string ElasticSearchHost { get; set; } public string ElasticLoginIndex { get; set; } public string ElasticErrorIndex { get; set; } public string ElasticAuditIndex { get; set; } } } |
“Dünyada kesin olan tek şey geçmiştir; fakat üzerinde çalışmak zorunda olduğumuz her şey gelecektir.” ―Auguste Comte
1-)Login Index:
Giriş yapan kişileri loglama amaçlı, ilk önce bir LoginLogFilter adında bir ActionFilter yaratılır. İki methodu vardır. “OnActionExecuted() ve OnActionExecuting()”. Login controller’ın tepesine konur. Böylece her login işleminde, ilgili LoginLogFilter sınıfına girilir.
- “LoginLogFilter(IElasticSearchService<LoginLogModel> elasticSearchService, IOptions<ElasticConnectionSettings> elasticConfig)” Yukarıda yazılan ElasticSearchServices ve elasticConfig sınıfları, dependency injection ile bu sınıfa dahil edilir.
- OnActionExecuted(): Login işlemi tamamlandıktan sonra çağrılan methoddur.
-
- “string action = (string)context.RouteData.Values[“action”];
string controller = (string)context.RouteData.Values[“controller”]” : Login olunulan Controller ve Action alınır. Burada amaç, projenin diğer yerlerinde de, kullacınıların giriş yaptıkları yerlerin kolaylıkla kaydedilmesidir. - “var result = context.Result” : Login işlemi sonrası dönen sonuç, result’a atanır. Amaç, kaydetme amaçlı login olan client’ın, “userID“‘sini almaktır.
- “userID = ((LoginResultModel)((ObjectResult)result).Value).UserId.ToString()” : UserId alınır.
- “LoginLogModel logModel = new LoginLogModel();
logModel.Action = action;
logModel.Controller = controller;
logModel.PostDate = DateTime.Now;
logModel.UserID = userID;” : LoginLogModel, ElasticSearch’e kaydedilecek document tipidir. İlgili alanlar doldurulur. - “_elasticSearchService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticLoginIndex)“: Config dosyada tanımlanan “login_log” adlı index, ElasticSearch’de yok ise yaratılır ve tanımlanan logModel document bu indexe atılır.
- “string action = (string)context.RouteData.Values[“action”];
-
- “OnActionExecuting()“: Action’a girmeden önce çalışan methoddur. Eğer client’ın yetkisi yok ise, hata fırlatılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
using Dashboard.Core; using Dashboard.Core.Caching; using Dashboard.Core.CoreContext; using Dashboard.Core.CustomException; using Dashboard.Core.Extensions; using Dashboard.Core.Models.Users; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Dashboard.Services.Users; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dashboard.Core.ElasticSearch; using Dashboard.Core.Configuration; namespace Dashboard.WebAPI.Infrastructure { public class LoginLogFilter : IActionFilter { private readonly IElasticSearchService<LoginLogModel> _elasticSearchService; Microsoft.Extensions.Options.IOptions<ElasticConnectionSettings> _elasticConfig; public LoginLogFilter(IElasticSearchService<LoginLogModel> elasticSearchService, Microsoft.Extensions.Options.IOptions<ElasticConnectionSettings> elasticConfig) { _elasticSearchService = elasticSearchService; _elasticConfig = elasticConfig; } public void OnActionExecuted(ActionExecutedContext context) { string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; var result = context.Result; string userID = String.Empty; if (result.GetType() == typeof(UnauthorizedResult)) { return; } userID = ((LoginResultModel)((ObjectResult)result).Value).UserId.ToString(); //Insert ElasticSearch LoginLogModel logModel = new LoginLogModel(); logModel.Action = action; logModel.Controller = controller; logModel.PostDate = DateTime.Now; logModel.UserID = userID; _elasticSearchService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticLoginIndex); //--------------------- return; } public void OnActionExecuting(ActionExecutingContext context) { try{ } catch (InvalidTokenException ex) { //Forbidden 430 Result. Yetkiniz Yoktur.. context.Result = new ObjectResult(context.ModelState) { Value = "Geçerli Bir Token Girilmemiştir", StatusCode = 430 }; return; } } } } |
Core/Models/Users/LoginLogModel : Login olan kişinin log kaydı, ElasticSearch’de aşağıdaki gibi indexlenir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Models.Users { public class LoginLogModel { public string UserID { get; set; } public DateTime PostDate { get; set; } public string Controller { get; set; } public string Action { get; set; } } } |
2-)User Log Index:
Bu loglama için, ElasticSearch üzerinde yeni bir index açılmasına gerek yoktur. Var olan login_log index, bu loglama içinde yeterli olacaktır. Amaç [LogAttribute] ile işaretli methodlara giren userların, ElasticSearch ile loglanmasıdır. Yani kim, ne zaman bu methodu çağırdı bilgisi tutulacaktır. Bu [LogAttribute] işaretlemesi ile, loglanması istenen Action kolaylıkla belirlenebilecektir.
WebApi/Infrastructure/LogAttribute: Öncelikle, loglanacak methodların işaretlenmesi için alttaki gibi bir ActionFilter yaratılır.
1 2 3 4 5 6 7 8 9 10 11 12 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Dashboard.WebAPI.Infrastructure { [AttributeUsage(AttributeTargets.Method)] public class LogAttribute : Attribute { } } |
WebApi/UserController: “GetUserById()” methodu [LogAttribute] ile işaretlenerek, bu methodu çağıran userların loglanması sağlanmıştır.
1 2 3 4 5 6 7 8 |
[LogAttribute] public ServiceResponse<UserModel> GetUserById(int userId) { var response = new ServiceResponse<UserModel>(HttpContext); response.Entity = _userService.GetById(userId).Entity; response.IsSuccessful = true; return response; } |
User Controller’ın tepesine ayrıca “LoginFilter” attribute’ü konulmuştur. Böylece, bu controller altında girilen tüm actionlar, önce bu filter’a girecektir. Ve içlerinden [LogAttribute] ile işaretli olan Actionlar, ElasticSearch’e loglanacakdır.
1 2 3 4 |
[ServiceFilter(typeof(LoginFilter))] [ApiController] [Microsoft.AspNetCore.Mvc.Route("[controller]")] public class UserController : ControllerBase{...} |
WebApi/Infrastructure/LoginFilter.cs:
OnActionExecuted() methoduna, ilgili method çalıştırılmış ve hata alınmadan tamamlanmış ise girilir. “HasLogAttribute()” methodu ile ilgili Action’ın LogAttribute ile işaretlenip işaretlenmediğine bakılır. İşaretli ise,
Elasticsearch’ün “logModel” index’ine, ilgili log document’ı atılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public void OnActionExecuted(ActionExecutedContext context) { if (HasLogAttribute(context)) { //Alınan Model Kaydedilecek string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; int.TryParse(context.HttpContext.Request.Headers["UserId"].FirstOrDefault(), out var userId); if (userId != 0) { //Elastic Log----- LoginLogModel logModel = new LoginLogModel(); logModel.Action = action; logModel.Controller = controller; logModel.PostDate = DateTime.Now; logModel.UserID = userId.ToString(); _elasticSearchService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticLoginIndex); return; } } } public bool HasLogAttribute(FilterContext context) { return ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.CustomAttributes.Any(filterDescriptors => filterDescriptors.AttributeType == typeof(LogAttribute)); } |
Kibana GetUserById Log: Aşağıda, belli bir tarih ve saat aralığına göre filitreleme örneği gösterilmektedir.
3-) Error Log Index:
Hata alınması durumunda, eğer istenir ise ErrorLogModelinde görüldüğü gibi, ElasticSearch de loglanabilir.
Core/Models/Management/ErrorLogModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Models.Management { public class ErrorLogModel { public int UserID { get; set; } public string Message { get; set; } public string Controller { get; set; } public string Action { get; set; } public string Method { get; set; } public string Service { get; set; } public DateTime PostDate { get; set; } public int ErrorCode { get; set; } } } |
Genellikle hatalarımızın hesabını tutmak, başarılarımızla övünmekten daha iyidir. ―Thomas Carlyle
1-) Service/Users/GetUserCompaniesById: İlgili servisde, test amaçlı özellikle hata fırlatılmıştır. İlgili ErrorLogModel’e methodun ismi, sınıfı, hata metni, userID, hata kodu ve tarihi bilgileri doldurularak ElasticSearch’de “error_log” index’ine atılacaktır.
- “System.ComponentModel.Win32Exception“: İlgili hatanın ID’si alınarak, ileride gruplama amaçlı kullanılmak üzere indexlenir.
- “_elasticErrorLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticErrorIndex)“: Yukarıda generic olarak yazılan “ElasticSearchService”‘e type olarak “ErrorLogModel” verilmiştir. “error_log” index, önceden yok ise yaratılıp, tanımlanan logModel yani document, ElasticSearch’e kaydedilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
IElasticSearchService<ErrorLogModel> elasticErrorLogService; //Dependency Injection ile ayağa kaldırılır. Microsoft.Extensions.Options.IOptions<ElasticConnectionSettings> elasticConfig; //Dependency Injection ile ayağa kaldırılır. public ServiceResponse<CompaniesInfoModel> GetUserCompaniesById(int userId) { var response = new ServiceResponse<CompaniesInfoModel>(null); try { //Throw Error for Test throw new StackOverflowException(); //--------------- } catch (Exception ex) { response.ExceptionMessage = ex.Message; response.IsSuccessful = false; response.HasExceptionError = true; //Insert ElasticSearch for ErrorLog ErrorLogModel logModel = new ErrorLogModel(); logModel.Action = "GetUserCompaniesById"; logModel.Controller = "User"; logModel.PostDate = DateTime.Now; logModel.UserID = _workContext.CurrentUserId; logModel.Message = ex.Message; logModel.Method = "GetUserCompaniesById"; logModel.Service = "User"; var w32ex = ex as System.ComponentModel.Win32Exception; if (w32ex == null) { w32ex = ex.InnerException as System.ComponentModel.Win32Exception; } if (w32ex != null) { int code = w32ex.ErrorCode; logModel.ErrorCode = code; } else { logModel.ErrorCode = ex.HResult; } _elasticErrorLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticErrorIndex); //------------------------------ return response; } } |
2-) WebApi/Infrastructure/LoginFilter.cs: Hatalar konusunda, bir de yetkiler yüzünden oluşabilecek hataların loglanmasını inceleyelim. UserController’da, action’a girilmeden önce, LoginFilter’ın OnActionExecuting() methodunda yetki kontrolü yapıldığında, eğer user’ın o action için geçerli bir yetkisi yok ise, ilgili hata ElasticSearche’ün “error_log” index’ine, document olarak kaydedilir.
- “if (_action.IdSecurityAction == 0)“: User’ın, ilgili action için yetkisinin olmadığı anlaşılır.
- “ ErrorLogModel logModel = new ErrorLogModel()“: ErrorLogModel’e yetkiden dolayı girilemeyen Action, Controller varsa Servis adı, yetkisi olmayan UserID, denendiği PostDate zamanı ve girilemiyen method ismi atanarak, ElasticSearch’de “error_log” indexine kaydedilir.
- “_elasticErrorLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticErrorIndex)“: “error_log” index önceden yok ise yaratılıp, tanımlanan ErrorLogModel yani document, ElasticSearch’e kaydedilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
if (_action.IdSecurityAction == 0) { //Forbidden 403 Result. Yetkiniz Yoktur.. context.Result = new ObjectResult(context.ModelState) { Value = "Bu sayfa için, geçerli bir yetkiniz yoktur.", StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden }; //Insert ElasticSearch for 403 ErrorLog ErrorLogModel logModel = new ErrorLogModel(); string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; logModel.Action = action; logModel.Controller = controller; logModel.PostDate = DateTime.Now; logModel.UserID = _workContext.CurrentUserId; logModel.Message = "Bu sayfa için, geçerli bir yetkiniz yoktur."; logModel.Method = "GetUserCompaniesById"; logModel.Service = "User"; logModel.ErrorCode = 403; _elasticErrorLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticErrorIndex); //------------------------------ return; } } |
Kibana error_log Index:
4-) *Audit Log Index:
Geldik, benim en önem verdiğim index tipine. Bir kayıdın güncellenmeden önceki halinin saklanması gerektiği durumlarda, elasticsearch’ün kullanımı hem çok pratik hem de sonradan erişilmek istendiğinde çok hızlıdır. Bunu gelin var olan bir projede, Global olarak nasıl implemente edebileceğimize hep beraber inceleyelim.
DB/PartialEntites/IAuditable : Aşağıda görüldüğü gibi “IAuditable” adında bir interface oluşturulmuştur. İşte Audit işleminin yapılacağı tabloların, bu interfaceden türetilmesi gerekmektedir.
1 2 3 4 5 6 7 8 9 10 11 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.DB.PartialEntites { public interface IAuditable { bool Deleted { get; set; } } } |
Core/Models/Management/AuditModel:
- UserID: Audit işlemini yapan kişi.
- JsonModel: Güncellenecek datanın önceki hali, json olarak tutulur.
- ClassName: Tür dönüşümü için, saklanacak sınıfın adı.
- Operation: Bu örnekte, sadece “Update” işlemi öncesi Audit kaydı yapılmaktadır. Ama ilerde başka işlemlerden önce, mesela hard delete öncesi de Audit kaydı yapılmak istenir ise, bunu ayrıştırmak için tanımlı “Operation” kolonu kullanılacaktır.
- “PostDate”: İşlemin yapıldığı tarih ve saat alanıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Models.Management { public class AuditLogModel { public int UserID { get; set; } public string JsonModel { get; set; } public string ClassName { get; set; } public string Operation { get; set; } public DateTime PostDate { get; set; } } } |
DB/PartialEntites: Aşağıda görüldüğü gibi, Audit kaydı alınmak istenen DbSecurityAction tablosu, IAuditable interfaceinden türetilmiştir.
1 |
public partial class DbSecurityAction : BaseEntity, IAuditable { } |
Repository /GeneralRepository: Aşağıdaki örnekte, projenin tamamında kullanılan repository sınıfının Update methodunu inceleyeceğiz. Audit işleminin, update işleminden hemen önce ve tek biryerden yönetilmesi için buna en uygun yer, repository katmanıdır.
Not: Eğer bunun gibi büyük projelerde, Repository katmanı kullanılmasa idi ve doğrudan servis katmanında DB işlemleri yapılsa idi, bu Audit entegrasyonunun tüm servis methodlarında tekrarlanması gerekecekti!
- “if (updateEntity is IAuditable)“: Güncelleme yapılacak sınıfın, IAuditable interface’inden türetilip türetilmediği kontrol edilir. Türetilmiş ise, Audit işlemi yapılır.
- “jsonString = JsonConvert.SerializeObject(updateEntity)“: Güncellenecek sınıf, serialize edilip string olarak ilgili property’e atanır.
- “AuditLogModel logModel = new AuditLogModel()” : Kaydedilecek Audit model, güncelleme işleminden önce ilgili propertyleri doldurularak oluşturulur.
- “_elasticAuditLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticAuditIndex)” : İlgili AuditLog model, önceden yaratılmamış ise yeni bir “audit_log” index ile Elasticsearch’e yaratılarak kaydedilir.
1 "ElasticAuditIndex": "audit_log"- “_context.Entry(updateEntity).CurrentValues.SetValues(setEntity)” : Tablonun yeni, hali eski hali üzerine ezilir.
- “if (property.CurrentValue == null) { _context.Entry(updateEntity).Property(property.Metadata.Name).IsModified = false; }” : Sadece, değişen alanların güncellenmesi için null kontrolü yapılır. Ve değişmeyen alanlar için “IsModified = false” değeri atanır.
- “_context.SaveChanges()“: Tablonun son güncel hali kaydedilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public virtual void UpdateMatchEntity(T updateEntity, T setEntity, bool isEncrypt = false) { //updateEntity: Varolan hali, setEntity: Güncellenmiş hali if (setEntity == null) throw new ArgumentNullException(nameof(setEntity)); if (updateEntity == null) throw new ArgumentNullException(nameof(updateEntity)); //Audit if (updateEntity is IAuditable) { //Insert ElasticSearch for AuditLog string jsonString; jsonString = JsonConvert.SerializeObject(updateEntity); AuditLogModel logModel = new AuditLogModel(); logModel.PostDate = DateTime.Now; logModel.UserID = _workContext.CurrentUserId; logModel.JsonModel = jsonString; logModel.Operation = "Update"; logModel.ClassName = updateEntity.GetType().Name; _elasticAuditLogService.CheckExistsAndInsertLog(logModel, _elasticConfig.Value.ElasticAuditIndex); } //-----------Audit Finish _context.Entry(updateEntity).CurrentValues.SetValues(setEntity);//Tüm kayıtlar, kolon eşitlemesine gitmeden bir entity'den diğerine atanır. foreach (var property in _context.Entry(setEntity).Properties) { if (property.CurrentValue == null) { _context.Entry(updateEntity).Property(property.Metadata.Name).IsModified = false; } } _context.SaveChanges(); } |
Kibana “audit_log” Index :
“Aramak Bulmanın Yarısıdır.” ―Bora Kaşmer
5-) ElasticSearch’de Sorgulama:
ElasticSearch’de ektiğini biçme yeri, tam olarak burasıdır. Yani sıra geldi, atılan indexlerin, belirlenen filitrelere göre sorgulanmasına.
ElasticSearchService.cs(2):
- SearchLoginLog():
- Login olan kullanıcıların sorgulandığı methoddur. Başlangıç(BeginDate) ve Bitiş(EndDate) tarihleri null ise, default değer atanmıştır.
- “var response = _client.Search<LoginLogModel>(s => s“: ElasticSearch üzerinde, “login_log” index’inde arama yapılmıştır. İlgili index’in “T” tipine, “LoginLogModel” tanımlanmıştır.
- “.From(page) .Size(rowCount)“: From ve Size tanımlamaları ile pagination yani sayfalama işlemleri yapılmıştır.
- “.Sort(ss => ss.Descending(p => p.PostDate))” : Kayıtlar, kaydedilme zamanına göre tersten sıralanmışdır.
- “Query(q => q .Bool(b => b .Must(” : Belirlenen koşulların var ise “and” ile sorguya eklenmesi sağlanmıştır.
- “q => q.Term(t => t.UserID, userID.ToLower().Trim()), q => q.Term(t => t.Controller, controller.ToLower().Trim()), q => q.Term(t => t.Action, action.ToLower().Trim())” : Elasticsearch’de Term, bir filitre koşuludur. UserID, Action ve Controller parametrelerinin herbiri ayrı bir term’dir. Termler, sadece boolen yani “Yes/No” veya string bir kelime ile eşleşebilecek durumlarda kullanılırlar. Eğer bu parametrelerden biri gelir ise, and’li şekilde filitreye eklenirler.
- “q.DateRange(r => r“: Tarihde, belirli bir aralığa göre filitreleme yapmak için kullanılırlar.
-
- “Field(f => f.PostDate)“: Tarih koşuluna bakılacak, field tanımlanır.
- “GreaterThanOrEquals(DateMath.Anchored(((DateTime)BeginDate).AddDays(-1)))“: Bu tarih değerinden büyük tarihli olanlar alınacaktır. AddDays(-1) bir gün önceye gidilme nedeni, saat farkıdır.
- .LessThanOrEquals(DateMath.Anchored(((DateTime)EndDate).AddDays(1))) )): Bu tarihden küçük olanlar alınacaktır. AddDays(1) bir gün sonrasına gidilme nedeni, yine saat farkıdır.
- “.Index(indexName)” : Aranacak index ismi, string olarak verilir(login_log).
-
- SearchErrorLog(): SearchLoginError ile hemen hemen aynıdır. Tek fark ErrorCode, Service ve Method’a göre ayrıca filter vardır. Ve tabi ki aranacak index adı değişmektedir.(error_log).
- SearchAuditLog(): Yine “audit_log” index üzerinden UserID, değişiklik yapılan zaman aralığı(BeginDate & EndDate) ve güncelleme yapılan tablo adına(className) göre filitreleme işlemi yapılabilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
public IReadOnlyCollection<LoginLogModel> SearchLoginLog(string userID, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", int? page = 0, int? rowCount = 10, string? indexName = "login_log") { BeginDate = BeginDate == null ? DateTime.Parse("01/01/1900") : BeginDate; EndDate = EndDate == null ? DateTime.Now : EndDate; var response = _client.Search<LoginLogModel>(s => s .From(page) .Size(rowCount) .Sort(ss => ss.Descending(p => p.PostDate)) .Query(q => q .Bool(b => b .Must( q => q.Term(t => t.UserID, userID.ToLower().Trim()), q => q.Term(t => t.Controller, controller.ToLower().Trim()), q => q.Term(t => t.Action, action.ToLower().Trim()), q => q.DateRange(r => r .Field(f => f.PostDate) .GreaterThanOrEquals(DateMath.Anchored(((DateTime)BeginDate).AddDays(-1))) .LessThanOrEquals(DateMath.Anchored(((DateTime)EndDate).AddDays(1))) )) ) ) .Index(indexName) ); return response.Documents; } public IReadOnlyCollection<ErrorLogModel> SearchErrorLog(int? userID, int? errorCode, DateTime? BeginDate, DateTime? EndDate, string controller="", string action="", string method="", string services="", int page = 0, int rowCount = 10, string indexName = "error_log") { BeginDate = BeginDate == null ? DateTime.Parse("01/01/1900") : BeginDate; EndDate = EndDate == null ? DateTime.Now : EndDate; var response = _client.Search<ErrorLogModel>(s => s .From(page) .Size(rowCount) .Sort(ss => ss.Descending(p => p.PostDate)) .Query(q => q .Bool(b => b .Must( q => q.Term(t => t.UserID, userID), q => q.Term(t => t.Controller, controller.ToLower().Trim()), q => q.Term(t => t.Action, action.ToLower().Trim()), q => q.Term(t => t.ErrorCode, errorCode), q => q.Term(t => t.Method, method.ToLower().Trim()), q => q.Term(t => t.Service, services.ToLower().Trim()), q => q.DateRange(dr => dr .Field(p => p.PostDate) .GreaterThanOrEquals(DateMath.Anchored(((DateTime)BeginDate).AddDays(-1))) .LessThanOrEquals(DateMath.Anchored(((DateTime)EndDate).AddDays(1))) )) ) ) .Index(indexName) ); return response.Documents; } public IReadOnlyCollection<AuditLogModel> SearchAuditLog(int? userID, DateTime? BeginDate, DateTime? EndDate, string className = "", string operation = "Update", int page = 0, int rowCount = 10, string indexName = "audit_log") { BeginDate = BeginDate == null ? DateTime.Parse("01/01/1900") : BeginDate; EndDate = EndDate == null ? DateTime.Now : EndDate; var response = _client.Search<AuditLogModel>(s => s .From(page) .Size(rowCount) .Sort(ss => ss.Descending(p => p.PostDate)) .Query(q => q .Bool(b => b .Must( q => q.Term(t => t.UserID, userID), q => q.Term(t => t.Operation, operation.ToLower().Trim()), q => q.Term(t => t.ClassName, className.ToLower().Trim()), q => q.DateRange(dr => dr .Field(p => p.PostDate) .GreaterThanOrEquals(DateMath.Anchored(((DateTime)BeginDate).AddDays(-1))) .LessThanOrEquals(DateMath.Anchored(((DateTime)EndDate).AddDays(1))) )) ) ) .Index(indexName) ); return response.Documents; } |
İlgili Sorgulama Servisleri için WebApi yazalım:
WebApi/UserController:
Aşağıda görüldüğü gibi, ElasticSearch üzerindeki tüm indexlere örnek amaçlı sorgular atılmıştır.
- GetUserLog : Login olan veya “[LogAttribute]“‘ü ile işaretlenmiş Actionlara giren Userların, Elasticsearch’de “login_log” index’i üzerinden sorgulandığı methoddur.
- GetErrorLog : Projede hatanın yakalandığı veya yetkisiz girişlerin olduğu yerlerin, Elasticsearch’de “error_log” index’i üzerinden sorgulandığı methoddur.
- GetSecurityActionAuditLog : IAuditable interface’inden türemiş sınıfların, update işleminden önceki hallerinin, Elasticsearch’de “audit_log” index’i üzerinden sorgulandığı methoddur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[HttpGet("GetUserLog")] public ServiceResponse<LoginLogModel> GetUserLog(int? userId, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", int? page = 0, int? rowCount = 10) { var response = new ServiceResponse<LoginLogModel>(HttpContext); response.List = _userService.GetUserLog(userId, BeginDate, EndDate, controller, action, page, rowCount).List; response.IsSuccessful = true; return response; } [HttpGet("GetErrorLog")] public ServiceResponse<ErrorLogModel> GetErrorLog(int? userId, int? errorCode, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", string method = "", string service = "", int page = 0, int rowCount = 10) { var response = new ServiceResponse<ErrorLogModel>(HttpContext); response.List = _userService.GetErrorLog(userId, errorCode, BeginDate, EndDate, controller, action, method, service, page, rowCount).List; response.IsSuccessful = true; return response; } [HttpGet("GetSecurityActionAuditLog")] public ServiceResponse<SecurityActionModel> GetSecurityActionAuditLog(int? userId, DateTime? BeginDate, DateTime? EndDate, string className = "", string operation = "Update", int page = 0, int rowCount = 10) { return _userService.GetAuditLog<SecurityActionModel>(userId, BeginDate, EndDate, className, operation, page, rowCount); } |
User/GetSecurityActionAuditLog:
Services/Users/UserService: Sorgulama amaçlı Controller tarafından çağrılan User Servicesler, aşağıda görüldüğü gibidir.
- “GetUserLog()” : Client’ın girdiği sayfaların, istenen kriterlere göre filitrelenip listelendiği methoddur. Elasticsearchde, “login_log” indexinden çekilen documentler, istenen LoginLogModel data tipinde geri dönülür.
- “GetErrorLog()“: Projede alınan hatalar, istenen kritere göre filitrelenip listelenir. Elasticsearchde, “error_log” indexinden çekilen documentlar, istenen ErrorLogModel data tipinde, geri dönülür.
- “GetAuditLog()“: İstenen tablonun geçmişi, belirtilen kriterlere göre listelenir. Elasticsearchde, “audit_log” indexinden çekilen AuditLogModel tipindeki documentların “JsonModel” kolonu, istenen data tipinde deserialize edilerek, ServiceResponse<T> tipinde geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public ServiceResponse<LoginLogModel> GetUserLog(int? userID, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", int? page = 0, int? rowCount = 10, string? indexName = "login_log") { var response = new ServiceResponse<LoginLogModel>(null); var model = _elasticSearchService.SearchLoginLog(userID.ToString(), BeginDate, EndDate, controller, action, page, rowCount, indexName); response.List = model.ToList(); return response; } public ServiceResponse<ErrorLogModel> GetErrorLog(int? userID, int? errorCode, DateTime? BeginDate, DateTime? EndDate, string controller = "", string action = "", string method = "", string service = "", int page = 0, int rowCount = 10, string indexName = "error_log") { var response = new ServiceResponse<ErrorLogModel>(null); var model = _elasticSearchService.SearchErrorLog(userID, errorCode, BeginDate, EndDate, controller, action, method, service, page, rowCount, indexName); response.List = model.ToList(); return response; } public ServiceResponse<T> GetAuditLog<T>(int? userID, DateTime? BeginDate, DateTime? EndDate, string className = "", string operation = "Update", int page = 0, int rowCount = 10, string indexName = "audit_log") { var response = new ServiceResponse<T>(null); try { var result = _elasticSearchService.SearchAuditLog(userID, BeginDate, EndDate, className, operation, page, rowCount, indexName); result.Each<AuditLogModel>(item => { T securitActionItem = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(item.JsonModel); response.List.Add(securitActionItem); }); response.IsSuccessful = true; } catch (Exception ex) { response.IsSuccessful = false; response.HasExceptionError = true; response.ExceptionMessage = ex.Message; } return response; } |
Böylece geldik bir makalenin daha sonuna. Bu makalede Elasticsearch’ü, var olan bir .Net Core projeye implementasyonunu inceledik. Amaç belirlenen indexlerin, mümkün olduğunca var olan koda dokunmadan, en basit kod parçaları ile ElasticSearch’e atılabilmesidir.
“Değiştirilmeyen bir düzen, kötü bir düzendir.” ―P.Syrus
Implemente Edilen Durum ve Indexler:
- Audit loglama için, tek yapılamsı gereken şey, logu alınacak tablonun “IAuditable“ interface’inden türetilmesidir.
- User Loglama yani ilgili Action’a giren clientların kayıtlarının tutulması için, ilgili controller’ın tepesine [LogAttribute] Attribute’ünün eklenmesi yeterlidir.
- Login olan clientların loglanması için, var olan koda hiçbir şeyin eklenmesine gerek yoktur. Çünkü zaten var olan [LoginLogFilter]‘a, login olunması durumunda ilgili ElasticSearch’e index atan kodlar yazılmıştır.
- Error Loglama için, tek yapılması gereken hata yakalanan alanlarda, ElasticSearch’ün, “CheckExistsAndInsertLog()” methodunun, “error_log” index’i ile çağrılmasıdır.
- Yetkisiz giriş durumunda, yine herhangi bir ekstra işlemin yapılmasına gerek yoktur. LoginFilter Attribute’ü üzerinde yapılan denetim ile ilgili ErrorLog, gene “CheckExistsAndInsertLog()” methoduna doldurulan ErrorLogModel‘inin göndermesi ile ElasticSearch’e kaydedilir.
Biz bu makalede Elasticsearch’ü, alışıla gelmiş gece çalışan joblar ile kümülatif olarak atmak yerine, anlık olarak ilgili documanların indexlendiği bir yol seçtik. Eğer bunun performans kaybına yol açacağı düşünülür ise, ilgili documanların bir Queue’ye konup, arakada çalışan Microserviceslerle de, asenkron olarak ElasticSearch’e atılması sağlanabilir.
Büyük bir uygulamada Clean Code , Dağıtık Mimarı, Reporistory Pattern ve OOP gibi prensiplerin kullanılması, onun kolay geliştirilebilmesini, test edilebilmesini ve okunabilmesini sağlar. Bu ElasticSearch implementasyonunda da görüldüğü gibi, ilgili entegrasyon yazılımı bittikten sonra, developerların gündelik kodlamalarında pek de birşey değişmemektedir. Tek bir [LogAttribute]’ünün, action’ın tepesine konması ya da ilgili sınıfların IAuditable interface’inden türetilmesi yeterlidir. Gerisi arkada otomatik olarak çalışacaktır. İşte kod prensiplerine uyarak yazmanın en önemli artısı, kod sadeliği ve geliştirmelere açık olduğu için minimum kod ile maximum işin yapılabilmesidir.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source:
Yine güzel bir makale. Emeğinize sağlık hocam :)
Teşekkür ederim.
Bunu neden daha önçe fark etmemişim :)
Ellerinize sağlık.
Elinize sağlık hocam. Bu projeyi githubtapaylaşabilmeniz mümkün mü hocam?