Open julienGrd opened 4 years ago
In addition to my previous message, and because im open to alternative solution if what i want is not possible for now, i see when unhandled exception happen, its logged (automatically) like this
2020-08-31 16:59:08.4854|111|ERROR|Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost|Unhandled exception in circuit 'FHhWh59KEnWWXmK4liQt9cZo4dMrHCUEm41s-RnY1HY'. System.NullReferenceException: Object reference not set to an instance of an object.
I have at least a circuitId i can use to find the client. But on the other side, i don't see how i can retrieve this circuitId from one of my scoped service. It would be very usefull, so i can log the assocation between a login and a circuitId and then retrieve on which client the exception happen with this cirtcuitId. I check around CircuitHandler but its a singleton ervices so i can't access my scoped services here.
thanks !
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
If you create your own scoped class, inject the logger into it, and then use that to log the info you need. I think that will work.
If you create your own scoped class, inject the logger into it, and then use that to log the info you need. I think that will work.
Not on this way, because of unexpected exception which i can handle only by my implementation of ILogger.
So if an exception happen in my app, im already in the Log method of my logger implementation, and here i can't access to my scoped service.
This can theoretically be solved by put try/catch all around method, but this cant be done in my app for now because i reuse code full of callback which can't be handle by the calling event (and i can't update the code because its use by others app).
I think im block by design on this problem, i hope scoped logger or global exception handling coming soon, i can't put my app in production because of that.
but thanks for your help, if you have other ideas i take it !
This issue has been opened for a couple of years. It would be nice to move this forward. Yesterday I spent several hours trying to find a work around for this.
@julienGrd did you ever find a solution to this?
I tried custom ILogger and ILoggerProvider implementations. I tried Serilog's ASP.Net extensions which gives access to the root container but you still cannot resolve scope services. I played with registering different lifetimes hoping that a transient logger would be able to access a scoped service. It really does not seem possible to have a scope logger.
Having scoped logging is critical for us and I am surprised this is not a bigger issue. Our solution is a multi-tenant/multi-database solution and scoped logging give use the ability to see which user generated a log event and for which database.
The only solution I have not tried yet is to replace the built in service container with a container that will allow registration of a ILogger as a singleton and scoped and then resolve the more specific one. I think that is possible with DryIoc but I am not sure.
@chrisg32 : no and i even dont use ILogger pattern anymore, i don't find it so much convenient. so i create a custom LoggerService (scoped) and i log by myself. In you other services/component inject it in place of the recommanded Ilogger
Inside the service you can do what you want, call the ILogger (singleton) or write in database or file or whatever.
Im agree with you, we are very limited actually with the ILgger pattern
I don't follow this subject since .NET 5 so maybe they did something for .NET 6 ?
I ended up creating my own Logger which can be scoped that consumes the global, static Logger and uses Serilog LogContext to enrich the log with the username/database of the user in the Logging class that the time of the logging event. This does mean that you cannot use ILogger. To make things simplier, I create an interface called ILogger in my namespace that extends the Microsoft I logger. I then add my logging namespace to the _Imports.razor so anywhere ILogger is injected my logger will be used. Framework logging events are still logged but are not enriched with the users scope. This is the most elegant work around I could come up with.
The example below shows my implementation working with Serilog but the same pattern can be used with any logger.
namespace MyApp.Utilities.Logging
{
public interface ILogger<out TCategory> : ILogger, Microsoft.Extensions.Logging.ILogger<TCategory> { }
public interface ILogger : Microsoft.Extensions.Logging.ILogger { }
public class BlazorLogger<TCategory> : ILogger<TCategory>
{
private readonly ILogger _logger;
public BlazorLogger(ILogger logger)
{
_logger = logger;
}
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}
public IDisposable BeginScope<TState>(TState state)
{
return _logger.BeginScope(state);
}
}
public class BlazorLogger : ILoggingService, ILogger
{
private readonly IBlazorScopedUserContext _loggerScope;
public BlazorLogger(BlazorLoggerScope loggerScope)
{
_loggerScope = loggerScope;
}
public bool Trace(string message)
{
LogToSerilog(LogEventLevel.Verbose, message);
return true;
}
public bool Debug(string message)
{
LogToSerilog(LogEventLevel.Debug, message);
return true;
}
public bool Info(string message)
{
LogToSerilog(LogEventLevel.Information, message);
return true;
}
public bool Warn(string message)
{
LogToSerilog(LogEventLevel.Warning, message);
return true;
}
public bool Error(string message)
{
LogToSerilog(LogEventLevel.Error, message);
return true;
}
public bool Error(Exception exception)
{
LogToSerilog(LogEventLevel.Error, exception.Message, exception);
return true;
}
public bool Error(string message, Exception exception)
{
LogToSerilog(LogEventLevel.Error, message, exception);
return true;
}
public bool Fatal(string message)
{
LogToSerilog(LogEventLevel.Fatal, message);
return true;
}
public bool LogException(Exception exception)
{
LogToSerilog(LogEventLevel.Error, exception.Message, exception);
return true;
}
public bool LogMessage(string message, LogLevel level)
{
if (level == LogLevel.OFF) return false;
LogToSerilog(ToSerilogEventLevel(level), message);
return true;
}
public bool LogObject(object obj, string message = null, LogLevel level = LogLevel.INFO)
{
return LogMessage($"{message ?? string.Empty}{obj}", level);
}
public Task<bool> LogObjectAsync(object obj, string message = null, LogLevel level = LogLevel.INFO)
{
return Task.FromResult(LogObject(obj, message, level));
}
public void Flush()
{
//not required for Serilog
}
public LogLevel Level { get; set; }
private void LogToSerilog(LogEventLevel level, string message, Exception exception = null)
{
using (LogContext.PushProperty("DatabaseName", _loggerScope.Database))
using (LogContext.PushProperty("UserName", _loggerScope.Username))
{
Serilog.Log.Write(level, exception, message);
}
}
private static LogEventLevel ToSerilogEventLevel(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.TRACE => LogEventLevel.Verbose,
LogLevel.DEBUG => LogEventLevel.Debug,
LogLevel.INFO => LogEventLevel.Information,
LogLevel.WARN => LogEventLevel.Warning,
LogLevel.ERROR => LogEventLevel.Error,
LogLevel.FATAL => LogEventLevel.Fatal,
LogLevel.OFF => LogEventLevel.Information,
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
};
}
private static LogEventLevel ToSerilogEventLevel(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return logLevel switch
{
Microsoft.Extensions.Logging.LogLevel.Trace => LogEventLevel.Verbose,
Microsoft.Extensions.Logging.LogLevel.Debug => LogEventLevel.Debug,
Microsoft.Extensions.Logging.LogLevel.Information => LogEventLevel.Information,
Microsoft.Extensions.Logging.LogLevel.Warning => LogEventLevel.Warning,
Microsoft.Extensions.Logging.LogLevel.Error => LogEventLevel.Error,
Microsoft.Extensions.Logging.LogLevel.Critical => LogEventLevel.Fatal,
Microsoft.Extensions.Logging.LogLevel.None => LogEventLevel.Information,
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
};
}
public class BlazorLoggerScope : IBlazorScopedUserContext
{
public void Set(ClaimsIdentity identity)
{
((IBlazorScopedUserContext)this).Database = identity.Claims.Get(Claims.DatabaseName);
((IBlazorScopedUserContext)this).Username = identity.Claims.Get(Claims.Username);
}
string IBlazorScopedUserContext.Database { get; set; }
string IBlazorScopedUserContext.Username { get; set; }
}
/// <summary>
/// Interface is used to hide access to this properties so that they can only be used for logging.
/// </summary>
private interface IBlazorScopedUserContext
{
string Database { get; protected internal set; }
string Username { get; protected internal set; }
}
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
LogToSerilog(ToSerilogEventLevel(logLevel), formatter(state, exception));
}
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return Serilog.Log.IsEnabled(ToSerilogEventLevel(logLevel));
}
public IDisposable BeginScope<TState>(TState state)
{
return default;
}
}
public static class BlazorLoggerExtensions
{
public static IServiceCollection AddCustomBlazorLogging(this IServiceCollection services)
{
services.AddScoped<BlazorLogger.BlazorLoggerScope>();
//we do registration this way so the same instance will be resolved for either the abstract or implementation types
services.AddScoped<BlazorLogger>();
services.AddScoped<ILoggingService>(provider => provider.GetRequiredService<BlazorLogger>());
services.AddScoped<ILogger>(provider => provider.GetRequiredService<BlazorLogger>());
services.AddScoped(typeof(BlazorLogger<>));
services.AddScoped(typeof(ILogger<>), typeof(BlazorLogger<>));
return services;
}
}
}
and in _Imports.razor
@using MyApp.Utilities.Logging
and Startup ConfigureServices
services.AddCustomBlazorLogging();
Usage matches that of the Microsoft logger so it shouldn't require any change to existing code
@inject ILogger<TabControl> _log
Note: If you use the Microsoft.Extensions.Logging.LoggerExtensions
methods you may need to define those extension method for your logger.
Note: ILoggingService in the example above is a custom interface we use. It is not required.
Maybe someone will find this useful in lieu of asp.net adding scoped logging support.
You can use IHttpContextAccessor
to resolve the authenticated user and any other scoped services you need to read data from. This example uses Serilog, but I'm sure it can be adapted to MS logging.
public static int Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
.UseSerilog((_, provider, logConfig) =>
{
logConfig.Enrich.With(new CustomLogEnricher(provider.GetRequiredService<IHttpContextAccessor>()));
});
public class CustomLogEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomLogEnricher(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
if (_httpContextAccessor.HttpContext is not { } httpContext) return;
if (httpContext.RequestServices.GetService<MyScopedDataService>() is { } myScopedDataService)
{
// TODO Enrich with scoped data, that may be manipulated by GUI and other scoped services.
}
if (httpContext.User is { } user)
{
logEvent.AddOrUpdateProperty(factory.CreateProperty("User",
new
{
DisplayName = user.GetDisplayName(),
Name = user.FindFirstValue(ClaimConstants.Name),
ObjectId = user.FindFirstValue(ClaimConstants.ObjectId),
Issuer = user.FindFirst(ClaimConstants.NameIdentifierId)?.Issuer,
},
destructureObjects: true));
}
}
}
Hello guys, In my blazor server app i would like to Inject some scoped services in my logger to trace some client information like circuitId (or other scoped data which allow me to identify the client). Otherwise it will be a mess in my logs, I will not be able to attach an exception to a specific client.
I already read here its a bad idea to have a scoped logger, however i really need this features, so i try with this :
However i have this error at startup
So the question, is it something can be achieved now ? (even if its dirty) or there is no way i can retrieve scoped information from the logger ?
thanks !