Skip to content

Commit

Permalink
feat(csharp): add logs and logger factory (#2640)
Browse files Browse the repository at this point in the history
  • Loading branch information
morganleroi authored Jan 31, 2024
1 parent 740ee74 commit e4ab486
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using Algolia.Search.Http;
using Algolia.Search.Models;
using Algolia.Search.Models.Common;
using Algolia.Search.Serializer;
using Algolia.Search.Transport;
using Algolia.Search.Utils;
using Microsoft.Extensions.Logging;

namespace Algolia.Search.Clients
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Algolia.Search.Http
{
Expand All @@ -17,10 +19,18 @@ internal class AlgoliaHttpRequester : IHttpRequester
/// https://docs.microsoft.com/en-gb/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client
/// </summary>
private readonly HttpClient _httpClient = new(
new TimeoutHandler
{
InnerHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }
});
new TimeoutHandler
{
InnerHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }
});

private readonly ILogger<AlgoliaHttpRequester> _logger;

public AlgoliaHttpRequester(ILoggerFactory loggerFactory)
{
var logger = loggerFactory ?? NullLoggerFactory.Instance;
_logger = logger.CreateLogger<AlgoliaHttpRequester>();
}

/// <summary>
/// Don't use it directly
Expand All @@ -31,8 +41,8 @@ internal class AlgoliaHttpRequester : IHttpRequester
/// <param name="connectTimeout">Connect timeout</param>
/// <param name="ct">Optional cancellation token</param>
/// <returns></returns>
public async Task<AlgoliaHttpResponse> SendRequestAsync(Request request, TimeSpan requestTimeout, TimeSpan connectTimeout,
CancellationToken ct = default)
public async Task<AlgoliaHttpResponse> SendRequestAsync(Request request, TimeSpan requestTimeout,
TimeSpan connectTimeout, CancellationToken ct = default)
{
if (request.Method == null)
{
Expand Down Expand Up @@ -63,14 +73,13 @@ public async Task<AlgoliaHttpResponse> SendRequestAsync(Request request, TimeSpa
try
{
using (httpRequestMessage)
using (HttpResponseMessage response =
await _httpClient.SendAsync(httpRequestMessage, ct).ConfigureAwait(false))
using (var response = await _httpClient.SendAsync(httpRequestMessage, ct).ConfigureAwait(false))
{
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
MemoryStream outputStream = new MemoryStream();
var outputStream = new MemoryStream();
await stream.CopyToAsync(outputStream).ConfigureAwait(false);
outputStream.Seek(0, SeekOrigin.Begin);

Expand All @@ -89,29 +98,35 @@ await _httpClient.SendAsync(httpRequestMessage, ct).ConfigureAwait(false))
}
}
}
catch (TimeoutException e)
catch (TimeoutException ex)
{
return new AlgoliaHttpResponse { IsTimedOut = true, Error = e.ToString() };
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Timeout while sending request");
}

return new AlgoliaHttpResponse { IsTimedOut = true, Error = ex.ToString() };
}
catch (HttpRequestException e)
catch (HttpRequestException ex)
{
// HttpRequestException is thrown when an underlying issue happened such as
// network connectivity, DNS failure, server certificate validation.
return new AlgoliaHttpResponse { IsNetworkError = true, Error = e.Message };
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Error while sending request");
}

return new AlgoliaHttpResponse { IsNetworkError = true, Error = ex.Message };
}
}

private async Task<string> StreamToStringAsync(Stream stream)
{
string content;

if (stream == null)
return null;

using (var sr = new StreamReader(stream))
{
content = await sr.ReadToEndAsync().ConfigureAwait(false);
}
using var sr = new StreamReader(stream);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);

return content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
//
// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT.
//

using System;
using System.Collections;
using System.Collections.Generic;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;

namespace Algolia.Search.Http
{
Expand Down Expand Up @@ -44,8 +45,7 @@ private static Dictionary<string, string> SplitQuery(string query)
/// <param name="ct"></param>
/// <returns></returns>
public Task<AlgoliaHttpResponse> SendRequestAsync(Request request, TimeSpan requestTimeout,
TimeSpan connectTimeout,
CancellationToken ct = default)
TimeSpan connectTimeout, CancellationToken ct = default)
{
string body = null;
if (!_bodyAsStream && request.Body != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
using System.Threading.Tasks;
using Algolia.Search.Exceptions;
using Algolia.Search.Models.Common;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Algolia.Search.Serializer;

internal class DefaultJsonSerializer
internal class DefaultJsonSerializer : ISerializer
{
private readonly JsonSerializerSettings _serializerSettings;
private readonly ILogger<DefaultJsonSerializer> _logger;

public DefaultJsonSerializer(JsonSerializerSettings serializerSettings)
public DefaultJsonSerializer(JsonSerializerSettings serializerSettings, ILoggerFactory logger)
{
_serializerSettings = serializerSettings;
_logger = logger.CreateLogger<DefaultJsonSerializer>();
}

/// <summary>
Expand Down Expand Up @@ -79,9 +82,14 @@ private async Task<object> Deserialize(Stream response, Type type)
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject(text, type, _serializerSettings);
}
catch (Exception e)
catch (Exception ex)
{
throw new AlgoliaException(e.Message);
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.Log(LogLevel.Debug, ex, "Error while deserializing response");
}

throw new AlgoliaException(ex.Message);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Threading.Tasks;
using Algolia.Search.Models.Common;

namespace Algolia.Search.Serializer
Expand All @@ -12,17 +13,14 @@ internal interface ISerializer
/// Converts the value of a specified type into a JSON string.
/// </summary>
/// <param name="data">The value to convert and write.</param>
/// <param name="stream">The Stream containing the data to read.</param>
/// <param name="compressionType">How the stream should be compressed <see cref="CompressionType"/></param>
/// <typeparam name="T">The type of the value to convert.</typeparam>
void Serialize<T>(T data, Stream stream, CompressionType compressionType);
string Serialize(object data);

/// <summary>
/// Parses the stream into an instance of a specified type.
/// </summary>
/// <param name="stream">The Stream containing the data to read.</param>
/// <typeparam name="T">The type of the value to convert.</typeparam>
/// <returns></returns>
T Deserialize<T>(Stream stream);
Task<T> Deserialize<T>(Stream stream);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Text;
Expand All @@ -10,9 +9,9 @@
using Algolia.Search.Clients;
using Algolia.Search.Exceptions;
using Algolia.Search.Http;
using Algolia.Search.Models.Common;
using Algolia.Search.Serializer;
using Algolia.Search.Utils;
using Microsoft.Extensions.Logging;

namespace Algolia.Search.Transport;

Expand All @@ -23,10 +22,11 @@ namespace Algolia.Search.Transport;
internal class HttpTransport
{
private readonly IHttpRequester _httpClient;
private readonly DefaultJsonSerializer _serializer = new(JsonConfig.AlgoliaJsonSerializerSettings);
private readonly ISerializer _serializer;
private readonly RetryStrategy _retryStrategy;
private readonly AlgoliaConfig _algoliaConfig;
private string _errorMessage;
private readonly ILogger<HttpTransport> _logger;

private class VoidResult
{
Expand All @@ -37,11 +37,14 @@ private class VoidResult
/// </summary>
/// <param name="config">Algolia Config</param>
/// <param name="httpClient">An implementation of http requester <see cref="IHttpRequester"/> </param>
public HttpTransport(AlgoliaConfig config, IHttpRequester httpClient)
/// <param name="loggerFactory">Logger factory</param>
public HttpTransport(AlgoliaConfig config, IHttpRequester httpClient, ILoggerFactory loggerFactory)
{
_algoliaConfig = config ?? throw new ArgumentNullException(nameof(config));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_retryStrategy = new RetryStrategy(config);
_serializer = new DefaultJsonSerializer(JsonConfig.AlgoliaJsonSerializerSettings, loggerFactory);
_logger = loggerFactory.CreateLogger<HttpTransport>();
}

/// <summary>
Expand Down Expand Up @@ -112,7 +115,7 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(HttpMethod metho

foreach (var host in _retryStrategy.GetTryableHost(callType))
{
request.Body = CreateRequestContent(requestOptions?.Data, request.CanCompress);
request.Body = CreateRequestContent(requestOptions?.Data, request.CanCompress, _logger);
request.Uri = BuildUri(host.Url, uri, requestOptions?.CustomPathParameters, requestOptions?.PathParameters,
requestOptions?.QueryParameters);
var requestTimeout =
Expand All @@ -123,6 +126,11 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(HttpMethod metho
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
}

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace("Sending request to {Method} {Uri}", request.Method, request.Uri);
}

var response = await _httpClient
.SendRequestAsync(request, requestTimeout, _algoliaConfig.ConnectTimeout ?? Defaults.ConnectTimeout, ct)
.ConfigureAwait(false);
Expand All @@ -137,16 +145,51 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(HttpMethod metho
return new VoidResult() as TResult;
}

return await _serializer.Deserialize<TResult>(response.Body);
if (_logger.IsEnabled(LogLevel.Trace))
{
var reader = new StreamReader(response.Body);
var json = await reader.ReadToEndAsync().ConfigureAwait(false);
_logger.LogTrace(
"Response HTTP {HttpCode}: {Json}", response.HttpStatusCode, json);
response.Body.Seek(0, SeekOrigin.Begin);
}

var deserialized = await _serializer.Deserialize<TResult>(response.Body);

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace("Object created: {objectCreated}", deserialized);
}

return deserialized;
case RetryOutcomeType.Retry:
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"Retrying ... Retryable error for response HTTP {HttpCode} : {Error}", response.HttpStatusCode,
response.Error);
}

continue;
case RetryOutcomeType.Failure:
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"Retry strategy with failure outcome. Response HTTP{HttpCode} : {Error}", response.HttpStatusCode,
response.Error);
}

throw new AlgoliaApiException(response.Error, response.HttpStatusCode);
default:
throw new ArgumentOutOfRangeException();
}
}

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retry strategy failed: {ErrorMessage}", _errorMessage);
}

throw new AlgoliaUnreachableHostException("RetryStrategy failed to connect to Algolia. Reason: " + _errorMessage);
}

Expand All @@ -155,11 +198,19 @@ private async Task<TResult> ExecuteRequestAsync<TResult, TData>(HttpMethod metho
/// </summary>
/// <param name="data">Data to send</param>
/// <param name="compress">Whether the stream should be compressed or not</param>
/// <param name="logger">Logger</param>
/// <typeparam name="T">Type of the data to send/retrieve</typeparam>
/// <returns></returns>
private MemoryStream CreateRequestContent<T>(T data, bool compress)
private MemoryStream CreateRequestContent<T>(T data, bool compress, ILogger logger)
{
return data == null ? null : Compression.CreateStream(_serializer.Serialize(data), compress);
var serializedData = _serializer.Serialize(data);

if (_logger.IsEnabled(LogLevel.Trace))
{
logger.LogTrace("Serialized request data: {Json}", serializedData);
}

return data == null ? null : Compression.CreateStream(serializedData, compress);
}

/// <summary>
Expand Down
Loading

0 comments on commit e4ab486

Please sign in to comment.