.Net Core MVC Bir Projede Actionların Çalışma Süresini Razor Ve Google Chart İle Raporlama
Selamlar Arkadaşlar,
Bu makalede yine herkesin ihtiyacı olabilecek bir konuyu tartışacağız. MVC bir web uygulamasında hangi sayfanın ortlama olarak ne kadar sürede geldiğini, ve ilgili sayfanın seçilmesi durumunda, detayına gidilerek son 10 kaydınını kronolojik olarak ekrana basıldığı uygulamayı hep beraber kodlayacağız. Bu raporlama için, Razor View Engine ve Google Chart kullanılarak animasyonlu bir hale getirilecektir. Bu yapı istenirse çalışma süresi, örneğin 2sn’den fazla olan Actionların yöneticiye mail atılarak bildirildiği, yani projenin monitör edilerek sorunlu yerlerin bildirildiği bir yapı olarak da kullanılabilir.
Gelin önce aşağıda görüldüğü gibi “ActionTotalTime” adında .Net Core Mvc bir proje oluşturalım.
“dotnet new mvc -o ActionTotalTime”
Bu projede farklı dummy birçok sayfamız olacak.
Amaç, farklı sayfaların açılış performancelarını gözlemlemektir. Bunun için her sayfaya ait Actionlar içerisine random olarak oluşturulucak bekleme süreli aşağıdaki gibi konulmuştur.
1 2 3 4 5 |
public int GenerateWaitTime() { Random rnd = new Random(); return rnd.Next(1, 5); } |
Bazı örnek amaçlı dummy sayfalara giden actionlar aşağıdaki gibidir:
HomeController.cs: Görüldüğü gibi tüm actionlarda ==>”System.Threading.Thread.Sleep(GenerateWaitTime() * 1000)” random maximum 5sn ye kadar bekleten threadler kullanılmış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 |
public class HomeController : Controller { public IActionResult Index() { System.Threading.Thread.Sleep(GenerateWaitTime() * 1000); return View(); } public IActionResult About() { System.Threading.Thread.Sleep(GenerateWaitTime() * 1000); ViewData["Message"] = "Your application description page."; return View(); } public IActionResult Contact() { System.Threading.Thread.Sleep(GenerateWaitTime() * 1000); ViewData["Message"] = "Your contact page."; return View(); } public IActionResult Privacy() { System.Threading.Thread.Sleep(GenerateWaitTime() * 1000); return View(); } |
Şimdi sıra geldi, DB’de tutulacak Data Model’e. Aşağıda Actionların çalışma zamanı ile ilgili tutlacak data model(Action Performance) tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 |
using System; public class ActionPerformance { public int ID { get; set; } public string ActionName { get; set; } public string ControllerName { get; set; } public DateTime CreatedDate { get; set; } public double TotalSeconds{get;set;} public string Description{get;set;} } |
Bu projede DB işlemleri için EntityFrameWork DBContext kullanılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 |
using Microsoft.EntityFrameworkCore; public class ActionContext : DbContext { public DbSet<ActionPerformance> ActionPerformance { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer("Server=tcp:10.211.55.9,1433;Initial Catalog=LuckyDB;User ID=*****;Password=*****;"); } } |
Ölçüm:
Şimdi sıra geldi Actionların çalışma zamanını ölçmeye. Bu işlem için bu projede Action Filter kullanılmıştır.
- “TimerFilter” olarak tanımlayacağımız action filter, Action’a ilk girildiği andaki zamanı [“Controller_Action”] isminden key oluşturarak, “HttpContext” altında tutmaktadır. Actiona ilk girildiği zaman “OnActionExecuting()” methodu çağrılmaktadır.
- İlgili Action’dan çıkıldığı zaman, bir başka değiş ile sonlanması sırasında “OnActionExecuted()” metodu çağrılmaktadır. Burada “HttpContext”‘den action’a ilk girilen zaman çekilir.(startTime) Şu anki zaman (endTime) ile zaman farkı alınarak, Action için harcanan toplam zaman bulunur (“TimeSpan“).
- Action için harcanan toplam zamanı saniye cinsinden, action ve controller’ın ismi, işlemin yapıldığı zaman ve saat_dakika_saniye cinsinden geçen zamanın açıklaması “ActionPerformance” tabosuna kaydedilir.
TimerFilter.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 |
using System; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; public class TimerFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; context.HttpContext.Items[controller + "_" + action] = DateTime.Now; } public void OnActionExecuted(ActionExecutedContext context) { string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; DateTime startTime = (DateTime)context.HttpContext.Items[controller + "_" + action]; DateTime endTime = DateTime.Now; TimeSpan diff = endTime.Subtract(startTime); string description = String.Format("{0}:{1}:{2}", diff.Hours, diff.Minutes, diff.Seconds); double totalSeconds = diff.TotalSeconds; using (ActionContext dbContext = new ActionContext()) { ActionPerformance data = new ActionPerformance(); data.ActionName = action; data.ControllerName = controller; data.CreatedDate = DateTime.Now; data.Description = description; data.TotalSeconds = totalSeconds; dbContext.ActionPerformance.Add(data); dbContext.SaveChanges(); } } } |
Not: Şimdi sırada Raporlama ekranı var. Ama öncesinde aklınıza şöyle bir soru gelebilir. Bazı ekranlar için DB’ye kayıt atmak istemezsek, örneğin Raporlama ekranı için log tutmak istemezsek ne yapmalıyız? Bunun için farklı bir çok yol var. Örneğin bir black list yapıp, bunu ya config’de ya da DB’de tutup, ilgili sayfa bu listede var ise log tutulmayabilir. Ama bundan daha şık yollar da var. O da, bir attribute yapıp, bununla işaretli olan Actionları kayıt altına almamak yani loglamamaktır. Kısaca Custom bir Attribute yazacağız ve bunu loglanmasını istemediğimiz Actionların üstüne koyacağız.
IgnoreAction.cs: Bu Attribute sadece loglanmıyacak Actionları, flaglemek için kullanılmıştır.
1 2 3 4 5 6 |
using System; [AttributeUsage(AttributeTargets.Method)] public class IgnoreAction : Attribute { } |
Şimdi gelin TimerFilter class’ını aşağıdaki gibi güncelleyelim.
TimerFilter.cs(Full):
- “HasIgnoreAction()” : Mehod üzerindeki tüm “CustomAttribute”‘ler gezilerek tipi “IgnoreAction” olan var mı diye bakılarak, “true” veya “false” olarak bir sonuç geri dönülür.
- “OnActionExecuting()” Methodunda HasIgnoreAction() methodundaki sonuca göre “context.HttpContext.Items[“HasIgnoreAction”]” değer atılır.
- “OnActionExecuted()” : Methodunda “if ((bool)context.HttpContext.Items[“HasIgnoreAction”]==false)” bakılır. Eğere ilgili Attribute yok ise Loglama işlemine devam edilir.
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 |
using System; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; public class TimerFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { context.HttpContext.Items["HasIgnoreAction"] = true; if (HasIgnoreAction(context)) { return; } string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; context.HttpContext.Items[controller + "_" + action] = DateTime.Now; context.HttpContext.Items["HasIgnoreAction"] = false; } public void OnActionExecuted(ActionExecutedContext context) { if ((bool)context.HttpContext.Items["HasIgnoreAction"]==false) { string action = (string)context.RouteData.Values["action"]; string controller = (string)context.RouteData.Values["controller"]; DateTime startTime = (DateTime)context.HttpContext.Items[controller + "_" + action]; DateTime endTime = DateTime.Now; TimeSpan diff = endTime.Subtract(startTime); string description = String.Format("{0}:{1}:{2}", diff.Hours, diff.Minutes, diff.Seconds); double totalSeconds = diff.TotalSeconds; using (ActionContext dbContext = new ActionContext()) { ActionPerformance data = new ActionPerformance(); data.ActionName = action; data.ControllerName = controller; data.CreatedDate = DateTime.Now; data.Description = description; data.TotalSeconds = totalSeconds; dbContext.ActionPerformance.Add(data); dbContext.SaveChanges(); } } } public bool HasIgnoreAction(ActionExecutingContext context) { foreach (var filterDescriptors in ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.CustomAttributes) { if (filterDescriptors.AttributeType == typeof(IgnoreAction)) { return true; } } return false; } } |
Report :
ReportModel.cs: Raporlama sayfası için kullanılacak data model aşağıdaki gibidir.
1 2 3 4 5 |
public class ReportModel { public string Name { get; set; } public double TotalSeconds { get; set; } } |
Report(): İlgili Controller’a ait Actionların “TotalSeconds” yani sayfanın açılış süresine göre bir Rapor datası çekilmiştir.
- İlgili action, Custom Attribute olan ” [IgnoreAction]” ile işaretlenmiştir. Bu neden ile çalışma süresi loglanmıyacaktır.
- Aşağıda görüldüğü gibi Raporlama amaçlı ilgili “Controller/Action“‘a göre bir guruplama yapılmış ve “TotalSeconds” alanının Ortalaması alınmıştır.
- Çekilen tüm data “ForEach()” ile dönülerek, View tarafında da beklenen “List<ReportModel>” tipindeki “viewModel“‘e ilgili datalar atılmıştır.
- Report.cshtml View’a, doldurulan “viewModel” ile 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 |
[IgnoreAction] public IActionResult Report() { using (ActionContext dbContext = new ActionContext()) { List<ReportModel> viewModel = new List<ReportModel>(); dbContext.ActionPerformance.GroupBy( res => new { Name = res.ControllerName + '/' + res.ActionName }) .Select(g => new { Name = g.Key.Name, TotalSeconds = g.Average(p => p.TotalSeconds) }).ToList() .ForEach(row => { viewModel.Add(new ReportModel() { Name = row.Name, TotalSeconds = row.TotalSeconds }); }); return View(viewModel); } } |
Report.cshtml: Raporlama amaçlı Razor View engine ile birlikte “Google Chart” kullanılmıştır. Google Chart en çok kullanılan toolardan biri olsa da offline kullanılmaya izin vermemesi büyük bir dezavantaj.
- Sayfa model olarak “List<ReportModel>” beklemektedir.
- Sayfa içerisinde kullanılacak scriptler, externel Url ile tanımlanmıştır. Araştırdığı kadarı ile Google Chart offline kullanılmaya izin vermiyor. “google.load()” methodu offline malesef işe yaramıyor.
- “@foreach (var item in Model)” ile tüm kayıt dönülerek ekrana basılmıştır.
- “<text>[‘@item.Name’, @item.TotalSeconds,’@color’],</text>” : Action ismi ve ortalama işlem süresi herbir kayıt için basılır.
- “var color = String.Format(“#{0:X6}”, random.Next(0x1000000))” : Her seferinde ilgili bar rengi, Random olarak ekrana basılır.
- ” var options = { animation:{ startup:true, duration: 1000, easing: ‘out’,”: Animasyon işlemleri burada yapılır. Amaç barların animatif olarak 1sn süresince gelmesidir. “startup:true”‘nun konulması önemlidir. Yoksa animasyon çalışmayacaktır.
- “curveType“: Burada X ve Y kordinatlarındaki başlık, sitil ve max-min değerleri ayarlanmaktadır.
- “google.visualization.events.addListener(chartPerformance, ‘select’, selectAction)” : İlgili charter’ın “select” event’i dinlenir ve herhangi bir bar’ın tıklanması durumunda “selectAction()” methodu çağrılır.
- “selectAction()“: İlgili methodda ==>
- “var selectedBar = chartPerformance.getSelection()[0]” : Seçilen bar’ın datası alınır.
- “window.location.href=”/Home/Detail/”+selectedAction.replace(/\//g,’_’)” : Controller ve Action’ın parametre olarak gönderilerek, Detay sayfasına yönlenilir.
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 |
@model List<ReportModel> <div id="visualization" style="width: 800px; height: 600px;"></div> @section scripts { <script type="text/javascript" src="//www.google.com/jsapi"></script> <script type="text/javascript"> google.load('visualization', '1', { packages: ['corechart'] }); </script> <script type="text/javascript"> function Report() { var data = google.visualization.arrayToDataTable([ ['Action', 'Seconds',{ role: 'style' }], @foreach (var item in Model) { var random = new Random(); var color = String.Format("#{0:X6}", random.Next(0x1000000)); <text>['@item.Name', @item.TotalSeconds,'@color'],</text> } ]); var options = { animation:{ startup:true, duration: 1000, easing: 'out', }, curveType: 'function' , smoothline: 'true' , width: 800 , height: 600 , title: 'All Action Total Seconds' , legend: {position: 'none'} ,vAxis: {minValue:0,title:'Total Seconds',titleTextStyle: {color: 'red'}} ,hAxis: {title:'Actions',titleTextStyle: {color: 'blue'}} }; var chartPerformance=new google.visualization.ColumnChart(document.getElementById('visualization')); function selectAction() { var selectedBar = chartPerformance.getSelection()[0]; if (selectedBar) { var selectedAction = data.getValue(selectedBar.row, 0); window.location.href="/Home/Detail/"+selectedAction.replace(/\//g,'_'); } } google.visualization.events.addListener(chartPerformance, 'select', selectAction); chartPerformance.draw(data, options); } google.setOnLoadCallback(Report); </script> } |
Detail(): Bu method’da da görüldüğü gibi “[IgnoreAction]” custom Attribute’ü ile işaretlenmiştir. Amaç bu Action için de performans kaydının tutulmamasıdır.
- ViewBag.Action’a gelen “Controller/Action” şeklinde Detayı gösterilen sayfa tanımlanır.
- ActionPerformance tablosundan ilgili Action ve Controller’a göre son 10 kayıt çekilir.
- Çekilen her bir kayıt gezilerek “List<DetailModel> viewModel”‘e doldurularak Detay sayfasına gidilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[IgnoreAction] public IActionResult Detail(string ActionName) { ViewBag.Action=ActionName.Replace("_","/"); string action = ActionName.Split('_')[1]; string controller = ActionName.Split('_')[0]; List<DetailModel> viewModel = new List<DetailModel>(); using (ActionContext dbContext = new ActionContext()) { dbContext.ActionPerformance.Where(res => res.ActionName == action && res.ControllerName == controller).Select(lis => new { Name = lis.ControllerName + '/' + lis.ActionName, TotalSeconds = lis.TotalSeconds, CreatedDate = lis.CreatedDate }).Take(10).OrderByDescending(fn => fn.CreatedDate).ToList().ForEach(row => { viewModel.Add(new DetailModel() { Name = row.Name, TotalSeconds = row.TotalSeconds, CreatedDate = row.CreatedDate }); }); } return View(viewModel); } |
Detail.cshtml: Seçilen Action’ı ait son 10 kayıt Google Chart’da “List<DetailModel>” ile yukarıdaki gibi animatif olarak gösterilir.
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 |
@model List<DetailModel> <div id="visualization" style="width: 1000px; height: 800px;"></div> @section scripts { <script type="text/javascript" src="//www.google.com/jsapi"></script> <script type="text/javascript"> google.load('visualization', '1', { packages: ['corechart'] }); </script> <script type="text/javascript"> function Detail() { var data = google.visualization.arrayToDataTable([ ['Created Date', 'Seconds',{ role: 'style' }], @foreach (var item in Model) { var random = new Random(); var color = String.Format("#{0:X6}", random.Next(0x1000000)); <text>['@item.CreatedDate', @item.TotalSeconds,'@color'],</text> } ]); var options = { animation:{ startup:true, duration: 1000, easing: 'out', }, curveType: 'function' , smoothline: 'true' , width: 1000 , height: 800 , title: '@ViewBag.Action Total Seconds' , legend: {position: 'none'} ,vAxis: {minValue:0,title:'Total Seconds',titleTextStyle: {color: 'red'}} ,hAxis: {title:'Created Date',titleTextStyle: {color: 'blue'}} }; var chartPerformance=new google.visualization.ColumnChart(document.getElementById('visualization')); chartPerformance.draw(data, options); } google.setOnLoadCallback(Detail); </script> } |
Son olarak uygulamanın “1923” portundan çalışması için Program.cs’deki “CreateWebHostBuilder()” aşağıdaki gibi düzenlenir.
Program.cs:
1 2 3 4 |
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("http://localhost:1923") .UseStartup<Startup>(); |
Geldik bir makalenin daha sonuna. Bu makalede Custom Filterlar kullanılarak, minimum Code ile Maximum iş ilkesine bağlı kalınarak, bir mvc projede istenen Actionların total çalışma zamanı ölçülmüş ve bir Sql DB’ye yazılmıştır. Ölçülmek istenmeyen Actionlar, Custom bir Attribute([IgnoreAction]) ile işaretlenmiş ve ilgili Action Filter’da(TimerFilter) işleme tabi tutulmamışlardır. Ölçüm yapılan tüm Actionlar, bir Raporlama ekranında Google Chart ile hareketli grafikler halinde gösterilmiştir. Ayrıca yine istenen Action’ın detayına tıklanılması sureti ile, Detail sayfasında son 10 kaydı olacak şekilde gösterilmiştir.
İstenir ise bu sistem, belli bir Rule yapısı çalıştırılarak, örneğin çalışma süresi 3sn den fazla Actionlar tespit edilerek, Admin’e mail veya Sms atan bir monitoring sisteme çevirilebilir.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source Code : https://github.com/borakasmer/MonitorActionExecutionTime
Source :
Son Yorumlar