Logging Flurl Http Calls with Serilog in .NET Core
By: Huang Donovan
Tags: Web Development, Programming, Csharp, Aspnetcore, Dotnet

Photo by Guilherme Vasconcelos on Unsplash

Application servers often have network dependencies to a 3rd party services. Http is probably the most common protocol used for these sort of server to server interactions.

Naturally, having telemetry of the data going in and out of your application server is vital for diagnostics, troubleshooting and audit purposes.

As logging code is normally a cross cutting concern, it is good to introduce aspect oriented programming to avoid weaving in noisy logging code all over your code base. This article explores setting up Serilog to automatically log all http calls and their corresponding responses.

1. Install the packages

Serilog — The primary logging framework

Serilog.Formatting.Compact — A simple json formatter for our logs

Serilog.Sinks.Console — A sink for serilog to output logs to the console

Serilog.Sinks.AzureEventHub — A sink for serilog to output logs to an AzureEventHub instance

Flurl.Http — A popular http client library

*A note on serilog sinks. For this example, we will be configuring Serilog to use the console sink for local dev environment and for environments above local dev, AzureEventHub.

There is a wide range of sinks available here. Which sink you eventually use will probably be influenced by a wider solution architecture and availability of resources to support your target sink.

2. ISerilogFactory

Serilog.ILogger is the primary interface used for logging events. We will be introducing an ISerilogFactory interface to configure and instantiate an ILogger object.

using Serilog;

using Serilog.Formatting.Compact;

using Serilog.Sinks.SystemConsole.Themes; public interface ISerilogFactory

{

ILogger Build();

}

The implementation of ISerilogFactory is highly tied to which Serilog sinks are used and how you would like to configure Serilog. Thus, your version of SerilogFactory.cs will probably be different from this example.

using Serilog;

using Serilog.Formatting.Compact;

using Serilog.Sinks.SystemConsole.Themes; public class SerilogFactory : ISerilogFactory

{

private readonly bool _writeToConsole;

private readonly string _eventHubConnectionString;

private readonly string _eventHubEntity;

private readonly ILogger _logger;



public SerilogFactory(

bool writeToConsole,

string eventHubConnString,

string eventHubEntity)

{

this._writeToConsole = writeToConsole;

this._eventHubConnectionString = eventHubConnString;

this._eventHubEntity = eventHubEntity;



var loggerConfig = new LoggerConfiguration(); // modify the behaviour of your logger in this region

// in accordance to your sinks and log strateg



#region configuration

if (this._writeToConsole)

{

loggerConfig.WriteTo.Console(

theme: AnsiConsoleTheme.Code);

}



if (!this._writeToConsole &&

!string.IsNullOrEmpty(this._eventHubConnectionString) &&

!string.IsNullOrEmpty(this._eventHubEntity))

{

loggerConfig.WriteTo.AzureEventHub(

new CompactJsonFormatter(),

this._eventHubConnectionString,

this._eventHubEntity);

}

#endregion // singleton object assignment

this._logger = loggerConfig.CreateLogger();

}



public ILogger Build()

{

return this._logger;

}

}

3. IFlurlTelemetryLogger

Flurl is handy library that is built on top of .NET’s HttpClient and related classes. Flurl offers various levels of abstractions while still offering a good level of control over it’s behavior.

HttpCall is the class used by Flurl to encapulate the details of every http call it makes.

using Flurl.Http; public interface IFlurlTelemetryLogger

{

Task Log(HttpCall call);

}

HttpTelemetry.cs

Before we get to the implementation, create our own telemetry class to capture the aspects of the http call we are interested in. For brevity, this example is only going to capture a few essential details. You will probably want to modify this class according to your own requirements.

public class HttpTelemetry

{

public string AspNetCoreEnvironment { get; set; }

public string Endpoint { get; set; }

public bool Succeeded { get; set; }

public string RequestBody { get; set; }

public int? HttpStatusCode { get; set; }

public string ResponseBody { get; set; }

}

The constructor of FlurlClientTelemetryLogger takes in the ISerilogFactory and calls .Build() to give us a singleton ILogger object.

It also accepts an additional ‘env’ parameter used to identify which environment a particular log originated from.

You may prefer to modify your constructor with different parameters to augment your log with custom details not derived from HttpCall.

using Flurl.Http;

using Newtonsoft.Json;

using Serilog;

using System;

using System.Linq;

using System.Threading.Tasks; internal class FlurlTelemetryLogger : IFlurlTelemetryLogger

{

private readonly ILogger _logger;

private readonly string _aspNetCoreEnvironment;



internal FlurlClientTelemetryLogger(

ISerilogFactory factory,

string env)

{

this._logger = factory.Build();

this._aspNetCoreEnvironment = env;

}



public async Task Log(HttpCall call)

{

var flurlTelemetry = new HttpTelemetry

{

AspNetCoreEnvironment = this._aspNetCoreEnvironment,

Endpoint = call.ToString(),

Succeeded = call.Succeeded,

RequestBody = call.RequestBody,

HttpStatusCode = call.HttpStatus.HasValue ?

(int)call.HttpStatus : default(int?)

}; if (call.Response != null &&

call.Response.Content != null)

{

var ct = call.Response.Content.Headers.ContentType.ToString(); // avoid logging file downloads

if (ct.ToLower().Contains("application/json") ||

ct.ToLower().Contains("text/plain"))

{

flurlTelemetry.ResponseBody = await call.Response.Content.ReadAsStringAsync();

}

} this._logger.Information("{@FlurlTelemetry}",

JsonConvert.SerializeObject(flurlTelemetry));

}

}

4. Wiring it all up

From here it’s fairly straight forward to register the classes to put the logging framework in place.

Create an extension method named AddFlurlTelemetry and call it from your Startup class.

public static IServiceCollection AddFlurlTelemetry(

this IServiceCollection services,

IHostingEnvironment env,

string eventHubConnectionString,

string eventHubEntity)

{

var writeToConsole = env.IsDevelopment(); services.AddSingleton<ISerilogFactory>(sp =>

new SerilogFactory(

writeToConsole,

eventHubConnectionString,

eventHubEntity)); services.AddSingleton<IFlurlTelemetryLogger>(sp =>

new FlurlClientTelemetryLogger(

sp.GetRequiredService<ISerilogFactory>(),

env)); services.AddSingleton<IFlurlClient>(sp =>

{

var client = new FlurlClient("https://exampleapi.com");

var logger = sp.GetRequiredService<IFlurlTelemetryLogger>(); client.Configure(settings =>

{

settings.AfterCallAsync = (httpCall) =>

{

logger.log(httpCall);

}

});



return client;

});

}

5. See the telemetry in action

To see the logs, inject the flurl client into your controller as per normal and make a call using the client in an action method

public class TestController : ControllerBase

{

private readonly IFlurlClient _client; public TestController(IFlurlClient client)

{

this._client = client;

} public async Task TestActionAsync()

{

await this._client.GetJsonAsync("/dummy");

}

}

Run your application locally and you should see Serilog automatically dumping your flurl telemetry into the command prompt. Obviously, a console sink will not be useful in your actual application servers. That’s where you will need to modify the SerilogFactory to use a different sink.

6. Next steps

This article covers the fundamental aspects of setting up a .NET Core Web application to automatically log http telemetry made through a Flurl client.

Setting up the actual logging framework to capture and query your telemetry data is beyond the scope of this article.

Which ever solution you end up using, take into consideration the volume, frequency and query support that your logging stack will need to handle.

Lastly, both Serilog and Flurl are flexible libraries offering the goodness of abstracting away lower level concerns while still maintaining a healthy number of extensible points for you to plug your application code into.

The purpose of this example is to demonstrate the technique to incorporate logging logic into an .NET Core app in a way that does not interfere with your regular application logic.

There are many tweaks and variations to this technique and the more familiar you are with the Serilog and Flurl libraries, the better you will be able to adopt this technique to the best fit for your application.