Sizlerinde bildiği gibi hemen hemen her backend application’ının sahip olması gereken en temel özelliklerden birisi, request ve response‘ların log‘lanmasıdır. Özellikle çoklu API kullanımının söz konusu olduğu ortamlarda, “nice-to-have” durumundan “must-have” durumuna geçiş yapan temel bir özellik.
Çünkü öyle bir zaman geliyor ki, API‘ı call eden bir client’a, gönderdiği request’in hatalı parametreler içerdiğini gösterebilmeniz gerekiyor.
Kısacası, daha iyi bir “iz sürebilmek” adına API’larımıza kazandırmamız gereken bir özellik.
Hikaye
Kısa bir süre içerisinde, tüm API request ve response’larını log’lama gereksinimimiz oluştu. Evet, bunu gerçekleştirebilmenin bir çok yöntemi mevcut. Örneğin her API‘ın içerisinde bir middleware yazarak, request ve response’ları log’layabilmek gibi. Tabi hepsinin de ayrı bir tradeoff’u mevcut.
Bir middleware yazdık diyelim, hangi API’a kim implemente edecek? Performans’a kötü bir şekilde yan etki edecek mi? Tabi, en önemlisi de vakit.
Bu makale kapsamında ise, request ve response’ları log’layabilme gereksinimimizi “quick-win” bir çözüm olarak nasıl gerçekleştirdiğime değinmeye çalışacağım.
Peki
Çözümün daha net anlaşılabilmesi için öncelikle sistemimiz hakkında sizlere kısaca bir bilgi vermek istiyorum. Üzerinde çalıştığım proje, tamamen cloud-native ve microservice yaklaşımı ile design edilmiş bir sistem’e sahip. Application’lar Azure Kubernetes Service üzerinde ve tüm API‘larımızı Azure API Management vasıtasıyla yönetiyoruz.
Azure API Management, distributed olarak design ettiğimiz sistemde bizim için olmazsa olmaz parçalardan bir tanesi. Bu makale kapsamında, microservice mimarisinde API Gateway kullanımının faydalarının detayına çok fazla girmeyeceğim.
Fakat, API Gateway kullanımının bana “quick-win” bir çözüm üretebilmemi sağlayan noktalarından bir tanesine değinmek istiyorum.
API Gateway, sisteme “tek bir giriş noktası” ve uniform bir experience oluşturabilmemize olanak sağlamaktadır.
Dolayısıyla routing işleminin dışında, request ve response’lar üzerinde de istediğimiz manupilasyonları yapabilmemize olanak tanımaktadır.
Hal buyken, neden request ve response log’lama işlemlerini tek tek her bir API içerisinden gerçekleştireyim? Değil mi?
Bir çok API Gateway’in de sahip olduğu gibi, Azure API Management‘da bir logging policy’sine sahip. Bu policy’leri kullanarak, platform-independet ve merkezi bir logging işlemi gerçekleştirebilmek mümkün oluyor.
Peki, Nasıl?
Bu noktada, bir kaç farklı adımı sırasıyla izleyeceğiz:
- Azure Event Hub oluşturulması
- Azure API Management Logger oluşturulması
- Azure API Management Log Policy’sinin configure edilmesi
- Azure Event Hub triggered Azure Function oluşturulması ve GrayLog‘a log gönderilmesi
- Oluşturulan Azure Function‘ın deploy edilmesi
1. Azure Event Hub oluşturulması
Azure API Management‘da “Log to EventHub” isminde bir advanced policy’e sahibiz. Bu policy ile performance impact’ini minimize ederek (performance impact’ini de göz önünde bulundurmalıyım), tüm log’ları Event Hub‘a gönderebilir ve daha sonra istediğimiz gibi işleyebilir, kaydedebiliriz.
Eğer hali hazırda bir Event Hub kullanmıyorsanız, bir Event Hub namespace’i ve instance’ına ihtiyacınız var demektir.
Azure Event Hub saniyede milyonlarca event’i alıp, işleyebilme yeteneğine sahip bir event ingestion service’idir.
Hemen oluşturabilmek için buradaki adımları takip edebiliriniz.
Event Hub oluşturma adımlarını tamamladıktan sonra, Azure API Management içerisinden Event Hub‘ı kullanabilmemiz için bir shared access policy‘e ihtiyacımız var. Bu policy’i elde edebilmek için oluşturmuş olduğunuz Event Hub entity’sine tıklayarak detaylarına girelim.
Ardından yukarıdaki gibi sol tarafda bulunan “Shared access policies” sekmesine tıklayalım.
Şimdi policy tanımlayabilmek için “Add” butonuna basalım ve tanımlama işlemini gerçekleştirelim. Ben “AzureAPIManagement” isminde bir policy tanımladım. Oluşturma işleminin ardından, yukarıda gördüğümüz gibi Event Hub instance’ının connection string bilgilerine erişebileceğiz.
2. Azure API Management Logger oluşturulması
Event Hub adımlarını tamamladık. Şimdi ise Azure API Management içerisinde bir logger‘a ihtiyacımız var. Logger’ı oluşturma işlemini Azure API Management REST API‘ı üzerinden gerçekleştireceğiz.
Logger oluşturma işlemine başlamadan önce, Azure API Management servisindeki REST API hizmetini enable etmemiz gerekmektedir. Enable edebilmek için Azure API Management instance’ı içerisinden, aşağıdaki gibi “Management API” sekmesine girerek “Enable API Management REST API” checkbox’ını işaretlememiz yeterli olacaktır.
REST API‘ı enable ettikten sonra, aşağıdaki “Accesss token” bölümündeki “Generate” butonuna basarak bir access token elde edelim.
Artık bir logger oluşturabiliriz. Logger oluşturabilmek için aşağıdaki URL‘e bir HTTP PUT request’i göndermemiz gerekmektedir.
https://my-api-management. management.azure-api.net/loggers/my-logger?api-version=2018-01-01
NOT: {my-api-management} ve {my-logger} parametrelerini, kendi service name’leriniz ile değiştirmeyi unutmayın.
Header’a ise eklememiz gereken bir kaç parametre bulunmaktadır.
- Content-Type : application/json
- Authorization : “API Management REST API access token”
ve payload ise aşağıdaki gibi olmalı:
{ "loggerType" : "AzureEventHub", "description" : "My event hub logger", "credentials" : { "name" : "my-log-hub", "connectionString" : "Endpoint=my-log-hub sender connection string" } }
Event Hub logger artık kullanıma hazır. Buradaki “my-logger” olan logger name’ini not alalım. Daha sonra logger policy’sini tanımlarken kullanacağız.
3. Azure API Management Log Policy’sinin configure edilmesi
Logger policy’sini tanımlayabilmek için ilgili Azure API Management instance’ına girelim ve sol menüdeki “APIs” sekmesine tıklayalım. Ardından aşağıdaki gibi “All APIs” bölümüne geçelim.
Burada yapmak istediğimiz şey, tüm API’‘arımız için geçerli olacak bir Event Hub logger eklemek. Bunu yapabilmek için “inbound” ve “outbound” processing kısımlarına bir “Log to EventHub” policy’si ekliyeceğiz.
Policy ekleyebilmek için, sağ kısımdaki “Policies” başlığının yanında bulunan icon’a tıklayalım ve code view ekranına geçelim.
Şimdi “inbound” ve “outbound” tag’lerinin arasına, “Advanced Policies” altındaki “Log to EventHub” policy’sini ekleyelim ve aşağıdaki gibi düzenleyelim.
<policies> <inbound> <set-variable name="message-id" value="@(Guid.NewGuid())" /> <log-to-eventhub logger-id="my-logger" partition-id="1">@{ var requestLine = string.Format("{0} {1} HTTP/1.1\r\n", context.Request.Method, context.Request.Url.Path + context.Request.Url.QueryString); var body = context.Request.Body?.As<string>(true); if (body != null && body.Length > 1024) { body = body.Substring(0, 1024); } var headers = context.Request.Headers .Where(h => h.Key != "Authorization" && h.Key != "Ocp-Apim-Subscription-Key") .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value))) .ToArray<string>(); var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty; return "request:" + context.Variables["message-id"] + "\n" + requestLine + headerString + "\r\n" + body; }</log-to-eventhub> </inbound> <backend> <forward-request /> </backend> <outbound> <log-to-eventhub logger-id="my-logger" partition-id="1">@{ var statusLine = string.Format("HTTP/1.1 {0} {1}\r\n", context.Response.StatusCode, context.Response.StatusReason); var body = context.Response.Body?.As<string>(true); if (body != null && body.Length > 1024) { body = body.Substring(0, 1024); } var headers = context.Response.Headers .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value))) .ToArray<string>(); var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty; return "response:" + context.Variables["message-id"] + "\n" + statusLine + headerString + "\r\n" + body; }</log-to-eventhub> </outbound> <on-error /> </policies>
Burada dikkat etmemiz gereken nokta “logger-id” parametresi. Bu parametreye value olarak, Event Hub logger oluştururken belirlediğimiz logger name’ini set ediyoruz. Ayrıca “message-id” variable’ı ile de request ve response’ları correlate ederek, Event Hub‘a gönderiyoruz.
Bu arada, Azure API Management içerisinden C# based policy yazabilmek çok güzel, değil mi?
4. Azure Event Hub triggered Azure Function oluşturulması ve GrayLog’a log gönderilmesi
Event Hub üzerinde artık request ve response log’larını tutuyoruz. Bu log’ları Event Hub üzerinden consume edebilmek için farklı yöntemler mevcut. Ben ise bu noktada, herhangi başka bir ekstra işle uğraşmamak için, serverless compute olan Azure Functions‘ı tercih ettim.
Öncelikle Azure Function oluşturma işlemini, buradaki dokümantasyonu takip ederecek gerçekleştirelim.
Dokümantasyon içerisinde “Create an HTTP triggered function” kısmına geldiğinizde ise, “Change template filter” seçeneğine tıklayarak “All” opsiyonunu seçelim. Ardından yukarıdaki gibi “EventHubTrigger” opsiyonunu seçelim. Devamında ise function’a bir isim verelim (ben RequestLoggingFunction ismini kullandım) ve ilgili adımları takip ederek, function oluşturma işlemini tamamlayalım.
Artık bir Azure Function template’ine sahibiz.
Burada yapacağımız iki adet işlem var.
- Log’ları Event Hub üzerinden consume edeceğiz.
- Sonra log’ları GrayLog içerisine göndereceğiz (eğer siz farklı bir log sağlayıcısı kullanıyorsanız, onu entegre edebilirsiniz)
Biz hali hazırda GrayLog kullandığımız için, request ve response message log’larını da GrayLog içerisinde tutmaya karar verdim. Dilerseniz Moesif gibi farklı çözümlere de bir göz atabilirsiniz.
GrayLog kurulumu hakkında bilgi edinmek isterseniz, şu makaleme bir göz atabilirsiniz.
Öncelikle NLog‘un Gelf target paketini, NuGet üzerinden projeye aşağıdaki gibi ekleyelim.
dotnet add package NLog.Web.AspNetCore.Targets.Gelf
Ardından “Models” isminde bir klasör oluşturarak, aşağıdaki üç class’ı tanımlayalım.
public class HTTPRequestMessageLog { public string Method { get; set; } public Guid MessageId { get; set; } public string UrlPath { get; set; } public string Content { get; set; } }
public class HTTPResponseMessageLog { public Guid MessageId { get; set; } public string StatusCode { get; set; } public string Content { get; set; } }
NOT: Message log structure’ını, kendinize göre değiştirebilirsiniz.
public class HttpMessage { public Guid MessageId { get; set; } public bool IsRequest { get; set; } public HttpRequestMessage HttpRequestMessage { get; set; } public HttpResponseMessage HttpResponseMessage { get; set; } public static HttpMessage Parse(Stream stream) { using (var sr = new StreamReader(stream)) { return Parse(sr.ReadToEnd()); } } public static HttpMessage Parse(string data) { var httpMessage = new HttpMessage(); HttpContent content; using (var sr = new StringReader(data)) { // First line of data is (request|response) followed by a GUID to link request to response // Rest of data is in message/http format var firstLine = sr.ReadLine().Split(':'); if (firstLine.Length < 2) { throw new ArgumentException("Invalid formatted event :" + data); } httpMessage.IsRequest = firstLine[0] == "request"; httpMessage.MessageId = Guid.Parse(firstLine[1]); var stream = new MemoryStream(Encoding.UTF8.GetBytes(sr.ReadToEnd())); stream.Position = 0; content = new StreamContent(stream); } var contentType = new MediaTypeHeaderValue("application/http"); content.Headers.ContentType = contentType; if (httpMessage.IsRequest) { contentType.Parameters.Add(new NameValueHeaderValue("msgtype", "request")); // Using .Result isn't too evil because content is a locally buffered memory stream // Although if this were hosted in a System.Web based ASP.NET host it might block httpMessage.HttpRequestMessage = content.ReadAsHttpRequestMessageAsync().Result; } else { contentType.Parameters.Add(new NameValueHeaderValue("msgtype", "response")); httpMessage.HttpResponseMessage = content.ReadAsHttpResponseMessageAsync().Result; } return httpMessage; } }
Event Hub üzerinden consume edeceğimiz log data’sını, “HttpMessage” class’ı vasıtasıyla “HttpRequestMessage” ve “HttpResponseMessage” type’larına parse edeceğiz.
https://github.com/dgilling/ApimEventProcessor/blob/master/src/ApimEventProcessor/HttpMessage.cs
Function template’i ile gelen class’ı ise aşağıdaki gibi düzenleyelim.
public static class RequestLoggingFunction { private static NLog.ILogger logger = null; [FunctionName ("RequestLoggingFunction")] public static async Task Run ([EventHubTrigger ("my-log-hub", Connection = "EVENTHUB_CONNECTION")] EventData[] events, ILogger log, ExecutionContext context) { ConfigureLogger (context.FunctionAppDirectory); var exceptions = new List<Exception> (); foreach (EventData eventData in events) { try { string messageBody = Encoding.UTF8.GetString (eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count); var httpMessage = HttpMessage.Parse (messageBody); httpMessage. if (httpMessage.IsRequest) { var httpRequestMessageLog = new HTTPRequestMessageLog (); httpRequestMessageLog.MessageId = httpMessage.MessageId; httpRequestMessageLog.UrlPath = httpMessage.HttpRequestMessage.RequestUri.ToString (); httpRequestMessageLog.Method = httpMessage.HttpRequestMessage.Method.Method; httpRequestMessageLog.Content = await httpMessage.HttpRequestMessage.Content.ReadAsStringAsync (); logger.Info (JsonConvert.SerializeObject (httpRequestMessageLog)); } else { var httpResponseMessageLog = new HTTPResponseMessageLog (); httpResponseMessageLog.MessageId = httpMessage.MessageId; httpResponseMessageLog.StatusCode = httpMessage.HttpResponseMessage.StatusCode.ToString (); httpResponseMessageLog.Content = await httpMessage.HttpResponseMessage.Content.ReadAsStringAsync (); logger.Info (JsonConvert.SerializeObject (httpResponseMessageLog)); } await Task.Yield (); } catch (Exception e) { // We need to keep processing the rest of the batch - capture this exception and continue. // Also, consider capturing details of the message that failed processing so it can be processed again later. exceptions.Add (e); } } // Once processing of the batch is complete, if any messages in the batch failed processing throw an exception so that there is a record of the failure. if (exceptions.Count > 1) throw new AggregateException (exceptions); if (exceptions.Count == 1) throw exceptions.Single (); } private static void ConfigureLogger (string appDirectory) { if (logger == null) { string nlogConfigPath = Path.Combine (appDirectory, "nlog.config"); NLog.LogManager.Configuration = new XmlLoggingConfiguration (nlogConfigPath); logger = NLog.LogManager.GetCurrentClassLogger (); } } }
NOT: “EventHubTrigger” attribute’üne parametre olarak set ettiğim “my-log-hub” ve “EVENTHUB_CONNECTION” key’lerini düzenlemeyi unutmayın.
Function içerisinde kısaca neler yaptığımıza bir bakalım.
GrayLog‘a log gönderebilmek için NLog‘un logger’ını configure ettik. “Run” method’u içerisinde ise event’in parse edilmesinin ardından ilgili event type’ına göre, log’lama işlemini gerçekleştirdik.
Şimdi ise function’ın root dizini altında, “nlog.config” file’ını aşağıdaki gibi oluşturmamız gerekmektedir.
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwExceptions="false" internalLogLevel="Off"> <extensions> <add assembly="NLog.Web.AspNetCore"/> <add assembly="NLog.Web.AspNetCore.Targets.Gelf"/> </extensions> <targets> <target xsi:type="Gelf" name="graylog" endpoint="udp://localhost:12201" facility="APM.RequestLogging.Function" SendLastFormatParameter="true" /> </targets> <rules> <logger level="Info" writeTo="graylog" /> </rules> </nlog>
NOT: GrayLog “endpoint” ini de düzenlemeyi unutmayın.
5. Oluşturulan Azure Function‘ın deploy edilmesi
Artık oluşturduğumuz function’ı deploy etmeye hazırız. Deployment işlemini hızlıca gerçekleştirebilmek için, ilk olarak buradaki adımları takip edelim.
NOT: Azure üzerinde function oluştururken, pricing tier‘ını dikkatli seçmenizi öneririm. Ben tüm request ve response message’larını log’layacak function’ımı kotrol altında tutabilmek için “consumption” plan yerine, “app service” plan’ı tercih ettim.
Deployment işlemini tamamladıktan sonra, Event Hub‘ın connection string bilgisini Azure Portal üzerinden function’ın app settings bölümüne eklememiz gerekmektedir.
Öncelikle portal üzerinden “Function Apps” sekmesine girelim ve oluşturmuş olduğumuz function’a tıklayalım. Ardından “Platform features” sekmesine geçerek “Application settings” seçeneğine tıklayalım.
Ben function içerisindeki “Connection” key’ini “EVENTHUB_CONNECTION” adıyla tanımladığım için, app settings içerisine de aynı şekilde ekledim.
Artık hazırız. Eğer herhangi bir hata yapmadıysak, oluşturduğumuz function başarıyla çalışmaya başlayacaktır. Function’ın sorunsuz çalıştığını anlayabilmek için ise, yukarıdaki ekranda bulunan “Monitor” sekmesine girerek kontrol edebilirsiniz.
Örnek bir request log’u ise GrayLog içerisinden aşağıdaki gibi görünecektir.
Sonuç
Her ne kadar basit bir işlem gibi görünse de, request ve response message’larının log’lanması bir çok durum için önem arz etmektedir. Ayrıca bu işlemi çok fazla bir overhead yaratmadan ve Azure‘un bize sağlamış olduğu service’leri kullanarak, mevcut sistemimiz içerisinden nasıl gerçekleştirdiğimi göstermeye çalıştım.
Happy clouding!
Referanslar
https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-log-event-hubs
https://docs.microsoft.com/en-us/azure/api-management/api-management-log-to-eventhub-sample
https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-vs-code
Teşekkürler hocam