ToolsPack.Logging
4.0.1
dotnet add package ToolsPack.Logging --version 4.0.1
NuGet\Install-Package ToolsPack.Logging -Version 4.0.1
<PackageReference Include="ToolsPack.Logging" Version="4.0.1" />
<PackageVersion Include="ToolsPack.Logging" Version="4.0.1" />
<PackageReference Include="ToolsPack.Logging" />
paket add ToolsPack.Logging --version 4.0.1
#r "nuget: ToolsPack.Logging, 4.0.1"
#:package ToolsPack.Logging@4.0.1
#addin nuget:?package=ToolsPack.Logging&version=4.0.1
#tool nuget:?package=ToolsPack.Logging&version=4.0.1
ToolsPack.Logging
Http Logging middlewares
This nuget provides several middlewares of HttpClient allowing to log HTTP requests and responses.
CompactHttpLoggingMiddleware loggingMiddleware = new(logger)
var httpClient = new HttpClient(loggingMiddleware)
//the request and response will be logged
await httpClient.GetAsync("https://api.example.com/user/42?metadata=off");
CompactHttpLoggingMiddlewareproduces only ONE log message per request. The log is emitted after receiving the response, the middleware logs both the request, response and the elapsed time in only one log item. In caseExceptionoccurs before receiving the response, the Exception would be logged in the same log item and be re-thrown.SimpleHttpLoggingMiddlewareproduces multiple log messages per requests. It logs the request before sending, then log the response after receiving. The middleware generates aHttpRequestCorrelationIdin the logging scope to correlate the request and response log items. In caseExceptionoccurs before receiving the response, only the request would be logged.Using the corresponding
CustomCompactHttpLoggingMiddlewareandCustomSimpleHttpLoggingMiddleware. You would be able- to configure the logging level depending on the request/response. You can configure for example:
LogLevel.Warningwhen the response status is not 2XX, andLogLevel.Informationotherwise.LogLevel.Nonefor all GET request to avoid logging them.
- to configure logging redaction depending on the request/response. You can configure for example:
- Hide the "password:*" value in the request's body of
POST https://external-api/registration/user - Hide the "bankCardNumber:*" in the response's body of
GET https://external-api/payment/*
- Hide the "password:*" value in the request's body of
- to configure the logging level depending on the request/response. You can configure for example:
Recommendation
- Choose
CompactHttpLoggingMiddlewareoverSimpleHttpLoggingMiddleware - Choose
CustomCompactHttpLoggingMiddlewareoverCustomSimpleHttpLoggingMiddleware
MockLogger
Your test is executing some application codes which log certain messages via the Microsoft.Extensions.Logging library.
Once the application codes finished, you want to Assert (or to Verify) that some messages are logged as expected.
There are several ways allowing you to "spy" on the log events happening during the application execution:
- Use
FakeLoggerin the Microsoft.Extensions.Diagnostics.Testing nuget package. Checkout the FakeLogger example. - Use the
Meziantou.Extensions.Logging.InMemorypackage - Use
MockLoggerof this ToolsPack.Logging nuget package. Checkout theMockLoggerTests.csfor demonstration.
While FakeLogger give you access to the collection of all log events so you can check whatever you wanted. MockLogger is suistable only for simple use cases which you interested in only certain log events, verify if they are logged or not?
Here how MockLogger works:
Step 1: Use a Mock library (such as Moq, NSubstitue..) to instantiate a MockLogger. For eg:
MockLogger _mocklogger = Substitute.For<MockLogger>();
Step 2: Ask your application codes to use the MockLogger by providing (or by injecting) it to your Application codes, for eg:
var diContainer = new ServiceCollection();
diContainer.AddLogging(builder =>
{
builder.AddMockLogger(_mocklogger);
//you can apply specific filters for MockLogger
builder
.AddFilter<MockLoggerProvider>("System", LogLevel.Error)
.AddFilter<MockLoggerProvider>("Microsoft", LogLevel.Error)
.AddFilter<MockLoggerProvider>("App.Metrics", LogLevel.Error);
//or else it will follows the global filters
builder.AddFilter(null, LogLevel.Trace);
});
step 3: Then use your Mock library to spy (NSubstitue.Received(), Moq.Verify..) on the IsLogged hooks of the MockLogger. For eg:
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Error),
Arg.Any<EventId>(),
Arg.Is<Exception?>(ex => ex.Message == "some exception"),
Arg.Is<string>(s => s.Contains("some error on Greeting")));
If the mockLogger is unable to capture the log message then check the config (or default config) of the filters.
Basic usages
//Act
_mocklogger.LogInformation("haha");
_mocklogger.LogError(new Exception("some exception"), "some error on {method}", "Greeting");
//Assert
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Information),
Arg.Any<EventId>(),
Arg.Any<Exception?>(),
Arg.Is("haha"));
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Error),
Arg.Any<EventId>(),
Arg.Is<Exception?>(ex => ex.Message == "some exception"),
Arg.Is<string>(s => s.Contains("some error on Greeting")));
Basic usages for High performance logging
//Logging definition
[LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "Send request {message}")]
public static partial void LogSendRequestInfo(this ILogger logger, string message);
[LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "Received something wrong")]
public static partial void LogGetResponseError(this ILogger logger, Exception ex);
//High performance logging
_mocklogger.LogSendRequestInfo("toto");
_mocklogger.LogGetResponseError(new Exception("Failed to do thing"));
//Assert the log content
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Information),
Arg.Any<EventId>(),
Arg.Any<Exception?>(),
Arg.Is("Send request toto"));
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Error),
Arg.Any<EventId>(),
Arg.Is<Exception?>(ex => ex.Message == "Failed to do thing"),
Arg.Is<string>(s => s.Contains("Received something wrong")));
Log structuring
In log structuring, a log item is usually a list of objects (key-value) rather than just a string.
Your application codes might also generate many log items with the same text message but with different objects in the log structure. In this case, Asserting (verifying) the log text message is not very interesting, you usually want to spy on the log structure instead.
//Logging definition
[LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "The person {person} odered {order}")]
public static partial void LogStructuringSampleInfo(this ILogger logger, SomePersonObject person, SomeOrderObject order);
//High performance logging, it will generate a structured log which contains 2 objects
_mocklogger.LogStructuringSampleInfo(new SomePersonObject("Hiep", 18), new SomeOrderObject("#1", 30.5));
//Assert the log content
using LogState = IReadOnlyList<KeyValuePair<string, object?>>;
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Information),
Arg.Any<EventId>(),
Arg.Any<Exception?>(),
Arg.Is<string>(msg => Regex.IsMatch(msg, "The person .* odered .*")),
Arg.Is<LogState>(state => AssertLogStructuring(state)));
//verify that the "person" and "order" objects in the log item got the expected values
private static bool AssertLogStructuring(LogState state)
{
var person = (from s in state
where s.Key == "person"
select s.Value as SomePersonObject).First();
var order = (from s in state
where s.Key == "order"
select s.Value as SomeOrderObject).First();
return person.Name == "Hiep" && person.Age == 18 && order.Id == "#1" && order.Amount == 30.5;
}
Use MockLogger to spy on other loggers used by your application codes
You have to add the MockLogger to the logging pipeline of your application, so that it can "monitor" all log events generated by your application codes:
//let's say you have a service provider similar to the one in your ASP.NET app
var diContainer = new ServiceCollection();
diContainer.AddLogging(builder =>
{
//add the MockLogger to the logging pipeline
builder.AddMockLogger(_mocklogger);
});
var services = diContainer.BuildServiceProvider();
//let's say your application is using this logger (which is not our MockLogger)
ILogger<MyApp> someAppLogger = services.GetRequiredService<ILogger<MyApp>>();
//your application logged something with this logger
someAppLogger.LogInformation("haha");
//Assert that application logged the expected message whatever the logger is used
_mocklogger.Received().IsLogged(
Arg.Is(LogLevel.Information),
Arg.Any<EventId>(),
Arg.Any<Exception?>(),
Arg.Is("haha"));
Use MockLogger in XUnit side by side with real loggers (Serilog for example)
Checkout full codes in TestOutputHelperExtensionTests.cs.
The example got a nice Helper to configure logging on the stack XUnit, NSubstitute..
//config serilog
var serilogLogger = new LoggerConfiguration()
.MinimumLevel.Verbose(); //serilog can see all the log events
.WriteTo.Seq("http://localhost:5304/")
.WriteTo.TestOutput(testOutputHelper)
.WriteTo.File("app.log")
.CreateLogger()
//create mockLogger with NSubstitute
var mockLogger = Substitute.For<MockLogger>();
//mix Serilog and mockLogger together
var loggerFactory = LoggerFactory.Create(builder => {
builder
.AddSerilog(serilogLogger)
.AddMockLogger(mockLogger)
//Make mockLogger receive all log events, similar to serilog
.AddFilter(null, LogLevel.Trace);
});
//Log something:
var logger = loggerFactory.CreateLogger();
logger.LogInformation("hello");
Note: To display the Xunit's Tests Output on the command line:
dotnet test --logger "console;verbosity=detailed":
Display logs of XUnit's extensibility classes (IClassFixture, ITestCaseOrderer...) in the Test console
The XUnit test console (ITestOutputHelper) is only available for unit tests. If the logs messages in XUnit's extensibility classes (IClassFixture, ITestCaseOrderer...) won't show up in the test console.
But there is a way to make it happening: checkout this Tutorial.
ElapsedTimeLogger
Micro-benchmark a part of code to investigate on performance. This is a normal ILogger or more precisely a decorator to a ILogger, when you use it to log a message it will "decorate" the message with the duration (elapsed time) since the previous logged message.
class MyCalculator
{
private static readonly ILogger Log = loggerFactory.CreateLogger<ElapsedTimeLoggerTests>();
public void Process()
{
using (var etw = ElapsedTimeLogger.Create(Log, "blockCodeName"))
{
...
etw.LogInformation("step 1");
...
etw.LogDebug("step 2");
...
etw.LogInformation("Step 3)");
...
} //"sum up log" is displayed here
}
}
- The
etwwrap the usual loggerLog, we useetwto log message instead of the usualLog - the
blockCodeNameis repeated in the start of each log message, so that we can filter log message by "blockCodeName" - Each log message will display the elapsed time (in micro-second) since the last log message.
- A sum up log will display the total elapsed time (in micro-second) when the
etwobject is disposed.
22:56:59,866 [DEBUG] Begin blockCodeName
22:56:59,970 [INFO ] blockCodeName - 102350 mcs - step 1
22:57:00,144 [DEBUG] blockCodeName - 173295 mcs - step 2
22:57:00,259 [INFO ] blockCodeName - 114036 mcs - Step 3)
22:57:00,452 [INFO ] End blockCodeName : Total elapsed 585436 mcs
Auto Jump Log Level
var etw = ElapsedTimeLogger.Create(Log, "checkIntraday").InfoEnd().AutoJump(150, 250).AutoJumpLastLog(500, 1000)
- The log level will auto jump to INFO if the elapsed time exceeds 150 ms
- The log level will auto jump to WARN if the elapsed time exceeds 250 ms
- The above sum up log will switch to INFO if the total elapsed time exceeds 500 ms
- The above sum up log will switch to WARN if the total elapsed time exceeds 1 sec
Customize Start Context and End context
var etw = ElapsedTimeLogger.Create(Log, "foo", "Start_context", "End_context");
will give
22:56:59,866 [DEBUG] Begin Start_context
22:56:59,970 [INFO ] foo - 102350 mcs - step 1
22:57:00,144 [DEBUG] foo - 173295 mcs - step 2
22:57:00,259 [INFO ] foo - 114036 mcs - Step 3)
22:57:00,452 [INFO ] End End_context : Total elapsed 585436 mcs
We often display the parameter of the functions in the "Start context". Example:
public void process(string val, bool useCache)
{
var context = string.Format("process(val={0}, useCache={1})", val, useCache);
using (var etw = ElapsedTimeLogger.Create(Log, "process", context))
{
...
etw.LogInformation("step 1");
...
etw.LogDebug("step 2");
...
etw.LogInformation("Step 3)");
...
} //"sum up log" is displayed here
}
will give
22:56:59,866 [DEBUG] Begin process(val=Lorem ipsum, useCache=true)
22:56:59,970 [INFO ] process - 102350 mcs - step 1
22:57:00,144 [DEBUG] process - 173295 mcs - step 2
22:57:00,259 [INFO ] process - 114036 mcs - Step 3)
22:57:00,452 [INFO ] End process(val=Lorem ipsum, useCache=true) : Total elapsed 585436 mcs
Other benchmark library
- If you want to optimize a particular static (and stateless) function, checkout BenchmarkDotNet project. See video tutorial here. (Unlike other project, the
ElapsedTimeLoggeris just aILoggerso it fits to be injected to any production application) - https://miniprofiler.com/dotnet/
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Logging (>= 6.0.0)
- ToolsPack.String (>= 3.1.3)
-
.NETStandard 2.1
- Microsoft.Extensions.Logging (>= 6.0.0)
- ToolsPack.String (>= 3.1.3)
-
net8.0
- Microsoft.Extensions.Logging (>= 6.0.0)
- ToolsPack.String (>= 3.1.3)
-
net9.0
- Microsoft.Extensions.Logging (>= 6.0.0)
- ToolsPack.String (>= 3.1.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.