Asp.Net Mvc’de MicroServis Mantığında RabbitMQ ve Dapper ile Error Log Tutma
Selamlar,
Bu makalede, herhangi bir uygulamanın log bilgisinin var olan sistemden bağımsız bir Microservis ile nasıl tutulacağını hep beraber inceleyeceğiz. Burada amaç çalışan yoğun sistemleri yormadan, farklı işlemler için küçük MicroServislerin asenkron olarak çalıştırılmasıdır.
Öncelikle ben Microservis olarak içinde RabbitMQ kullanan, bir queue ‘dan data çeken basit bir Windows servis oluşturacağım. RabbitMQ ile ilgili detaylı bilgiyi, önceki makalelerimden edinebilirsiniz. Makinanızda Erlang ve RabbitMQ’nun kurulu olması gerekmektedir.
Kaydedilecek data bir error log dosyası olacaktır. Öncelikle ilgili MSSql Error Database’ini ve ErrorLog tablosunu aşağıdaki gibi oluşturalım.
ErrorLog Tablosu:
ErrorLog Class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace ErrorService { public partial class ErrorLog { public int ID { get; set; } public string ErrorText{ get; set; } public string Platform{ get; set; } public int ErrorCode{ get; set; } public DateTime? CreatedDate { get; set; } } } |
Visual Studio 2015 ortamında bir ErrorService adında bir Windows Service projesi aşağıdaki gibi yaratılır.
Nugetten aşağıdaki paketler indirilir:
Program.cs: Aşağıda görüldüğü gibi ilgili windows service, debug modda çalıştırılıp test edilebilmesi için “#if DEBUG” tanımlaması ile “ErrorListener” sınıfının “DoWork()” methodu çalıştırılır. Bu şeklide gerektiğinde ilgili servisin satır satır test edilmesi sağlanmıştı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 |
using System; using System.Collections.Generic; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading.Tasks; namespace ErrorService { static class Program { /// <summary> /// The main entry point for the application. /// </summary> static void Main() { #if DEBUG var service = new ErrorListener(); service.DoWork(); #else ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new ErrorService() }; ServiceBase.Run(ServicesToRun); #endif } } } |
ErrorService.cs: Release Modda aşağıda görülen ErrorService sınıfı çalıştırılmaktadı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 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading.Tasks; namespace ErrorService { public partial class ErrorService : ServiceBase { public ErrorService() { InitializeComponent(); } protected override void OnStart(string[] args) { var service = new ErrorListener(); service.DoWork(); } protected override void OnStop() { } } } |
Windows Servisin çalışması için run prompt’da “services.msc” yazılıp yukarıdaki “RabbitMQ” servisinin “Runnig” yani çalışır modda olduğuna bakılmalıdır. İlgili servisin çalışması durumunda, browser penceresine “http://localhost:15672” yazılması ile ilgli RabbitMQ Queue’leri, aşağıdaki gibi görülebilmektedir.
ErrorListener: Sıra geldi esas işin yapıldığı ErrorListener sınıfının yazılmasına. Burada amaç, RabbitMQ’da sırada bekleyen ErrorLog paketlerinin alınması ve herbirinin Dapper ile yukarıda yaratılan MsSql DB’ye kaydedilmesidir. Kaydedilme işleminden sonra ilgili paketin Queue’dan temizlenir.
Aşağıda ilk dikkat edilecek husus sistemden bağımsız bir “new Thread(()=>“‘in çalıştırılmasıdır.
- Bağlanılacak RabbitMQ sunucusu “localhost“‘dur.
- “Connection ve channel” using ile oluşturulur.
- “channel.QueueDeclare” ile dinlenecek sıranın ErrorLog olduğu, “queue: ErrorLog” ile tanımlanır.
- “channel.BasicConsume” ile ilgili mesaj çekme işlemine başlanır.
- “while(true)” burada ilgili dinleme işleminin sonlanmayıp, ilgili queue lerin sürekli olarak dinlenmesi sağlanmıştır.
- “consumer.Queue.Dequeue()” methodu ile ilgili queue’den sıradaki “ErrorLog” alınmıştır.
- İlgili paket içeriği “message“‘a atanmış ve Newtonsoft.Json kullanılarak ilgili message “<ErrorLog>” tipine Deserialize edilmiştir.
- İlgili “Error” DB’sine ait SqlConnection açılır.
- Queue’den gelen pakete göre yeni bir “ErrorLog” kaydı oluşturulur. “ErrorLog log = new ErrorLog() { ErrorText= _errorLog.ErrorText, Platform = _errorLog.Platform, ErrorCode = _errorLog.ErrorCode, CreatedDate = DateTime.Now };“
- Dapper sayesinde yanda görülen, tabloya kaydedilecek insert cümlesi oluşturulur. “Insert into [dbo].[ErrorLog]([ErrorText],[Platform],[ErrorCode],[CreatedDate]) VALUES (@ErrorText,@Platform,@ErrorCode,@CreatedDate)“
- Son olarak ilgili query, yandaki gibi execute edilir. “sqlConnection.Execute(sqlQuery, log);“
App.config(connectionStrings):
1 2 3 4 |
<connectionStrings> <add name="Error" connectionString="Server=(local);Database=Error; Trusted_Connection=True;" providerName="System.Data.SqlClient" /> </connectionStrings> |
ErrorListener.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 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 |
using Dapper; using Newtonsoft.Json; using RabbitMQ.Client; using RabbitMQ.Client.Events; using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ErrorService { class ErrorListener { public void DoWork() { try { new Thread(() => { var factory = new ConnectionFactory() { HostName = "localhost" }; using (IConnection connection = factory.CreateConnection()) using (IModel channel = connection.CreateModel()) { channel.QueueDeclare(queue: "ErrorLog", durable: false, exclusive: false, autoDelete: false, arguments: null); var consumer = new QueueingBasicConsumer(channel); channel.BasicConsume(queue: "ErrorLog", noAck: true, consumer: consumer); while (true) { var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue(); var body = ea.Body; var message = Encoding.UTF8.GetString(body); ErrorLog _errorLog = JsonConvert.DeserializeObject<ErrorLog>(message); using (SqlConnection sqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["Error"].ToString())) { sqlConnection.Open(); ErrorLog log = new ErrorLog() { ErrorText= _errorLog.ErrorText, Platform = _errorLog.Platform, ErrorCode = _errorLog.ErrorCode, CreatedDate = DateTime.Now }; string sqlQuery = "Insert into [dbo].[ErrorLog]([ErrorText],[Platform],[ErrorCode],[CreatedDate]) VALUES (@ErrorText,@Platform,@ErrorCode,@CreatedDate)"; sqlConnection.Execute(sqlQuery, log); Console.WriteLine($" ErroCode: {_errorLog.ErrorCode} Platform:{_errorLog.Platform} [{_errorLog.ErrorText}]"); sqlConnection.Close(); } } } }).Start(); } catch (Exception ex) { string error = ex.Message; } } } } |
İlgili Windows Service’nin yani ErrorService’in Install edilmesi:
- Öncelikle .NetFrameWork’ün kurulu olduğu folder’a command prompt’dan yandaki gibi “Admin” yetkisi ile açılıp gelinir. Sizdeki vesiyonu farklı olabilir. “C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\“
- InstallUtil.exe “C:\Users\Bora KASMER\Documents\Visual Studio 2015\Projects\Error\ErrorService\bin\Release\ErrorService.exe” komutu çalıştırılır.
- Önemli bir nokta”services.msc” komutu ile ilgili “ErrorService”‘e gidilip sağ tıklanıp “Log On” sekmesindeki “This account” SqlServer’ınızdaki User ile aynı olmalıdır. Yoksa ilgili servis DB’ye yazma sırasında hata vermektedir.
İlgili ErrorService’in User’ı aşağıdaki gibi değiştirilir:
SqlDB’ye hangi user ile bağlanıldığı, property penceresinden aşağıdaki gibi görülmektedir:
Error Windows Servis için ProjectInstaller Oluşturma:
ErrorService sağ tıklanıp “Add Installer” seçilir. Projeye “ProjectInstaller” adında bir dosya eklenir. “serviceProcessInstaller1” ve “ErrorService“‘in property ekranları aşağıdak gibidir.
“ErrorService”‘in, install işleminden sonra Servisler kısmında aşağıdaki gibi gözükmesi gerekmektedir. Servise ait “Yetkili User”‘ı değiştirmeyi ve ve tabi ki start etmeyi unutmayın:)
Şimdi sıra geldi ilgili servisi hata durumunda çağırmaya:
“ErrorLogTest” şeklinde boş bir Mvc Web Application açıyoruz. 2 sayıyı birbirine böldüreceğiz ve sonucu ekrana yazacağız:
Index.cshtml: Post işlemi ile “Calc” action’ına gidilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@{ Layout = null; } <!DOCTYPE html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <form action="/Home/Calc" method="post" autocomplete="off"> <div> 1. Sayı <input type="text" id="firstNumber" name="firstNumber" tabindex="1" autofocus> </div><br> <div> 2. Sayı <input type="text" id="secondNumber" name="secondNumber" tabindex="2"><input type="submit" value="Hesapla" tabindex="3"/> </div><br><br> <hr /></hr> <div> Bölüm Sonucu <input type="text" id="result" name="result" value="@ViewBag.Result" readonly> </div> </form> </body> |
HomeController.cs: Aşağıda görüldüğü gibi 2 sayının bölümü ve sonucu tekrardan geri dönülmüştür. Örnek basit ama amaç tamamen oluşabilecek hatalara göre MicroServisler kullanılarak asenkron logların tutulmasıdı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 |
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace ErrorLogTest.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { ViewBag.Result = ""; if (TempData["result"]!=null) { ViewBag.Result = TempData["result"]; } return View(); } public ActionResult Calc(int firstNumber,int secondNumber) { var result = firstNumber / secondNumber; TempData.Add("result", result); return RedirectToAction("Index"); } } } |
Öncelikle gelin hep beraber ilgili “RabbitMQ“‘ya bağlanılıp nasıl Queue’ya yeni bir “ErrorLog” paketi atılıyor onu inceleyelim.
- Yukarıda tanımlanan “ErrorLog” modeli, bu projede de yine Model klasörü altına tanımlanır.
Publisher.cs: Burada “localhost“‘da çalışan ve credential istemeyen bir RabbitMQ servisine bağlanılmaktadır. Parametre olarak alınan “ErrorLog” sınıfı byte’a çevrilip “BasicPublish()” methodu ile gönderilmektedir.
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 |
using Newtonsoft.Json; using RabbitMQ.Client; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; namespace ErrorLogTest.Controllers { public static class Publisher { public static void SendMessage(ErrorLog log) { var factory = new ConnectionFactory() { HostName = "localhost" }; using (IConnection connection = factory.CreateConnection()) using (IModel channel = connection.CreateModel()) { channel.QueueDeclare(queue: "ErrorLog", durable: false, exclusive: false, autoDelete: false, arguments: null); string _errorLog = JsonConvert.SerializeObject(log); var body = Encoding.UTF8.GetBytes(_errorLog); channel.BasicPublish(exchange: "", routingKey: "ErrorLog", basicProperties: null, body: body); Console.WriteLine($" ErroCode: {log.ErrorCode} Platform:{log.Platform} [{log.ErrorText}]"); } } } } |
Platform(Enum):
1 2 3 4 5 6 7 |
enum Platform { Web=1, Mobile=2, Library=3, MicroService=4 } |
Home/Calc() Update: Aşağıda görüldüğü gibi hata olması durumunda “Publisher” static sınıfına ait “SendMessage()” methodu, oluşan hataya göre yeni bir “ErrorLog()” sınıfı oluşturulup parametre olarak gönderilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public ActionResult Calc(int firstNumber,int secondNumber) { try { var result = firstNumber / secondNumber; TempData.Add("result", result); return RedirectToAction("Index"); } catch(Exception ex) { int _errorCode = System.Runtime.InteropServices.Marshal.GetExceptionCode(); Publisher.SendMessage(new ErrorLog() { ErrorText = ex.Message, ErrorCode = _errorCode, Platform = Platform.Web.ToString(), CreatedDate = DateTime.Now, }); return RedirectToAction("Index"); } } |
Ayrıca Global Hataların Yakalanması İçin Global.asax’a “Application_Error()” methodun’da aşağıdaki gibi static “Publisher” sınıfına ait “SendMessage()” methodu çağrılmış ve ilgili hata RabbitMQ’ya gönderilmiştir.
Global.asax:
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 |
using ErrorLogTest.Controllers; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace ErrorLogTest { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } void Application_Error(object sender, EventArgs e) { Exception ex = Server.GetLastError(); if (ex != null) { int _errorCode = System.Runtime.InteropServices.Marshal.GetExceptionCode(); //Log Publisher.SendMessage(new ErrorLog() { ErrorText = ex.Message, ErrorCode = _errorCode, Platform = Platform.Web.ToString(), CreatedDate = DateTime.Now, }); } } } } |
Böylece bu makalede özellikle yoğun tarafik alan bir portalın, var olan sistemi yormadan yapması gereken işleri kendisinin değilde, asenkron olarak MicroServicelere nasıl yaptırdığını hep beraber inceledik.
Bu makalede DB’ye hata loglarını atan bir yapıyı kurgularken, mesajlaşma sisteminde chat kayıtlarını tutan ya da bir portalda güncellenen kayıtların cache’lerini düşüren yapılar da kurguluyabilirdik. Hatta binlerce gelen başvuru formlarını bile bu mikroserviceslere, kolaylıkla asenkron olarak yaptırabilirdik. Birçok farklı Quee sistem bulunmaktadır. MsMq, Kafka, RabbitMQ gibi… Hepsinin kendine göre artıları ve eksileri bulunmaktadır. Bazen hıza önem verilip, güvenliğe çok önem verilmediği durumlarda Quee yerine “Redis Pub/Sub” gibi direk haberleşen yapılara da kullanılabilmektedir. Kısaca bir işin yapılabileceği birçok yöntem vardır. Yeter ki amaca, bütçeye ve ihtiyaca göre en makul teknolojinin, kullanılmasıdır. Bu da hiçbir zaman bitmeyen bir yolculuktur.
Geldik bir makelenin daha sonuna :) Yeni bir makalede görüşmek üzere hoşçakalın.
Source:
Merhabalar;
Öncelikle bu güzel yazı için teşekkürler. Bir konuda yorumunuzu rica edeceğim:
Loglama işlemini, sistemden ayrı tutmasaydık eski usül ile aynı uygulama veritabanında bir ErrorLog tablosu oluşturacaktık ve excetion’u yakaladığımız yerde, hatayı bu tabloya direk yazacaktık.
Yani şu kod yerine:
Publisher.SendMessage(new ErrorLog() { ErrorText = ex.Message, ErrorCode = _errorCode, Platform = Platform.Web.ToString(), CreatedDate = DateTime.Now, });
Şöyle bir kodumuz olacaktı:
ErrorLog.SaveError(ex.Message, errorCode, platform, Datetime.Now);
Şimdi soru şu: DB’ye kaydetmek ile RabbitMQ servisini çağırmak arasında nasıl bir performans farkı var?
Teşekkürler
Hocam asp.net mvc projelerinizde , mvc nin kendi Authentication ‘unu mu kullanıyorsunuz yoksa kendiniz mi yazıyorsunuz. Profesyönel hayatta nasıl oluyor merak ediyorum
Teşekkür ederim eğitimleriniz için.
Authentication kurumsal firmalarda genelde custom yapılır. Social Login’de(Facebook,Google,Twitter gibi..) vardır. Ama yazılan uygulamanın önem ve güvenlik derecesine göre değişiklik gösterir. Genelde firmaya özel yazılır.
İyi çalışmalar.
Hocam ellerinize saglık. Çok faydalı bir yazı olmuş. Biz de şirket olarak kendi altyapımızda bu sorunu bir thread havuzu olusturarak çözdük. Thread havuzu da benzer sekilde 1producerNconsumer şeklinde loglamaları yapıyor. Threadleri kodlarken tabiki IRegisteredObject kullandık. Aklımıza bunun msmq’lu cozumu de geldi. Fakat kurulumu zorlastırmamak adına tek uygulamada çözmeye çalıştık problemi. Şu an için sorun yok. Fakat bizim öngöremediğimiz bir sorun çıkabilir mi sizce? Yorumlarınızı merak ettim açıkçası.
Selam Mehmet,
Öncelikle teşekkürler.
Sizin yapıda Loglama İşlemi o an yapılamaz ise kalır. Farklı, yani dağıtık uygulamalar ile çalışmanın faydası, işlem yapılacak microservis patlasa dahi, joblar Queue’de toplanacak ve Microservis ayağa kalkınca sıradaki log işlemleri tekrardan işlenme amaçlı devreye alınacaktır. İşte Queue’nin ve dağıtık mimarinin en büyük faydası budur :)
İyi çalışmalar.