Skip to content

Commit

Permalink
Add the support of the log message templates. (#6)
Browse files Browse the repository at this point in the history
* Add the support of the log message templates.
  • Loading branch information
GillesTourreau authored Oct 16, 2023
1 parent 44a83bc commit 72d5c4f
Show file tree
Hide file tree
Showing 11 changed files with 623 additions and 128 deletions.
82 changes: 75 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class CustomerManager

public async Task SendEmailAsync(int id, string name)
{
this.logger.LogInformation($"Starting to send an email to the customer '{name}'");
this.logger.LogInformation($"Starting to send an email to the customer '{Name}' with the identifier '{Id}'", name, id);

using (this.logger.BeginScope(new { Id = id }))
{
Expand Down Expand Up @@ -81,7 +81,7 @@ public async Task SendEmailAsync()
.Returns(Task.CompletedTask);

var logger = new Mock<ILogger<CustomerManager>>(MockBehavior.Strict);
logger.Setup(l => l.Log(LogLevel.Information, "Starting to send an email to the customer 'Gilles TOURREAU'", ..., ..., ... )) // WTF???
logger.Setup(l => l.Log(LogLevel.Information, "Starting to send an email to the customer '{Name}' with the identifier '{Id}'", ..., ..., ... )) // WTF???
...
logger.Setup(l => l.BeginScope<...>(...)) // WTF???
Expand All @@ -106,7 +106,7 @@ void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? ex
```

And also to check the scope usage with the [ILogger](https://learn.microsoft.com/fr-fr/dotnet/api/microsoft.extensions.logging.ilogger)
interface it can be hard !
interface it can be hard!

The [PosInformatique.Logging.Assertions](https://www.nuget.org/packages/PosInformatique.Logging.Assertions/) library
allows the developpers to mock the [ILogger](https://learn.microsoft.com/fr-fr/dotnet/api/microsoft.extensions.logging.ilogger)
Expand All @@ -124,7 +124,8 @@ public async Task SendEmailAsync()

var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer 'Gilles TOURREAU'")
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments("Gilles TOURREAU", 1234)
.BeginScope(new { Id = 1234 })
.LogDebug("Call the SendAsync() method")
.LogDebug("SendAsync() method has been called.")
Expand Down Expand Up @@ -155,7 +156,8 @@ For example to check nested log scopes write the following code with the followi
```csharp
var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer 'Gilles TOURREAU'")
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments("Gilles TOURREAU", 1234)
.BeginScope(new { Id = 1234 })
.BeginScope(new { Name = "Gilles" })
.LogError("Error in the scope 1234 + Gilles")
Expand Down Expand Up @@ -184,7 +186,8 @@ public async Task SendEmailAsync_WithException()

var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer 'Gilles TOURREAU'")
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments("Gilles TOURREAU", 1234)
.BeginScope(new { Id = 1234 })
.LogDebug("Call the SendAsync() method")
.LogError("Unable to send the email !")
Expand All @@ -209,7 +212,8 @@ during the *Arrange* phase), use the version with a delegate to check the conten
```csharp
var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer 'Gilles TOURREAU'")
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments("Gilles TOURREAU", 1234)
.BeginScope(new { Id = 1234 })
.LogDebug("Call the SendAsync() method")
.LogError("Unable to send the email !")
Expand All @@ -221,6 +225,70 @@ logger.SetupSequence()
.EndScope();
```

### Test log message templates
The power of this library is the ability to assert the
[log message templates](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/#log-message-template)
including the arguments. (*You know the kind of log messages
which contains the vital identifiers to search in emergency in production environment and are often bad logged by the developpers...*
:laughing: :laughing:).

To assert the log message templates parameters use the `WithArguments()` method which is available with 2 overloads:
- `WithArguments(params object[] expectedArguments)`: Allows to specify the expected arguments of the log message template.
- `WithArguments(int expectedCount, Action<LogMessageTemplateArguments> expectedArguments)`: Allows to specify
an delegate to assert complex arguments.

For example, to assert the following log message:
```csharp
this.logger.LogInformation($"Starting to send an email to the customer '{Name}' with the identifier '{Id}'", name, id);
```

Using the first way with the `WithArguments(params object[] expectedArguments)` method:

```csharp
var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments("Gilles TOURREAU", 1234)

... // Continue the setup expected log sequence
```

Using the second way with the `WithArguments(int expectedCount, Action<LogMessageTemplateArguments> expectedArguments)` method
which give you more control of the assertions:

```csharp
var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments(2, args =>
{
args["Name"].Should().Be("Gilles TOURREAU");
args["Id"].Should().Be(1234);
})

... // Continue the setup expected log sequence
```

> Here we use the FluentAssertions library to check the arguments values of the log message template, but of course you can use your
favorite assertion framework to check it.

The second way allows also to check the arguments of the log template message by there index (*it is not what I recommand,
because if the trainee developper in your team change the name of the arguments name in the log message template, you will not
see the impacts in your unit tests execution...*):

```csharp
var logger = new LoggerMock<CustomerManager>();
logger.SetupSequence()
.LogInformation("Starting to send an email to the customer '{Name}' with the identifier '{Id}'")
.WithArguments(2, args =>
{
args[0].Should().Be("Gilles TOURREAU");
args[1].Should().Be(1234);
})

... // Continue the setup expected log sequence
```

### Assertion fail messages
The [PosInformatique.Logging.Assertions](https://www.nuget.org/packages/PosInformatique.Logging.Assertions/) library
try to make the assert fail messages the most easy to understand for the developers:
Expand Down
2 changes: 1 addition & 1 deletion src/Logging.Assertions/ILoggerMockSetupSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public interface ILoggerMockSetupSequence
/// <param name="logLevel"><see cref="LogLevel"/> of the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/> call expected.</param>
/// <param name="message">Message of the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/> call expected.</param>
/// <returns>The current <see cref="ILoggerMockSetupSequence"/> which allows to continue the setup of the <see cref="ILogger"/> method calls.</returns>
ILoggerMockSetupSequence Log(LogLevel logLevel, string message);
ILoggerMockSetupSequenceLog Log(LogLevel logLevel, string message);

/// <summary>
/// Expect the call to the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/> method
Expand Down
2 changes: 1 addition & 1 deletion src/Logging.Assertions/ILoggerMockSetupSequenceError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace PosInformatique.Logging.Assertions
/// Allows to setup the sequence of <see cref="ILogger"/> method calls for the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/>
/// with the <see cref="LogLevel.Error"/> log level when an <see cref="Exception"/> is occured.
/// </summary>
public interface ILoggerMockSetupSequenceError : ILoggerMockSetupSequence
public interface ILoggerMockSetupSequenceError : ILoggerMockSetupSequenceLog
{
/// <summary>
/// Allows to check the <see cref="Exception"/> passed in the argument of the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/>.
Expand Down
25 changes: 25 additions & 0 deletions src/Logging.Assertions/ILoggerMockSetupSequenceLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//-----------------------------------------------------------------------
// <copyright file="ILoggerMockSetupSequenceLog.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Logging.Assertions
{
using Microsoft.Extensions.Logging;

/// <summary>
/// Allows to setup the sequence of <see cref="ILogger"/> method calls for the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/>.
/// </summary>
public interface ILoggerMockSetupSequenceLog : ILoggerMockSetupSequence
{
/// <summary>
/// Allows to assert the template message arguments.
/// </summary>
/// <param name="expectedCount">Number of template message arguments expected.</param>
/// <param name="expectedArguments">A delegate which allows to check the template message arguments. All the arguments can be asserted using the <see cref="LogMessageTemplateArguments"/>
/// parameter of the delegate.</param>
/// <returns>An instance of <see cref="ILoggerMockSetupSequence"/> which allows to continue the setup of the method calls for the <see cref="ILogger"/>.</returns>
ILoggerMockSetupSequence WithArguments(int expectedCount, Action<LogMessageTemplateArguments> expectedArguments);
}
}
92 changes: 92 additions & 0 deletions src/Logging.Assertions/LogMessageTemplateArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//-----------------------------------------------------------------------
// <copyright file="LogMessageTemplateArguments.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Logging.Assertions
{
using System.Collections;
using Microsoft.Extensions.Logging;

/// <summary>
/// Contains the message template arguments of the <see cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception?, Func{TState, Exception?, string})"/>
/// which are accessible by the delegate specified on the <see cref="ILoggerMockSetupSequenceLog.WithArguments(int, Action{LogMessageTemplateArguments})"/>
/// to assert the message template arguments.
/// </summary>
public sealed class LogMessageTemplateArguments : IEnumerable<object?>
{
private readonly IReadOnlyList<KeyValuePair<string, object?>> arguments;

/// <summary>
/// Initializes a new instance of the <see cref="LogMessageTemplateArguments"/> class.
/// </summary>
/// <param name="arguments">Arguments to assert.</param>
internal LogMessageTemplateArguments(IEnumerable<KeyValuePair<string, object?>> arguments)
{
this.arguments = arguments.ToArray();
}

/// <summary>
/// Gets the number of message template arguments.
/// </summary>
public int Count => this.arguments.Count;

/// <summary>
/// Gets the message template argument value by his key.
/// </summary>
/// <param name="key">Template argument name to retrieve the value.</param>
/// <returns>The message template argument value by his key.</returns>
/// <exception cref="KeyNotFoundException">If the <paramref name="key"/> argument name has not been found.</exception>
public object? this[string key]
{
get
{
var valueFound = this.arguments.SingleOrDefault(kv => kv.Key == key);

if (valueFound.Equals(default(KeyValuePair<string, object?>)))
{
throw new KeyNotFoundException($"The given message template argument '{key}' was not present.");
}

return valueFound.Value;
}
}

/// <summary>
/// Gets the message template argument value by his index position.
/// </summary>
/// <param name="index">Template argument index position to retrieve the value.</param>
/// <returns>The message template argument value at the specified <paramref name="index"/> position.</returns>
/// <exception cref="ArgumentOutOfRangeException">If the <paramref name="index"/> is out of the range of the template message arguments list.</exception>
public object? this[int index]
{
get
{
if (index < 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}

if (index >= this.arguments.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}

return this.arguments[index].Value;
}
}

/// <inheritdoc />
public IEnumerator<object?> GetEnumerator()
{
return this.arguments.Select(kv => kv.Value).GetEnumerator();
}

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
}
Loading

0 comments on commit 72d5c4f

Please sign in to comment.