.Net Core Uygulama Takibi

Middleware yardımıyla talepleri takip edin

Adem Catamak
7 min readNov 6, 2019

Bu yazımızda geliştirdiğimiz uygulamalara gelen isteklerin takibini nasıl yapabileceğimize dair bir çalışma içerisinde bulunacağız.

İster monolith ister service oriented isterse microservice mimarisi ile yapımızı kuralım, her zaman süreçleri takip etmeye ihtiyaç duyuyoruz. Bir istek geldiği zaman, uygulamamız hangi aksiyonları yürütmüş, istek hangi aşamalardan geçmiş sorusunun cevabına sık sık ihtiyaç duyarız. Bu soruların cevapları üzerinden, uygulamamızı daha efektif hale getirebilir veya oluşmuş hataların sebeplerini bulup bunları düzeltebiliriz. Bu sebeple aksiyonların kayıt altına alınması (loglanması), uygulama geliştirme sürecinde önem verilmesi gereken adımlardan biridir.

Bu yazıda .Net Core üzerinde geliştirilmiş Web Api tipindeki servisimize gelen tüm isteklere bir etiket verecek ve bunlar üzerinden süreçlerin nasıl takip edilebileceğini inceleyeceğiz. Ekstra başlığı altında ise, örnek projemizin üzerinde birkaç ekleme yaparak, takip sistemimizin Web uygulaması olarak geliştirilmemiş projelerde de nasıl kullanabileceğimizi göreceğiz. Ayrıca bu yazıda HttpClient ve DelegatingHandler kullanarak, giden isteklere de bu etiketleri sağlayarak takip işlemlerinin sadece uygulama içinde değil, uygulamalar arasında da nasıl yapılabileceğine dair bir çalışma yürüteceğiz.

Başlarken

.Net Core 3.0 kullanarak Web Api taslağı üzerinden WebApplication adında bir proje açarak işe başlayalım. Bu yazıda oluşturduğumuz kodların tamamına bu link aracılığı ile ulaşabilirsiniz.

.Net Core Middleware

.Net Core üzerinde Middleware olarak adlandırılan bir ara katman tasarlanabilmektedir. Bu ara katman iş hattı üzerinde yer alan yapılardır. İstekler geldiği zaman bu katmanlar üzerinden geçerek Controller üzerine erişmekte veya erişmemektedir. Bu tipten bir sınıfa istek geldiğinde, ister bir sonraki sınıfa geçmeden önce istersek de bir sonraki ara bağlaşım sınıfından geldikten sonra neler yapabileceğimizi gerçekleyebiliriz Daha detaylı bilgi için tıklayınız.

Biz de bu ara katmanlardan yararlanarak, gelen her istek için yürütülecek aksiyonları bu katmanlarda gerçekleştireceğiz.

.Net Core IHttpClientFactory, IHttpContextAccessor

.Net Core öncesinde de HttpClient sınıfı vardı. Kullanımının kolay olduğu kadar, doğru kullanımı zor olan bir yapı sunmaktaydı. .Net Core 2.1 ve sonrası için elden geçirilerek kullanım kolaylığı sağlamak için bir çok geliştirme yapıldı. Yeni yapı aracılığı ile, bağlanmak istediğimiz farklı servisler için HttpClient objesini merkezi bir noktadan yönetebileceğimiz araçlar bize sunuldu. Bu yapılar hakkında da daha sonra detaylı bir inceleme yapabiliriz. Microsoft dokümanları üzerinden daha detaylı bilgi edinebilirsiniz.

.Net Core Middleware İmplemetasyonu

TraceIdMiddleware

İlk adımda TraceIdMiddleware adında bir sınıf oluşturacağız. Bu sınıf gelen istekler üzerinde bir trace-id olup olmadığını kontrol edecek. Eğer varsa biz de bu trace-id ile işlemlerimize devam edeceğiz. Eğer böyle bir veri sağlanmamışsa, kendimiz bir takip numarası oluşturacağız. Elimizde nihayetinde bir takip numarası olduğu anda da bu değeri HttpContext üzerindeki TraceIdentifier alanına yerleştireceğiz.

Ayrıca middleware sınıfımıza geçilen ayarlara göre, bu verilerin response objesi üzerinden geri gönderilip gönderilmeyeceğine de bu sınıf üzerinde karar vereceğiz. Bu kararları yönetebilmek adına “TraceIdMiddlewareConfiguration” adında bir sınıf oluşturarak işe başlayabiliriz.

Yukarıdaki sınıf aracılığı ile bahsedilen ayarları ara katman sınıfına geçebiliriz. Makine adı ve takip numarası verilerinin okunacağı ve yazılacağı başlık adı ve cevap üzerinde bu alanların olup olmayacağı gibi ayarları buradan yapabiliriz.

Şimdi işi yapacak ara katmanımıza kodlamaya geçebiliriz.

Ara katmanımız artık kullanıma hazır durumda. Bu katmanı iş hattına dahil etmek için yapmamız gerekenlere geçebiliriz şimdi.

Startup sınıfımıza giderek, Configure metodu içerisinde app.UseMiddleware<TraceIdMiddleware>(); satırı eklememiz yeterli olacaktır.

Bu adımdan sonra uygulamamızı çalıştırıp internet tarayıcımız üzerinde yer alan “ağ” (network) sekmesinden gelen cevabı incelersek, “x-trace-id” ve “x-machine-name” başlıkları altında bazı verilerin yer aldığını görebiliriz.

Not: Eğer varsayılan ayarlar ile değil istediğiniz farklı ayarlar ile TraceIdMiddleware ara katmanını kullanmak isterseniz aşağıdaki gibi bir kod parçasını kullanabilirsiniz.

LogScopeMiddleware

Şimdi log işlemlerimiz için TraceIdentifier içerisinde yer alan verilerden nasıl yararlanacağımıza bir göz atalım.

Bunun için LogScopeMiddleware adında bir ara katman tanımlayacağız. Bu ara katmanın görevi; log sınıfımız üzerinde bir Scope oluşturarak, yürüteceğimiz kayıt işlemlerini yaparken sağladığımız takip verisini mesajın öncesine yerleştirmek olacak. Bu şekilde kayıt

Bu sınıfı ekledikten sonra Startup sınıfı üzerine gidip Configure metodunda değişiklik yapmayı unutmamalıyız.

app.UseMiddleware<TraceIdMiddleware>();
app.UseMiddleware<LogScopeMiddleware>();

Log Ayarları

Şimdi “WeatherForecastController” üzerindeki Get metodu içerisinde birkaç log işlemi yürütelim.

Log without Scope Info

Gördüğünüz üzere, mesajlarımız ekrana yazıldı, fakat takip edebilme adına yerleştirmek istediğimiz veriler görünmemektedir. Anlık olarak birçok isteğin gerçekleşeceği bir sistem içerisinde, mesajların arasındaki ilişkiyi çözemeyeceğiz. Yani bir isteğin yerine getirilmesi sırasında hangi mesajların peşi sıra geldiğini bu yapıdan çıkaramayacağız. Bu durumu çözebilmek adına appsettings.Development.json ve appsettings.json dosyalarına gidelim ve birkaç ayarlama yapalım.

"Console": {
"IncludeScopes": true
}

Bu ayarlamadan sonra bir kere daha uygulamamızı çalıştıralım.

Log with Scope Info

Bu durumda mesajların her birinin başında hangi Scope’a bağlı olduklarına dair bilgiler de yerleştirilmiş oldu.

Tüm bu mesajlar “Graylog”, “Kibana” vb. merkezi log depoları (central log storage) üzerinde saklanması ve sorgulanması durumunda süreçleri ve özel olarak bir isteğin hangi aksiyonlara sebep olduğunu kolaylıkla inceleyebiliriz.

.Net Core IHttpClientFactory Kullanımı ile Giden İsteklere TraceId bilgisini geçmek

Bu kısım projemizde yeni bir Controller tanımlayacağız. Daha sonra bir proxy katmanı oluşturacağız. Proxy katmanımızda tanımlayacağımız bir HttpClient olacak ve aslında bu client yine kendi uygulamamıza istek gönderen bir sınıf olacak. Bunu yapmamızdaki amacımız; uygulamamızdan dışarı çıkan isteklerin üzerine, elimizde var olan takip bilgilerini nasıl yerleştirebileceğimizi görmek olacak. Bir diğer kazanımımız da şu olacak ki, dışarıdan gelen isteklerin üzerinde TraceId bilgisi olması halinde uygulamamız bu veriyi doğru bir şekilde algılayabilecek ve kullanabilecek mi? Birden fazla test senaryosunu incelemiş olacağız.

Öncelikle bir CityController adında bir sınıf oluşturalım.

Bu Controller üzerine istek geldiği zaman elimizde var olan şehirlerden biri rastgele seçilerek geri dönülecektir.

Şimdi yeni hazırladığımız hizmet noktasına (endpoint) istekleri gönderecek bir sınıf tasarlayalım. Bu sınıfı tasarlamada aşamasında ihtiyacımızı karşılayacak ara yüzü tanımlayarak başlıyalım.

Şimdi de ihtiyacımızı karşılayacak ara yüzün implementasyonunu CityHttpClient adlı sınıfımızda yapalım.

Bu sınıf sistemimize dahil etmek için Startup sınıfımıza giderek, ConfigureServices metodu içerisine aşağıdaki gibi bir blok ekliyoruz.

Ardından da “WeatherForecastController” üzerinden ICityClient interface’inin bir örneğini elde ederek kullanımını sağlayacağız.

Bu şekilde uygulama çalıştırıldığı zaman aşağıdaki gibi bir çıktı ile karşılaşacağız. İlk ve son kaydı incelersek, isteklerin TraceId değerlerinin farklı olduğunu göreceğiz. Bunun sebebi, istek gönderirken elimizdeki TraceId verisini isteğin içerisine koymamızdır.

Multiple TraceId

Bu durumu aşmak için ise “HttpClientTraceIdInterceptor”, “HttpClientTraceIdInterceptorConfiguration” sınıflarını oluşturalım.

HttpClientTraceIdInterceptorConfiguration sınıfı ile oluşturacağımız interceptor sınıfı üzerinde yapılması gereken ayarları yöneebileceğiz. Daha önce “TraceIdMiddleware” sınıfı üzerinde yaptığımıza benzer bir işlem yürütüyor olacağız.

“TraceIdMiddleware” sınıfımıza benzer bir şekilde, bir iş hattı olarak düşünebiliriz bu sınıfı. İstekler sistemden çıkmadan önce “HttpClientTraceIdInterceptor” sınıfımızın SendAsync metoduna gelecekler. Biz de istek gitmeden önce veya gittikten sonra yürütmek istediğimiz işlemler varsa burada yürütüyor olacağız.

Bizim ihtiyacımız istek gitmeden önce, isteğin içerisine kendi TraceId verimizi yerleştirmek olduğu için, base.SendAync(…) metodundan öncesine dilediğimiz aksiyonları yerleştiriyoruz.

Not: yukarıda geliştirmiş olduğumuz DelegatingHandler sınıfımız IHttpContextAccessor bağımlılığı sebebiyle Web uygulamalarımızın dışında kullanıma uygun değildir. Ekstra kısmında bu durumun da nasıl üstesinden gelinebileceğine dair bilgiler bulabilirsiniz.

Bu sınıfın implementasyonunu da tamamladıktan sonra Startup sınıfımıza geri dönüp, ConfigureServices metodumuzda, implemente ettiğimiz Interceptor sınıfımızı sisteme dahil ediyor ve son olarak da yazdığımız CityClient sınıfımızın bu interceptoru kullanması gerektiğini belirtiyoruz. Şunu da unutmamak gerekiyor ki, yazdığımız DelegatingHandler IHttpContextAccessor tipine bağımlı olduğu için bu interface’i de DI container üzerine dahil etmemiz gerekecek.

Şimdi uygulamamızı tekrar çalıştırdığımız zaman aşağıdaki ekran görüntüsünde olduğu gibi, her iki isteğin üzerindeki ParentId isimli alanın başlangıç değerlerinin (yani TraceId) aynı olduğunu görebiliriz.

Single TraceId

Ekstra

Sadece TraceId ihtiyacınızı karşılamıyor olabilir veya HttpContext ile çalışmıyor olabilirsiniz. Örneğin, mesaj kuyruklarını izleyerek, size gelen her yeni mesajda işlemlerinizi yürüten bir arka plan servisi kurgulamış olabilirsiniz. Bu gibi durumlarda kullanılabilecek bir çözüme ihtiyaç duyuyorsanız bu kısmı okuyabilirsiniz.

Bu kısımda implemente edeceğimiz kod parçalarını aynı proje üzerinde farklı bir branch’de yazacağım. Bu link ile doğrudan yeni branch’e erişebilirsiniz.

Öncelikli olarak TraceId ile birlikte ihtiyaç duyabileceğim diğer verileri saklamak adına ITraceInfo interface ve bu arayüzü implement eden TraceInfo sınıfını hazırlıyorum. Örnek olması açısından MachineName adında bir alan ekledim, siz de ihtiyacınıza göre buraya yeni alanlar ekleyebilir veya çıkarabilirsiniz.

Bu sınıfı ekledikten sonra ise, bu sınıfın içerisindeki verilere farklı thread içerisinden de erişebilmek adına ITraceInfoAccessor ara yüzünü ve bu ara yüzün implementasyonunu yazıyorum.

Not: Doğrudan TraceInfo nesnesini “Scoped” olarak sisteme dahil etmemiz birkaç farklı kullanım senaryosunda sorun çıkarabilecektir. Bunlardan biri DelegatingHandler tipindeki interceptor sınıflarımızın, HttpClientFactory tarafından, farklı bir iş mantığı ile (uygulamamızın daha etkin çalışması için) yaşam döngüsünü yönetiyor olması. Scoped olarak eklenmiş olan TraceInfo sınıfınızın bir objesine doğrudan “HttpClientTraceIdInterceptor” üzerinden constructor injection ile almaya çalışabilirsiniz. Bu durumda TraceInfo objesinin içerisinin boş olduğunu görebilirisiniz ki bu durumda istediğimiz bir durum değil.

Bu adımdan sonra Startup sınıfını değiştirerek işe başlayabiliriz. Öncelikli olarak bir üst bloktaki kodun içerisinde yazılmış Dependency Injection extension metodunu kullanalım.

Not: Bu geliştirmenin sonucunda IHttpContextAccessor bağımlılığımızı ortadan kaldıracağız. Dilerseniz bu adım da AddHttpContextAccessor metodunun kullanımını da Startup sınıfınızdan silebilirsiniz.

Bu işlemin ardından, TraceIdMiddleware ve LogScopeMiddleware üzerinde değişikliklerimizi yapmaya başlayabiliriz.

— TraceIdMiddleware sınıfı içerisinde, Invoke metoduna ITraceInfoAccessor bağımlılığını ekleyelim. Ardından context objemizin TraceIdentifier alanı üzerinde değişiklik yaptığımız kod parçasının hemen altında, saklamak istediğim TraceInfo sınıfını oluşturup ITraceInfoAccessor üzerinden bu veriyi saklayalım.

— LogScopeMiddleware sınıfının da Invoke metodu içerisine ITraceInfoAccessor bağımlılığını ekleyelim ve scope başlatma noktasında TraceInfo içerisine sakladığımız verilerden yararlanalım. (Opsiyonel)

Tüm bu işlemlerden sonra son olarak da HttpClientTraceIdInterceptor sınıfımız üzerinde değişiklik yapalım. (Bu adımda yaptığımız hareket sayesinde IHttpContextAccessor bağımlılığımızı yok etmiş oluyoruz.)

Yaptığımız değişiklik, IHttpContextAccessor yerine ITraceInfoAccessor tipine bağımlılık yaratmak ile sonuçlandı. Bu durumun olumlu tarafı, artık HttpContext’e sahip olmayan, Batch-Job, Console App gibi uygulamalarda da, geliştirmiş olduğumuz Interceptor sınıfımızı kullanabiliriz.

--

--