Enhance CombinedErrorHandlerAndLoggingMiddleware with caching and improved token handling

This commit is contained in:
Suphonchai Phoonsawat 2026-01-20 11:09:13 +07:00
parent a463df5716
commit 90eea1ac7f

View file

@ -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<string, (GetProfileByKeycloakIdLocal Profile, DateTime ExpiryTime)> _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<object>(responseBody), jsonOptions);
var json = JsonSerializer.Deserialize<JsonElement>(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<string> ExtractKeycloakIdFromToken(string? authorizationHeader)
{
var tokenInfo = await ExtractTokenUserInfoAsync(authorizationHeader);
return tokenInfo.KeycloakId;
}
private async Task<TokenUserInfo> 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<GetProfileByKeycloakIdLocal?> 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; }