Skip to content

Custom HTTP client

FantasticFiasco edited this page Apr 10, 2022 · 6 revisions

The HTTP client is responsible for sending log events over the network to a receiving log server. The default client works in many circumstances, but some log servers might place certain requirements on the sender. Perhaps a certain HTTP header is required, or the log server is using a self signed certificate?

This sink is supporting these cases by letting you provide your own implementation of IHttpClient, defined below.

/// <summary>
/// Interface responsible for posting HTTP requests.
/// </summary>
public interface IHttpClient : IDisposable
{
    /// <summary>
    /// Configures the HTTP client.
    /// </summary>
    /// <param name="configuration">The application configuration properties.</param>
    void Configure(IConfiguration configuration);

    /// <summary>
    /// Sends a POST request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <param name="requestUri">The Uri the request is sent to.</param>
    /// <param name="contentStream">The stream containing the content of the request.</param>
    /// <returns>The task object representing the asynchronous operation.</returns>
    Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream);
}

Example: The basic authenticated HTTP client

Close your eyes and imagine we've got a log server requiring basic authentication. The default HTTP client does not support any authentication protocol, which would force us to implement a custom client. This is not difficult, what we have to do is to implement the interface IHttpClient and provide the implementation to the sink.

Lets start with the implementation. Below is the code required to authenticate the sender to the log server.

public class BasicAuthenticatedHttpClient : IHttpClient
{
  private readonly HttpClient httpClient;

  public BasicAuthenticatedHttpClient()
  {
    httpClient = new HttpClient();
  }

  public void Configure(IConfiguration configuration)
  {
    // The keys will probably have to be changed in case you copy/paste the class. The keys defined
    // here match the application settings defined below.
    var username = configuration["logServer:username"];
    var password = configuration["logServer:password"];

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Basic",
      Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
    }

  public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream)
  {
    using var content = new StreamContent(contentStream);
    content.Headers.Add("Content-Type", "application/json");

    var response = await httpClient
      .PostAsync(requestUri, content)
      .ConfigureAwait(false);

    return response;
  }

  public void Dispose() => httpClient?.Dispose();
}

The custom HTTP client is implemented, lets continue with the sink configuration.

Configure sink using source code

Lets assume you are defining the sink in source code. The following code will configure a non-durable HTTP sink to use a custom HTTP client.

var configuration = new ConfigurationBuilder()
  .AddJsonFile("appsettings.json")
  .Build();

ILogger logger = new LoggerConfiguration()
  .WriteTo.Http(
    requestUri: "https://www.mylogs.com",
    queueLimitBytes: null,
    httpClient: new BasicAuthenticatedHttpClient(),
    configuration: configuration)
  .CreateLogger();

The credentials required by BasicAuthenticatedHttpClient are stored in the application settings, in a file called appsettings.json.

{
  "logServer": {
    "username": "<username>",
    "password": "<password>"
  }
}

<username> and <password> are placeholders for your log server credentials. Their keys are logServer:username and logServer:password, and you might remember them because they are defined in BasicAuthenticatedHttpClient as the source for the credentials.

That's it! We now have a custom HTTP client that supports basic authentication and a sink configuration defined in source code. We're done here, it's time for coffee.

Configure sink using application settings

Lets assume you are defining the sink in application settings. The glue between Serilog and the application settings is a NuGet package called Serilog.Settings.Configuration. With the package added to your project you can write the following code to configure the sink.

var configuration = new ConfigurationBuilder()
  .AddJsonFile("appsettings.json")
  .Build();

var logger = new LoggerConfiguration()
  .ReadFrom.Configuration(configuration)
  .CreateLogger();

Serilog.Settings.Configuration will analyze the application settings, in this case a JSON file called appsettings.json, and create the sink accordingly.

Add the following JSON to appsettings.json in order to create a non-durable HTTP sink.

{
  "logServer": {
    "username": "<username>",
    "password": "<password>"
  },
  "Serilog": {
    "WriteTo": [
      {
        "Name": "Http",
        "Args": {
          "requestUri": "https://www.mylogs.com",
          "queueLimitBytes": null,
          "httpClient": "<namespace>.BasicAuthenticatedHttpClient, <assembly>"
        }
      }
    ]
  }
}

<username> and <password> are placeholders for your log server credentials. Their keys are logServer:username and logServer:password, and you might recognize them because they are defined in BasicAuthenticatedHttpClient as the source for the credentials.

<namespace> and <assembly> are placeholders for the namespace and assembly name, and together with the class name they uniquely identify the custom HTTP client.

That's it! We now have a custom HTTP client that supports basic authentication and a sink configuration defined using application settings. We're done here, it's time for coffee.