Skip to content

Instantly share code, notes, and snippets.

@to11mtm
Last active February 19, 2025 01:10
Show Gist options
  • Save to11mtm/1631f8ac450834a98cc72ba97f175d06 to your computer and use it in GitHub Desktop.
Save to11mtm/1631f8ac450834a98cc72ba97f175d06 to your computer and use it in GitHub Desktop.
HttpClientAsyncLogger Impl via Jetbrains AI
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