Test ILogger with FakeItEasy - c#

I'm working on a fun project for myself to learn blazor, a lot of core concepts and just general practice.
I'm trying to implement logger extension for FakeItEasy to easier test Microsofts ILogger.
Based on https://dev.azure.com/BrideButler/Bride%20Butler%20Open%20Source/_git/FakeItEasy.DotnetCore.LoggerExtensions?path=%2FFakeItEasy.DotnetCore.LoggerExtensions%2FLoggerExtensions.cs but now trying to get it to work for .NET 5.
Now that FormattedLogValues is internal this code doesn't work anymore.
The goal is to have something like this in my test (and not use Moq)
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
or A.CallTo(() => _logger.Log(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly(); It doesn't have to have syntactic sugar but I would like to get it to work with FakeItEasy instead of Moq.
Below is all the involved code. Ofcourse I see some difference in the first line Microsoft.Extensions.Logging.ILogger.Log1[System.Object]vsMicrosoft.Extensions.Logging.ILogger1[Server.Controllers.WeatherForecastController].Log1[Microsoft.Extensions.Logging.FormattedLogValues]` but I've no clue how to "fix" this since I dont fully understand what is going wrong.
My question is obviously, what should I do to fix it, but also curious what part i'm missing.
My error is the following:
Assertion failed for the following call:
Microsoft.Extensions.Logging.ILogger.Log`1[System.Object]
(
logLevel: Information,
eventId: <Ignored>,
state: <e => e.ToString().Contains(value(Server.Tests.Extensions.LoggerExtensions+<>c__DisplayClass2_0`1[Server.Controllers.WeatherForecastController]).message)>,
exception: <Ignored>,
formatter: <Ignored>
)
Expected to find it once exactly but didn't find it among the calls:
1: Microsoft.Extensions.Logging.ILogger`1[Server.Controllers.WeatherForecastController].Log`1[Microsoft.Extensions.Logging.FormattedLogValues]
(
logLevel: Information,
eventId: 0,
state: [[{OriginalFormat}, Get Weather Called]],
exception: NULL,
formatter: System.Func`3[Microsoft.Extensions.Logging.FormattedLogValues,System.Exception,System.String]
)
Tests:
(using xUnit, FakeItEasy, Fixture)
LoggerExtensions
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level,
string message)
{
return A.CallTo(() => logger.Log(
level,
A<EventId>._,
A<object>.That.Matches(e => e.ToString().Contains(message)),
A<Exception>._,
A<Func<object, Exception, string>>._)
);
}
}
TestConstructor:
private readonly IFixture _fixture;
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastControllerTests()
{
_fixture = new Fixture().Customize(new AutoFakeItEasyCustomization());
_logger = _fixture.Freeze<Fake<ILogger<WeatherForecastController>>>().FakedObject;
}
Test:
[Fact]
public void WeatherForecast_Get_Should_Log()
{
// Arrange
var weatherController = new WeatherForecastController(_logger);
// Act
weatherController.Get();
// Assert
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
}
Controller:
public class WeatherForecastController : ControllerBase
{
private readonly ILogger _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// Left out other code then logging
_logger.Log(LogLevel.Information, "Get Weather Called");
}
}
Update:
Options found
Only test if logger was called with correct log level:
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger).Where(call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == level);
}
Switch to Moq:
https://stackoverflow.com/a/59182490/1112413
https://stackoverflow.com/a/56728528/1112413

After reading https://stackoverflow.com/users/4728685/pavel-anikhouski comment on the post. I "got" what was wrong. Went to countless topics and made a solution that "works" tho probably could use some better coding standards. Hope it helps anyone else!
Edit: Some explaining.
Found this github comment that is the base of the solution. Then found this github pr where they mentioned to change Microsoft.Extensions.Logging.FormattedLogValues to IReadOnlyList<KeyValuePair<string, object>>
This can be extracted from GetArgument(2). (Source: https://stackoverflow.com/a/63701968/1112413)
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger)
.Where(call => call.Method.Name == "Log"
&& call.GetArgument<LogLevel>(0) == level
&& CheckLogMessages(call.GetArgument<IReadOnlyList<KeyValuePair<string, object>>>(2), message));
}
private static bool CheckLogMessages(IReadOnlyList<KeyValuePair<string, object>> readOnlyLists, string message)
{
foreach(var kvp in readOnlyLists)
{
if (kvp.Value.ToString().Contains(message))
{
return true;
}
}
return false;
}
}

Based on the same comments of Github
I ended up with a solution like that:
public class MyClassWithLoggerTests
{
private readonly MyClassWithLogger _sut;
private readonly ILogger<MyClassWithLogger> _logger;
public MyClassWithLoggerTests()
{
this._logger = A.Fake<ILogger<MyClassWithLogger>>();
this._sut = new MyClassWithLogger(this._logger);
}
[Fact]
public ATestMethodTitle()
{
this._sut.MethodToTest();
A.CallTo(_logger).Where(
call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == LogLevel.Information) // Modify with your type of log
.MustHaveHappened(1, Times.Exactly); // Modify with the number of times log is called
}
}

Related

Aspnetboilerplate ObjectMapper or Logger is null

I have a small assembly that utilises the following NuGet packages Abp, Abp.AspNetCore and Abp.AbpAutoMapper.
For reference I am using Abp 6.4.0
I have added the depends to the module, the main service is also below.
The constructor inject is valid for both IObjectMapper and Ilogger. Whereas the inherited DomainService and ApplicationService for ObjectMapper and Logger are always null / NullLogger.Instance or NullObjectMapper.Instance.
[DependsOn(typeof(AbpAutoMapperModule))]
public class ServiceModule:AbpModule {
// other methods and logic
public override void PostInitialize(){
var mainService = Configuration.Get<IMainService>();
Task.Run(() =>
{
mainService.Start();
});
base.PostInitialize();
}
}
public class MainService : ApplicationService, IMainService
{
public MainService(IObjectMapper mapper, ILogger logger)
{
if (Logger == NullLogger.Instance)
{
Debug.WriteLine("No logger configured.....");
}
if (ObjectMapper == NullObjectMapper.Instance)
{
Debug.WriteLine("No mapper configured.....");
}
}
}
// OR domain service
public class MainService : DomainService, IMainService
{
public MainService(IObjectMapper mapper, ILogger logger)
{
if (Logger == NullLogger.Instance)
{
Debug.WriteLine("No logger configured.....");
}
if (ObjectMapper == NullObjectMapper.Instance)
{
Debug.WriteLine("No mapper configured.....");
}
}
}
Any ideas on how this can be resolved?
Thanks
I think, u have to register your injections. Something like this:
public class ServiceModule:AbpModule {
// other methods and logic
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddScoped<IMainService>(sp => sp.GetRequiredService<MainService>());
}
public override void PostInitialize(){
var mainService = Configuration.Get<IMainService>();
Task.Run(() =>
{
mainService.Start();
});
base.PostInitialize();
}
}
DomainService and ApplicationService allow IObjectMapper and ILogger to be property-injected.
If MainService requires non-null object pattern instances to work properly, you can proactively assign your constructor-injected instances:
public MainService(IObjectMapper mapper, ILogger logger)
{
ObjectMapper = mapper;
Logger = logger;
}
References:
https://aspnetboilerplate.com/Pages/Documents/Dependency-Injection#property-injection-pattern
https://aspnetboilerplate.com/Pages/Documents/Logging#getting-the-logger

.NET Core 3 ExceptionHandler with Generic Logger parameter - AmbiguousMatchException

I am trying to implement .NET Core 3 app.UseExceptionHandler for an API project, but I have a generic logger per API controller that I would like to pass into the error method. If, for instance, the error happens in my WorkingController, I would like to have ILogger<WorkingController> be the logging entity. I have found that using the built-in ExceptionHandler, I lose some of the context of the request, and I would like to capture this context if possible.
Here's what all of my API methods used to look like before:
[Route("api/working")]
[ApiController]
public class WorkingController
{
private readonly ILogger<WorkingController> _logger;
public WorkingController(ILogger<WorkingController> logger)
{
_logger = logger;
}
[Route("workingRoute")]
public IActionResult SomeMethod()
{
_logger.LogInformation("Starting SomeMethod");
try
{
// doing some stuff here
}
catch(Exception ex)
{
_logger.LogError(ex, "Something happened");
return Problem();
}
return Ok();
}
}
I tried setting up a BaseErrorController from which other Controllers could inherit:
[ApiController]
public abstract class BaseErrorController<T> : ControllerBase
{
protected readonly ILogger<T> Logger;
public BaseErrorController(ILogger<T> logger)
{
Logger = logger;
}
[AllowAnonymous]
[Route("/error")]
public IActionResult Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (context != null)
{
var ex = context.Error;
Logger.LogError(ex, $"{context.Path} failed - {ex.Message}");
return Problem(
detail: context.Error.StackTrace,
title: context.Error.Message);
}
return Problem();
}
}
And now my former WorkingController looks like this, which is arguably a lot cleaner (and less code):
[Route("api/working")]
[ApiController]
public class WorkingController : BaseErrorController<WorkingController>
{
public WorkingController(ILogger<WorkingController> logger) : base(logger) { }
[Route("workingRoute")]
public IActionResult SomeMethod()
{
Logger.LogInformation("Starting SomeMethod");
// doing some stuff here
return Ok();
}
}
In Startup.cs, I'm registering this all with app.UseExceptionHandler("/error"), and it seems to work OK . . . except now in my logs, I see the following error (because I have more than one controller implementing this base controller):
An exception was thrown attempting to execute the error handler.
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
Matches: Namespace.Controllers.WorkingController.Error (Namespace)
Namespace.Controllers.AnotherController.Error (Namespace)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateState[] candidateState)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, CandidateState[] candidateState)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.Select(HttpContext httpContext, CandidateState[] candidateState)
at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.Matching.DataSourceDependentMatcher.MatchAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
Does anybody have an idea what might be the solution here? Is there an overload of the ExceptionHandler that might be what I'm looking for? Is this solution too boutique, and I should go back to what I was doing before? Help me, Stack Overflow. Thank you!
Unfortunately, there is simply no built-in way to do this cleanly. You could find a package to help here (I haven't looked), or you could want to re-write some parts of ASP.NET Core, but I wouldn't really want to do that.
There is another way of doing this, which depending on which version you like more, is recommended/recommended against, but I'm pro of the former. Instead of throwing/catching exceptions at the Controller level, I treat Controllers as the dumbest thing possible, so they just call some service and that's it.
If you want to know where an exception was thrown, or you specifically want the exception to go uncaught, a strategy my team follows is to create custom exceptions. You could then leave these uncaught (and the HTTP500 will be returned to the caller) or you could have a custom Middleware and define there what should happen.
The following is an example, written entirely here so there may be some changes needed, and it's solely to demonstrate a possible approach, not a working demo.
Given some exceptions valid to your domain:
public class UserNotFoundException : Exception { public Guid UserId { get; set; } }
public class InvalidModelException : Exception { }
And an exception handler:
public class MyCustomExceptionHandlerMiddleware
{
private readonly ILogger<MyCustomExceptionHandlerMiddleware> _logger;
public MyCustomExceptionHandlerMiddleware(ILogger<MyCustomExceptionHandlerMiddleware> logger)
{
_logger = logger;
}
public async Task Invoke(RequestDelegate next)
{
try
{
await next(); // without this, the request doesn't execute any further
}
catch (UserNotFoundException userNotFound)
{
_logger.LogError(userNotFound, "The user was not found");
// manipulate the response here, possibly return HTTP404
}
catch (Exception ex)
{
_logger.LogError(ex, "Something really bad happened");
// manipulate the response here
}
}
}
You could have something like this:
public class UsersService : IUsersService
{
private readonly ILogger<UsersService> _logger;
private readonly UsersContext _context;
// assume UsersContext is an EntityFramework context or some database service
public UsersService(ILogger<UsersService> logger, UsersContext context)
{
_logger = logger;
_context = context;
}
public async Task<User> GetUserAsync(Guid userId)
{
try
{
if (userId == Guid.Empty)
{
throw new InvalidModelException();
}
var user = await _context.FindUserAsync(userId);
if (user == null)
{
throw new UserNotFoundException(userId);
}
return user;
}
catch (InvalidModelException invalidModel)
{
_logger.LogWarning("The received user id is empty");
return null;
}
}
}
And its corresponding controller:
public class UsersController : ControllerBase
{
private readonly IUsersService _usersService;
public UsersController(IUsersService usersService)
{
_usersService = usersService;
}
[HttpGet("userId:guid")]
public async Task<IActionResult> GetUser(Guid userId)
{
var user = await _usersService.GetUserAsync(userId);
if (user == null)
{
return BadRequest();
}
return Ok(user);
}
}
Again, this is just an example to demonstrate how you could approach this, normally you'd do input validation in a more consistent way.

How do I generate an Exception for this test?

I am trying to test the Exception handler in my application.
However I can't generate an Exception for the Constructor.
Normally, I would create a moq of an Object and then do a setup where a call to Object.method throws an Exception. Then simply detect the Exception in the Test.
However, in this Constructor the only call is:
CredentialProfileStoreChain.TryGetAWSCredentials
CredentialProfileStoreChain.TryGetAWSCredentials can't be overridden so I can't use moq Setup to generate an Exception.
Code:
public class AWSDynamoDbManager : IAWSDynamoDbManager
{
private readonly ILogger _logger;
private readonly AmazonDynamoDBClient _dynamoDbClient;
//NOTE: This setting is in the app.config of the calling application so that different uses can use different profiles
private readonly string _awsProfileName = ConfigurationManager.AppSettings["AWSProfileName"];
public AWSDynamoDbManager(CredentialProfileStoreChain chain, ILogger logger)
{
this._logger = logger;
try
{
AWSCredentials awsCredentials;
chain.TryGetAWSCredentials(_awsProfileName, out awsCredentials);
_dynamoDbClient = new AmazonDynamoDBClient(awsCredentials, RegionEndpoint.EUWest2);
}
catch (Exception e)
{
logger.Error("Could Not Open DynamoDB");
logger.Error("Error: " + e.Message);
throw;
}
}
}
Test:
public void TestToSeeIfWeCatchTheExceptionIfWeCannotConnectToTheDatabase()
{
// arrange
var mockLogger = new Mock<ILogger>();
var mockChain = new Mock<CredentialProfileStoreChain>();
// act / assert
Assert.Catch<ArgumentException>(() => new AWSDynamoDbManager(mockChain.Object, mockLogger.Object));
}
What can I use to force the Constructor to cause an Exception?
when my hands are tied because of an external dependency or static type, i use a wrapper, so that's what i'd use here. since we can't mock a CredentialProfileStoreChain, we throw it in a wrapper and use the wrapper.
public interface ICredentialProfileStoreChainWrapper
{
void TryGetAWSCredentials(/*TODO*/);
}
public class CredentialProfileStoreChainWrapper
{
readonly CredentialProfileStoreChain _Chain;
public CredentialProfileStoreChainWrapper(CredentialProfileStoreChain chain)
{
_Chain = chain;
}
public void TryGetAWSCredentials(/*TODO*/)
{
_Chain.TryGetAWSCredentials(/*TODO*/);
}
}
public class AWSDynamoDbManager : IAWSDynamoDbManager
{
public AWSDynamoDbManager(ICredentialProfileStoreChainWrapper chainWrapper, ILogger logger)
{
//TODO
chainWrapper.TryGetAWSCredentials(/*TODO*/);
}
}
public class Tests
{
[Test]
public void TestToSeeIfWeCatchTheExceptionIfWeCannotConnectToTheDatabase()
{
var wrapper = new Mock<ICredentialProfileStoreChainWrapper>();
var logger = new Mock<ILogger>();
var manager = new AWSDynamoDbManager(wrapper.Object, logger.Object);
wrapper.Setup(s => s.TryGetAWSCredentials(/*TODO*/)).Throws(new Exception());
//TODO
}
}

Mocking and testing the LogError message using Moq and xUnit

I have a class level ILogger which is set up with the ILoggerFactory in the constructor. The logger is then used within a method within that class and this works perfectly.
I am struggling on how to Mock the ILogger and ILoggerFactory so I can unit test the LogError message. Can anyone show me an example of this?
I am using xUnit and Microsoft.Extentions.Logging for the loggin
//This is my unit test project
[Fact]
public void TestLogErrorMessage()
{
MyClass myClass = new MyClass (MockLoggerFactory().Object);
var result = myClass.Mymethod("a")
//How do I test the LogError message???
}
//Not sure if this is correct
private Mock<ILoggerFactory> MockLoggerFactory()
{
Mock<ILoggerFactory> mockLoggerFactory = new
Mock<ILoggerFactory>(MockBehavior.Default);
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
.Returns(MockLogger().Object);
return mockLoggerFactory;
}
private Mock<ILogger> MockLogger()
{
var logger = new Mock<ILogger>();
return logger;
}
//This is the class/method i need to test
private readonly ILogger logger;
public MyClass(ILoggerFactory loggerFactory)
{
if (loggerFactory != null)
{
this.logger = loggerFactory.CreateLogger<MyClass>();
}
}
public string Mymethod(string msg)
{
if(msg = "a")
{
this.logger.LogError($"error");
}
return "a string";
}
This is what finally worked for me, successful verification of LogError call:
var loggerMock = new Mock<ILogger>();
Expression<Action<ILogger>> logAction = x =>
// You can change this up with other severity levels:
x.Log<It.IsAnyType>(LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>());
loggerMock.Setup(logAction).Verifiable();
// Call a method that calls LogError here.
loggerMock.Object.LogError("test");
loggerMock.Verify(logAction, Times.Once);
You could do something like this
(note: this does not work in this specific case because LogError is an extension method, see below for an update):
Mock<ILogger> _logger; // declare it somewhere where your test can access it
[Fact]
public void TestLogErrorMessage()
{
MyClass myClass = new MyClass(MockLoggerFactory().Object);
var result = myClass.Mymethod("a")
_logger.Verify();
}
private Mock<ILoggerFactory> MockLoggerFactory()
{
_logger = new Mock<ILogger>();
_logger.Setup(log => log.LogError(It.IsAny<string>())).Verifiable();
Mock<ILoggerFactory> mockLoggerFactory = new Mock<ILoggerFactory>(MockBehavior.Default);
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
.Returns(_logger.Object);
return mockLoggerFactory;
}
The key thing to note is the call to Verifiable() after setting up the ILogger mock. You can read a bit more about Verifiable() in this SO question. After your test ran, you can check whether the method was called by calling .Verify() on the mock.
Depending on your needs, you could set this up in the constructor of your test class (if you need it for all/most tests) or directly inside your test method. You could also return it alongside the ILoggerFactory. The point is to hold onto the logger so you can verify against it that the method was called.
For your specific case (trying to verify calls to ILogger.LogError) you must not mock LogError, but the underlying method ILogger.Log. That could look like this:
var formatter = new Func<It.IsAnyType, Exception, string>((a, b) => "");
m.Setup(x => x.Log<It.IsAnyType>(LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
formatter))
.Verifiable();
Another alternative would be to make a fake implementation of ILogger and return that from the Mock<ILoggerFactory>:
mockLoggerFactory.Setup(_ => _.CreateLogger(It.IsAny<string>())
.Returns(new FakeLogger());
public class FakeLogger : ILogger
{
public static bool LogErrorCalled { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if(logLevel == LogLevel.Error)
{
LogErrorCalled = true;
}
}
}
And then after your test you can check whether FakeLogger.LogErrorCalled is true. This is simplified for your specific test - you can get more elaborate than this of course.

Create logs for Identity Server 4

I'm trying to figure out how to get more detailed logs out of Identity Server 4, especially if things go wrong. I have read their documentation here http://docs.identityserver.io/en/latest/topics/logging.html However, I can't seem to get it to work. For one I don't use serilog and I have the IS4 running on a server I don't have remote access to so console logging is not going to work for me.
As such, I've tried to inject my own custom logging library that I have with the following:
public class Temp : IDisposable
{
public void Dispose() { }
}
public class CustomLogger : ILogger
{
private readonly IDatabaseLoggerService _databaseLoggerService;
public CustomLogger(IDatabaseLoggerService databaseLoggerService)
{
_databaseLoggerService = databaseLoggerService;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if(exception == null) return;
_databaseLoggerService.Fatal(new LogErrorDetails
{
Exception = exception,
Message = "LIS - " + formatter
});
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
return new Temp();
}
}
public class CustomLoggerProvider : ILoggerProvider
{
private readonly IDatabaseLoggerService _databaseLoggerService;
public CustomLoggerProvider(IDatabaseLoggerService databaseLoggerService)
{
_databaseLoggerService = databaseLoggerService;
}
public void Dispose() { }
public ILogger CreateLogger(string categoryName)
{
return new CustomLogger(_databaseLoggerService);
}
}
As you can see its fairly straightforward because at this point I don't need it polished, just need it working, and I'll work on the proper implementation of it later once I have the sample working.
Now to set this up I did it in the Startup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//code emmited
var serviceProvider = app.ApplicationServices;
var dbLogger = serviceProvider.GetService<IDatabaseLoggerService>();
loggerFactory.AddProvider(new CustomLoggerProvider(dbLogger));
}
However, I don't see any logs about it, the only thing I saw was the occasional anti-forgery token error that now pops up and that is it:
at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgeryTokenSerializer.Deserialize(String serializedToken) at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.GetCookieTokenDoesNotThrow(HttpContext httpContext)
What I would like to see is if a token request was successful or if it failed and why, but I'm not seeing any of it.
How would I go to accomplish higher level of logging for Identity Server 4?
You should be able to add your logger right into the WebHostBuilder giving you probably most insightful logs.
You can start of by creating an extension method to keep things nice and clean. We just need to add a singleton of our logger factory to the DI.
public static IWebHostBuilder UseCustomLogger(this IWebHostBuilder builder, CustomLogger logger = null, bool dispose = false)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
builder.ConfigureServices(collection =>
collection.AddSingleton<ILoggerFactory>(services => new CustomLoggerFactory(logger, dispose)));
return builder;
}
Factory itself would be very simple.
public class CustomLoggerFactory : ILoggerFactory
{
private readonly CustomLoggerProvider _provider;
public CustomLoggerFactory(ILogger logger = null, bool dispose = false)
{
_provider = new CustomLoggerProvider(logger, dispose);
}
public void Dispose()
{
_provider.Dispose();
}
public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
{
return _provider.CreateLogger(categoryName);
}
}
You can then add your logger in Program.cs:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseCustomLogger();
Lastly, looking at your logger implementation - you will need to create instance of IDatabaseLoggerService in the factory so that the logger can be created. Similar approach can of course be used with the ASP.NET Core DI.

Categories

Resources