From 90eea1ac7febedfc33e57258705d81f64991c496 Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat Date: Tue, 20 Jan 2026 11:09:13 +0700 Subject: [PATCH] Enhance CombinedErrorHandlerAndLoggingMiddleware with caching and improved token handling --- ...ombinedErrorHandlerAndLoggingMiddleware.cs | 230 ++++++++++++------ 1 file changed, 162 insertions(+), 68 deletions(-) diff --git a/BMA.EHR.Domain/Middlewares/CombinedErrorHandlerAndLoggingMiddleware.cs b/BMA.EHR.Domain/Middlewares/CombinedErrorHandlerAndLoggingMiddleware.cs index a9de25fc..5f5aa2af 100644 --- a/BMA.EHR.Domain/Middlewares/CombinedErrorHandlerAndLoggingMiddleware.cs +++ b/BMA.EHR.Domain/Middlewares/CombinedErrorHandlerAndLoggingMiddleware.cs @@ -18,6 +18,10 @@ namespace BMA.EHR.Domain.Middlewares { private readonly RequestDelegate _next; private readonly IConfiguration _configuration; + private static ElasticClient? _elasticClient; + private static readonly object _lock = new object(); + private static readonly Dictionary _profileCache = new(); + private static readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(10); private string Uri = ""; private string IndexFormat = ""; @@ -31,19 +35,28 @@ namespace BMA.EHR.Domain.Middlewares Uri = _configuration["ElasticConfiguration:Uri"] ?? "http://192.168.1.40:9200"; IndexFormat = _configuration["ElasticConfiguration:IndexFormat"] ?? "bma-ehr-log-index"; SystemName = _configuration["ElasticConfiguration:SystemName"] ?? "Unknown"; + + // สร้าง ElasticClient แค่ครั้งเดียว + if (_elasticClient == null) + { + lock (_lock) + { + if (_elasticClient == null) + { + var settings = new ConnectionSettings(new Uri(Uri)) + .DefaultIndex(IndexFormat) + .DisableDirectStreaming() // เพิ่มประสิทธิภาพ + .RequestTimeout(TimeSpan.FromSeconds(5)); // กำหนด timeout + _elasticClient = new ElasticClient(settings); + } + } + } } public async Task Invoke(HttpContext context) { - Console.WriteLine("=== CombinedErrorHandlerAndLoggingMiddleware Start ==="); - - var settings = new ConnectionSettings(new Uri(Uri)) - .DefaultIndex(IndexFormat); - var client = new ElasticClient(settings); - var startTime = DateTime.UtcNow; var stopwatch = Stopwatch.StartNew(); - string? responseBodyJson = null; string? requestBodyJson = null; Exception? caughtException = null; @@ -64,27 +77,15 @@ namespace BMA.EHR.Domain.Middlewares string keycloakId = Guid.Empty.ToString("D"); var token = context.Request.Headers["Authorization"]; GetProfileByKeycloakIdLocal? pf = null; + var tokenUserInfo = await ExtractTokenUserInfoAsync(token); - // ลองดึง keycloakId จาก JWT token ก่อน (ถ้ามี) - try - { - keycloakId = await ExtractKeycloakIdFromToken(token); - } - catch (Exception ex) - { - Console.WriteLine($"Error extracting keycloakId from token: {ex.Message}"); - } + // ดึง keycloakId จาก JWT token + keycloakId = tokenUserInfo.KeycloakId; - try + // ดึง profile จาก cache หรือ API + if (Guid.TryParse(keycloakId, out var parsedId) && parsedId != Guid.Empty) { - if (Guid.TryParse(keycloakId, out var parsedId) && parsedId != Guid.Empty) - { - pf = await GetProfileByKeycloakIdAsync(parsedId, token); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error getting profile: {ex.Message}"); + pf = await GetProfileWithCacheAsync(parsedId, token); } try @@ -103,17 +104,17 @@ namespace BMA.EHR.Domain.Middlewares Console.WriteLine($"Updated keycloakId from authenticated user: {keycloakId}"); // อัพเดต profile ด้วย keycloakId ที่ถูกต้อง - try - { - if (Guid.TryParse(keycloakId, out var parsedId)) - { - pf = await GetProfileByKeycloakIdAsync(parsedId, token); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error updating profile after authentication: {ex.Message}"); - } + // try + // { + // if (Guid.TryParse(keycloakId, out var parsedId)) + // { + // //pf = await GetProfileByKeycloakIdAsync(parsedId, token); + // } + // } + // catch (Exception ex) + // { + // Console.WriteLine($"Error updating profile after authentication: {ex.Message}"); + // } } } @@ -142,7 +143,19 @@ namespace BMA.EHR.Domain.Middlewares finally { stopwatch.Stop(); - await LogRequest(context, client, startTime, stopwatch, pf, keycloakId, requestBodyJson, memoryStream, caughtException); + + // ทำ logging แบบ fire-and-forget เพื่อไม่ block response + _ = Task.Run(async () => + { + try + { + await LogRequestAsync(context, _elasticClient!, startTime, stopwatch, pf, keycloakId, requestBodyJson, memoryStream, caughtException); + } + catch (Exception ex) + { + Console.WriteLine($"Background logging error: {ex.Message}"); + } + }); // เขียนข้อมูลกลับไปยัง original Response body if (memoryStream.Length > 0) @@ -379,7 +392,7 @@ namespace BMA.EHR.Domain.Middlewares } } - private async Task LogRequest(HttpContext context, ElasticClient client, DateTime startTime, Stopwatch stopwatch, + private async Task LogRequestAsync(HttpContext context, ElasticClient client, DateTime startTime, Stopwatch stopwatch, GetProfileByKeycloakIdLocal? pf, string keycloakId, string? requestBodyJson, MemoryStream memoryStream, Exception? caughtException) { try @@ -399,7 +412,7 @@ namespace BMA.EHR.Domain.Middlewares string? message = null; string? responseBodyJson = null; - // อ่านข้อมูลจาก Response + // อ่านข้อมูลจาก Response (ลด serialization ที่ซ้ำ) if (memoryStream.Length > 0) { memoryStream.Seek(0, SeekOrigin.Begin); @@ -407,7 +420,7 @@ namespace BMA.EHR.Domain.Middlewares if (!string.IsNullOrEmpty(responseBody)) { - var contentType = context.Response.ContentType; + var contentType = context.Response.ContentType ?? ""; var isFileResponse = !contentType.StartsWith("application/json") && !contentType.StartsWith("text/html") && ( contentType.StartsWith("application/") || contentType.StartsWith("image/") || @@ -422,33 +435,23 @@ namespace BMA.EHR.Domain.Middlewares } else { + // ใช้ response body ที่มีอยู่แล้วโดยไม่ serialize ซ้ำ + responseBodyJson = responseBody; + try { - var jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = true, - Converters = { new DateTimeFixConverter() } - }; - responseBodyJson = JsonSerializer.Serialize(JsonSerializer.Deserialize(responseBody), jsonOptions); - var json = JsonSerializer.Deserialize(responseBody); if (json.ValueKind == JsonValueKind.Array) { message = "success"; } - else + else if (json.TryGetProperty("message", out var messageElement)) { - if (json.TryGetProperty("message", out var messageElement)) - { - message = messageElement.GetString(); - } + message = messageElement.GetString(); } } catch { - responseBodyJson = responseBody; message = caughtException?.Message ?? "Unknown error"; } } @@ -536,11 +539,19 @@ namespace BMA.EHR.Domain.Middlewares private async Task ExtractKeycloakIdFromToken(string? authorizationHeader) { + var tokenInfo = await ExtractTokenUserInfoAsync(authorizationHeader); + return tokenInfo.KeycloakId; + } + + private async Task ExtractTokenUserInfoAsync(string? authorizationHeader) + { + var defaultResult = new TokenUserInfo { KeycloakId = Guid.Empty.ToString("D") }; + try { if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer ")) { - return Guid.Empty.ToString("D"); + return defaultResult; } var token = authorizationHeader.Replace("Bearer ", ""); @@ -549,7 +560,7 @@ namespace BMA.EHR.Domain.Middlewares var parts = token.Split('.'); if (parts.Length != 3) { - return Guid.Empty.ToString("D"); + return defaultResult; } // Decode Base64Url payload (JWT uses Base64Url encoding, not standard Base64) @@ -570,31 +581,55 @@ namespace BMA.EHR.Domain.Middlewares Console.WriteLine($"JWT Payload: {payloadJson}"); - // Parse JSON และดึง sub (subject) claim + // Parse JSON และดึง claims ต่างๆ var jsonDoc = JsonDocument.Parse(payloadJson); + var result = new TokenUserInfo(); - // ลองหา keycloak ID ใน claims ต่างๆ - string? keycloakId = null; - + // ดึง keycloak ID if (jsonDoc.RootElement.TryGetProperty("sub", out var subElement)) { - keycloakId = subElement.GetString(); + result.KeycloakId = subElement.GetString() ?? Guid.Empty.ToString("D"); } else if (jsonDoc.RootElement.TryGetProperty("nameid", out var nameidElement)) { - keycloakId = nameidElement.GetString(); + result.KeycloakId = nameidElement.GetString() ?? Guid.Empty.ToString("D"); } else if (jsonDoc.RootElement.TryGetProperty("user_id", out var userIdElement)) { - keycloakId = userIdElement.GetString(); + result.KeycloakId = userIdElement.GetString() ?? Guid.Empty.ToString("D"); + } + else + { + result.KeycloakId = Guid.Empty.ToString("D"); } - return keycloakId ?? Guid.Empty.ToString("D"); + // ดึง preferred_username + if (jsonDoc.RootElement.TryGetProperty("preferred_username", out var preferredUsernameElement)) + { + result.PreferredUsername = preferredUsernameElement.GetString(); + Console.WriteLine($"Extracted preferred_username: {result.PreferredUsername}"); + } + + // ดึง given_name + if (jsonDoc.RootElement.TryGetProperty("given_name", out var givenNameElement)) + { + result.GivenName = givenNameElement.GetString(); + Console.WriteLine($"Extracted given_name: {result.GivenName}"); + } + + // ดึง family_name + if (jsonDoc.RootElement.TryGetProperty("family_name", out var familyNameElement)) + { + result.FamilyName = familyNameElement.GetString(); + Console.WriteLine($"Extracted family_name: {result.FamilyName}"); + } + + return result; } catch (Exception ex) { - Console.WriteLine($"Error extracting keycloak ID from token: {ex.Message}"); - return Guid.Empty.ToString("D"); + Console.WriteLine($"Error extracting token user info: {ex.Message}"); + return defaultResult; } } @@ -640,7 +675,58 @@ namespace BMA.EHR.Domain.Middlewares } catch { - throw; + return null; + } + } + + private async Task GetProfileWithCacheAsync(Guid keycloakId, string? accessToken) + { + var cacheKey = keycloakId.ToString(); + + // ตรวจสอบ cache + lock (_profileCache) + { + if (_profileCache.TryGetValue(cacheKey, out var cached)) + { + if (cached.ExpiryTime > DateTime.UtcNow) + { + return cached.Profile; + } + // ลบ cache ที่หมดอายุ + _profileCache.Remove(cacheKey); + } + } + + // ดึงข้อมูลจาก API + try + { + var profile = await GetProfileByKeycloakIdAsync(keycloakId, accessToken); + if (profile != null) + { + // เก็บใน cache + lock (_profileCache) + { + _profileCache[cacheKey] = (profile, DateTime.UtcNow.Add(_cacheExpiry)); + + // ลบ cache เก่าที่เกิน 1000 รายการ + if (_profileCache.Count > 1000) + { + var expiredKeys = _profileCache + .Where(x => x.Value.ExpiryTime < DateTime.UtcNow) + .Select(x => x.Key) + .ToList(); + foreach (var key in expiredKeys) + { + _profileCache.Remove(key); + } + } + } + } + return profile; + } + catch + { + return null; } } @@ -655,6 +741,14 @@ namespace BMA.EHR.Domain.Middlewares } // Model classes + public class TokenUserInfo + { + public string KeycloakId { get; set; } = string.Empty; + public string? PreferredUsername { get; set; } + public string? GivenName { get; set; } + public string? FamilyName { get; set; } + } + public class GetProfileByKeycloakIdLocal { public Guid Id { get; set; }