Last active
February 19, 2025 01:10
-
-
Save to11mtm/1631f8ac450834a98cc72ba97f175d06 to your computer and use it in GitHub Desktop.
HttpClientAsyncLogger Impl via Jetbrains AI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Net.Http; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.Extensions.Http.Logging; | |
using Microsoft.Extensions.Logging; | |
namespace httplogging | |
{ | |
public class HttpLoggingProvider : IHttpClientAsyncLogger | |
{ | |
private readonly ILogger<HttpLoggingProvider> _logger; | |
public HttpLoggingProvider(ILogger<HttpLoggingProvider> logger) | |
{ | |
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |
} | |
public object LogRequestStart(HttpRequestMessage request) | |
{ | |
var startTime = DateTime.UtcNow; | |
_logger.LogInformation("Request started: {Method} {Uri}", request.Method, request.RequestUri); | |
return startTime; | |
} | |
public async ValueTask<object> LogRequestStartAsync(HttpRequestMessage request, | |
CancellationToken cancellationToken = default) | |
{ | |
var startTime = DateTime.UtcNow; | |
_logger.LogInformation("Request started: {Method} {Uri}", request.Method, request.RequestUri); | |
return startTime; | |
} | |
public void LogRequestStop(object context, HttpRequestMessage request, HttpResponseMessage response, | |
TimeSpan elapsed) | |
{ | |
_logger.LogInformation( | |
"Request completed: {Method} {Uri} in {ElapsedMilliseconds} ms, Status: {StatusCode}", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds, response.StatusCode); | |
} | |
public async ValueTask LogRequestStopAsync(object context, HttpRequestMessage request, | |
HttpResponseMessage response, TimeSpan elapsed, CancellationToken cancellationToken = default) | |
{ | |
_logger.LogInformation( | |
"Request completed: {Method} {Uri} in {ElapsedMilliseconds} ms, Status: {StatusCode}", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds, response.StatusCode); | |
} | |
public void LogRequestFailed(object context, HttpRequestMessage request, HttpResponseMessage response, | |
Exception exception, TimeSpan elapsed) | |
{ | |
if (response != null) | |
{ | |
var responseBody = ReadResponseBodyAsync(request, response).Result; | |
_logger.LogError(exception, | |
"!!!SYNC!!! Request failed: {Method} {Uri} in {ElapsedMilliseconds} ms. Status: {StatusCode}. Response Body: {ResponseBody}", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds, response.StatusCode, responseBody); | |
} | |
else | |
{ | |
_logger.LogError(exception, | |
"Request failed: {Method} {Uri} in {ElapsedMilliseconds} ms. No response available.", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds); | |
} | |
} | |
public async ValueTask LogRequestFailedAsync(object context, HttpRequestMessage request, | |
HttpResponseMessage response, Exception exception, TimeSpan elapsed, | |
CancellationToken cancellationToken = default) | |
{ | |
if (response != null) | |
{ | |
var responseBody = await ReadResponseBodyAsync(request, response, cancellationToken); | |
_logger.LogError(exception, | |
"Request failed: {Method} {Uri} in {ElapsedMilliseconds} ms. Status: {StatusCode}. Response Body: {ResponseBody}", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds, response.StatusCode, responseBody); | |
} | |
else | |
{ | |
_logger.LogError(exception, | |
"Request failed: {Method} {Uri} in {ElapsedMilliseconds} ms. No response available.", | |
request.Method, request.RequestUri, elapsed.TotalMilliseconds); | |
} | |
} | |
// Helper to read response body Asynchronously | |
private async ValueTask<string> ReadResponseBodyAsync( | |
HttpRequestMessage request, | |
HttpResponseMessage response, | |
CancellationToken cancellationToken = default) | |
{ | |
try | |
{ | |
var content = await response.Content.ReadAsStringAsync(); | |
return TruncateContent(content); | |
} | |
catch (Exception ex) | |
{ | |
_logger.LogWarning(ex, "Failed to read response body for {Method} {Uri}", request.Method, | |
request.RequestUri); | |
return "Unable to read response body"; | |
} | |
} | |
// To avoid excessive logs, truncate response content if too long | |
private string TruncateContent(string content, int maxLength = 1000) | |
{ | |
if (!string.IsNullOrWhiteSpace(content) && content.Length > maxLength) | |
{ | |
return content.Substring(0, maxLength) + "...(truncated)"; | |
} | |
return content; | |
} | |
} | |
public interface ITokenProvider | |
{ | |
Task<string> GetAccessTokenAsync(string resource); | |
} | |
public class BearerTokenHandler : DelegatingHandler | |
{ | |
private readonly ITokenProvider _tokenProvider; | |
private readonly string _resource; | |
private string _cachedToken; | |
private DateTime _tokenExpiry; | |
private static readonly SemaphoreSlim TokenLock = new SemaphoreSlim(1, 1); // Prevent concurrent token refresh. | |
public BearerTokenHandler(ITokenProvider tokenProvider, string resource) | |
{ | |
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider)); | |
_resource = resource ?? throw new ArgumentNullException(nameof(resource)); | |
} | |
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | |
CancellationToken cancellationToken) | |
{ | |
// Ensure a valid token is available | |
if (!request.Headers.Contains("Authorization")) | |
{ | |
string token = await GetTokenAsync(cancellationToken); | |
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); | |
} | |
return await base.SendAsync(request, cancellationToken); | |
} | |
private async Task<string> GetTokenAsync(CancellationToken cancellationToken) | |
{ | |
// Acquire lock to prevent concurrent token refresh | |
await TokenLock.WaitAsync(cancellationToken); | |
try | |
{ | |
// Check if a cached token is still valid (greater than 5 seconds remaining) | |
if (_cachedToken != null && DateTime.UtcNow < _tokenExpiry.AddSeconds(-5)) | |
{ | |
return _cachedToken; | |
} | |
// Fetch a new token | |
(_cachedToken, _tokenExpiry) = await FetchTokenAsync(cancellationToken); | |
return _cachedToken; | |
} | |
finally | |
{ | |
TokenLock.Release(); | |
} | |
} | |
private async Task<(string Token, DateTime Expiry)> FetchTokenAsync(CancellationToken cancellationToken) | |
{ | |
string token = await _tokenProvider.GetAccessTokenAsync(_resource); | |
// Extract expiration from token payload (assumes JWT structure if applicable) | |
// Example logic; adapt based on your token provider | |
DateTime expiry = ExtractExpiryFromJwt(token); | |
return (token, expiry); | |
} | |
private DateTime ExtractExpiryFromJwt(string jwtToken) | |
{ | |
// Decode JWT and extract "exp" (expiration) claim, if applicable. | |
var tokenParts = jwtToken.Split('.'); | |
if (tokenParts.Length < 3) throw new ArgumentException("Invalid JWT token format."); | |
var payload = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>( | |
Convert.FromBase64String(tokenParts[1])); // Decode payload from Base64Url. | |
if (payload != null && payload.TryGetValue("exp", out var expValue)) | |
{ | |
// Convert "exp" to DateTime (assumes exp is in seconds since epoch) | |
var expSeconds = Convert.ToInt64(expValue); | |
return DateTimeOffset.FromUnixTimeSeconds(expSeconds).UtcDateTime; | |
} | |
throw new Exception("Token does not contain a valid 'exp' claim for expiration."); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment