I am trying to create a custom logger based on serilog. The application i am building is based on net6 blazor-server-side.
The goal is that every time a user logs into the application, I create a specific log file for him.
First I create a dependency injection in the program.cs
file Program.cs
builder.Services.AddScoped<ICustomLogger>( s => new CustomLogger());
In the Customlogger class, I initialize the loggerconfiguration in the constructor
file CustomLogger.cs
private ILogger<CustomLogger> _logger;
protected readonly LoggerConfiguration _loggerConfig;
public CustomLogger()
{
_loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
}
In the Login.razor , once the login is successful, I call the CreateLogger method, passing the username as a parameter (this is to create a specific folder)
file CustomLogger.cs
public void CreateLogger(string username)
{
var l = _loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30).CreateLogger();
_logger = new SerilogLoggerFactory(l).CreateLogger<CustomLogger>(); // creates an instance of ILogger<CustomLogger>
}
Beyond that, I've created methods to write the various log levels
file CustomLogger.cs
public void LogInformation(string m)
{
_logger.LogInformation(m);
}
public void LogError(string m)
{
_logger.LogError(m);
}
public void LogWarning(string m)
{
_logger.LogWarning(m);
}
The Customlogger class is bound to the ICustomLogger interface
file ICustomLogger.cs
public interface ICustomLogger
{
void LogInformation(string m);
void LogError(string m);
void LogWarning(string m);
void CreateLogger(string username);
ILogger<CustomLogger> GetLogger();
}
For the moment I see that the system works, if I connect with a user, his folder and the file are created, and so on for each user.
My question is :
Could this approach cause problems?
Is it already possible to do this via Serilog?
Thanks for your time
N.
UPDATE
the system works well in the login page, for each user to create his own logger, but as soon as I move to the index page, the constructor of the CustomLogger class is called and the ILogger is null.
I thought AddScoped was only called once for "session"
Is it possible to call AddSingleton every time the user logs in, that way the specific dependency remains for as long as needed?
UPDATE 2
I changed the injection, now I use AddSingleton
Program.cs
builder.Services.AddSingleton<IOramsLoggerService>(s => new OramsLoggerService());
Inside the OramsLoggerService class, I created a list of loggers, which is filled at each login
OramsLoggerSerivce.cs
public class OramsLoggerService : IOramsLoggerService
{
private List<OramsLogger> loggers;
public OramsLoggerService()
{
loggers = new List<OramsLogger>();
}
public void CreateLogger(string? username)
{
if (string.IsNullOrEmpty(username))
{
throw new ArgumentNullException("username");
}
if (loggers.Where(x => x.Username == username).Count() > 0)
{
return;
}
// characters not allowed in the folder name
string originaUsername = username;
username = username.Replace("<", "-");
username = username.Replace(">", "-");
username = username.Replace(":", "-");
username = username.Replace("/", "-");
username = username.Replace("\\", "-");
username = username.Replace("|", "-");
username = username.Replace("?", "-");
username = username.Replace("*", "-");
username = username.Replace("\"", "-");
var loggerConfig = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug();
var l = loggerConfig.WriteTo.File($"./Logs/{username}/log_.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 100).CreateLogger();
var logger = new SerilogLoggerFactory(l).CreateLogger<OramsLogger>(); // creates an instance of ILogger<OramsLogger>
loggers.Add(new OramsLogger(logger, originaUsername));
}
}
by doing so, I have all the loggers created in the other pages available.
The OramsLogger class contains the logger and the username.
I use the username to search the list, I think I'll change it to the id in the future.
OramsLogger.cs
public class OramsLogger
{
public ILogger<OramsLogger> logger;
public string Username { get; set; }
public OramsLogger(ILogger<OramsLogger> l, string username)
{
logger = l;
Username = username;
}
}
For the moment I have created 1000 dummy users when I login with my user and it seems to work.
Could this be a good approach?
For user-level custom logs, this method is appropriate.
There are no particular concerns.
Related
I am creating a Blazor app with basic authentication.
The problem is the following: When I launch the application, I cannot access anything, I am not connected (registration is only done via a user who has the right). So I want to create an admin: admin account when the database is initialized.
Here is what I did:
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<T_Utilisateur> _userManager){
...
app.InitBaseDeDonnes(Configuration["DefaultApplicativePassword"], _userManager);
...
}
InitData.cs
public async static void InitBaseDeDonnes(this IApplicationBuilder _app, string _defaultPassword, UserManager<T_Utilisateur> _userManager){
...
await _userManager.CreateAsync(user, "admin");
...
}
But here is the error that returned to me (about the "_userManager" object) :
Cannot access a disposed object.
Do you know which method should I use to create this admin user?
Extracting the manager from the services is a possibility to access the class allowing this to manage the users. Pass this class from Startup.cs not working.
public async static void InitBaseDeDonnes(this IApplicationBuilder _app, string _defaultPassword) {
...
int nbUser = await UtilisateurDal.CountAsync();
if (nbUser < 1) {
using (IServiceScope scope = _app.ApplicationServices.CreateScope()) {
UserManager<T_Utilisateur> userManager = scope.ServiceProvider.GetRequiredService<UserManager<T_Utilisateur>>();
Task.Run(() => InitAdminUser(userManager, "admin#domain.com")).Wait();
}
}
...
}
private static async Task InitAdminUser(UserManager<T_Utilisateur> _userManager, string _email) {
T_Utilisateur user = new T_Utilisateur {
Id = Guid.NewGuid(),
UserName = _email,
Email = _email,
EmailConfirmed = true,
DateDeCreation = DateTime.Now,
SecurityStamp = Guid.NewGuid().ToString()
};
await _userManager.CreateAsync(user, Constants.MotDePasseDefaut);
}
I'm creating a webjob in .net core 3.1. In this project I have a function that is timer activated which should read the number of messages in a queue Q1 and if empty, put a message in Q2 as well as trigger a rest call to an API.
In order to check how many messages are in the API I need to access the AzureWebJobsStorage in my appsettings.json and then the url which is also in the settings.
Program.cs
class Program
{
static async Task Main()
{
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
b.AddTimers();
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
});
builder.ConfigureAppConfiguration((context, b) =>
{
b.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
});
builder.ConfigureServices((context, services) =>
{
var mySettings = new MySettings
{
AzureWebJobsStorage = context.Configuration.GetValue<string>("AzureWebJobsStorage"),
AzureWebJobsDashboard = context.Configuration.GetValue<string>("AzureWebJobsDashboard"),
url = context.Configuration.GetValue<string>("url"),
};
services.AddSingleton(mySettings);
});
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Fuctions.cs
public class Functions
{
public static void UpdateChannels([QueueTrigger("Q1")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void WhatIsThereToUpdate([QueueTrigger("Q2")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void CronJob([TimerTrigger("0 * * * * *")] TimerInfo timer, [Queue("Q2")] out string message, ILogger logger, MySettings mySettings)
{
message = null;
// Get the connection string from app settings
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
// Instantiate a QueueClient which will be used to create and manipulate the queue
QueueClient queueClient = new QueueClient(connectionString, "Q1");
if (queueClient.Exists())
{
QueueProperties properties = queueClient.GetProperties();
// Retrieve the cached approximate message count.
int cachedMessagesCount = properties.ApproximateMessagesCount;
// Display number of messages.
logger.LogInformation($"Number of messages in queue: {cachedMessagesCount}");
if (cachedMessagesCount == 0)
message = "Hello world!" + System.DateTime.Now.ToString(); //here I would call the REST API as well
}
logger.LogInformation("Cron job fired!");
}
}
appsettings.json
{
"AzureWebJobsStorage": "constr",
"AzureWebJobsDashboard": "constr",
"url": "url"
}
My Settings
public class MySettings
{
public string AzureWebJobsStorage { get; set; }
public string AzureWebJobsDashboard { get; set; }
public string url { get; set; }
}
However when I run this I get the following error:
Error indexing method 'Functions.CronJob'
Microsoft.Azure.WebJobs.Host.Indexers.FunctionIndexingException: Error indexing method 'Functions.CronJob'
---> System.InvalidOperationException: Cannot bind parameter 'mySettings' to type MySettings. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
In addition to what is shown in the above codes I also tried using ConfigurationManager and Environment.GetEnvironmentVariable, both methods gave me null when I tried to read the values. For example ConfigurationManager.AppSettings.GetValues("AzureWebJobsStorage").
I also tried to register IConfiguration as a service services.AddSingleton(context.Configuration); and inject it in the parameters (instead of MySettings), but it also gave me the same binding error.
I'm really at a loss here, I've scoured the SO archives trying to find a solution and I think I tried everything I saw gave people positive results, but unfortunately I wasn't as lucky as the other posters.
Any guidance is much appreciated.
Edited to add my packages
In case it helps anyone, I'm using the following
Azure.Storage.Queues (12.4.0)
Microsoft.Azure.WebJobs.Extensions (3.0.6)
Microsoft.Azure.WebJobs.Extensions.Storage (4.0.2)
Microsoft.Extensions.Logging.Console (3.1.7)
When using DI, I suggest you use non-static method and constructor inject.
Here is the Functions.cs:
public class Functions
{
private readonly MySettings mySettings;
public Functions(MySettings _mySettings)
{
mySettings = _mySettings;
}
public void ProcessQueueMessage([TimerTrigger("0 */1 * * * *")] TimerInfo timer, [Queue("queue")] out string message, ILogger logger)
{
message = null;
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
}
}
No code change in other .cs file.
Here is the test result:
This one is a follow up question to Dependency Injection using Unity
So , as a set up I have a CustomConfiguration.cs file which is supposed to populate from a config section in my web.config file
This is the signature for the file
public class CustomConfiguration : ICustomProcessorConfig, IEmailConfig, IReportConfig
{
#region Properties
private CustomProcessorConfig ConfigSection { get; set; }
#endregion
#region Constructors (1)
public CustomConfiguration()
{
ConfigSection = ConfigurationManager.GetSection("customConfiguration") as ConfigSection;
}
#endregion Constructors
#region ICustomConfiguration Members
public string Text { get { return ConfigSection.Text; } }
public string Subject { get { return ConfigSection.Subject; } }
public string SmtpHost { get { return ConfigSection.EmailSettings.SmtpHost; } }
public int SmtpPort { get { return ConfigSection.EmailSettings.SmtpPort; } }
These are the 3 files I have for Email Generation :
public interface IEmailManager
{
void SendEmail(string toAddress, string fromAddress, string subject, string body, bool htmlBody);
}
public interface IEmailConfig
{
string SmtpHost { get; }
int SmtpPort { get; }
}
And Finally I have the Email Manager which inherits the IEmailManager interface
public class EmailManager: IEmailManager
{
#region Constructors (1)
public EmailManager(IEmailConfiguration configuration)
{
CurrentSmtpClient = new SmtpClient
{
Host = configuration.SmtpHost,
Port = configuration.SmtpPort,
Credentials =
new NetworkCredential(configuration.UserName, configuration.Password)
};
}
#endregion Constructors
// send Mail is also implemented
}
Coming back to the previous question I have my set up like :
Container = new UnityContainer();
Container.RegisterType<ICustomConfiguration,CustomConfiguration>(new ContainerControlledLifetimeManager());
Container.RegisterType<IEmailManager, EmailManager>(new ContainerControlledLifetimeManager());
Container.RegisterType<IReportGenerator, ReportGenerator>(new ContainerControlledLifetimeManager());
Configuration = Container.Resolve<ICustomConfiguration>();
Emailer = Container.Resolve<IEmailManager>();
ReportGenerator = Container.Resolve<IReportGenerator>();
I'm getting a ResolutionFailedExceptionsaying The parameter configuration could not be resolved when attempting to call constructor for EmailManager.
I had a different DI setup and I would need the configuration information from IEmailConfig still. Is there a way of going past this ? I need the config information to proceed with sending the email as you can guess with my setup.
Am I binding different repo to my Container ? Or how should I change my EmailManager code ?
You need to register the IEmailConfig interface with the CustomConfiguration class in the container.
Container.RegisterType<IEmailConfig , CustomConfiguration >(new ContainerControlledLifetimeManager());
IEmailConfiguration missing mapping in the unity container. Need to add the concrete class that maps this interface
It's an outdated article, but http://msdn.microsoft.com/en-us/library/ff650308.aspx#paght000026_step3 illustrates what I want to do. I've chosen Nancy as my web framework because of it's simplicity and low-ceremony approach. So, I need a way to authenticate against Active Directory using Nancy.
In ASP.NET, it looks like you can just switch between a db-based membership provider and Active Directory just by some settings in your web.config file. I don't need that specifically, but the ability to switch between dev and production would be amazing.
How can this be done?
Really the solution is much simpler than it may seem. Just think of Active Directory as a repository for your users (just like a database). All you need to do is query AD to verify that the username and password entered are valid. SO, just use Nancy's Forms Validation and handle the connetion to AD in your implementation of IUserMapper. Here's what I came up with for my user mapper:
public class ActiveDirectoryUserMapper : IUserMapper, IUserLoginManager
{
static readonly Dictionary<Guid, long> LoggedInUserIds = new Dictionary<Guid, long>();
readonly IAdminUserValidator _adminUserValidator;
readonly IAdminUserFetcher _adminUserFetcher;
readonly ISessionContainer _sessionContainer;
public ActiveDirectoryUserMapper(IAdminUserValidator adminUserValidator, IAdminUserFetcher adminUserFetcher, ISessionContainer sessionContainer)
{
_adminUserValidator = adminUserValidator;
_adminUserFetcher = adminUserFetcher;
_sessionContainer = sessionContainer;
}
public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
{
_sessionContainer.OpenSession();
var adminUserId = LoggedInUserIds.First(x => x.Key == identifier).Value;
var adminUser = _adminUserFetcher.GetAdminUser(adminUserId);
return new ApiUserIdentity(adminUser);
}
public Guid Login(string username, string clearTextPassword, string domain)
{
var adminUser = _adminUserValidator.ValidateAndReturnAdminUser(username, clearTextPassword, domain);
var identifier = Guid.NewGuid();
LoggedInUserIds.Add(identifier, adminUser.Id);
return identifier;
}
}
I'm keeping a record in my database to handle roles, so this class handles verifying with AD and fetching the user from the database:
public class AdminUserValidator : IAdminUserValidator
{
readonly IActiveDirectoryUserValidator _activeDirectoryUserValidator;
readonly IAdminUserFetcher _adminUserFetcher;
public AdminUserValidator(IAdminUserFetcher adminUserFetcher,
IActiveDirectoryUserValidator activeDirectoryUserValidator)
{
_adminUserFetcher = adminUserFetcher;
_activeDirectoryUserValidator = activeDirectoryUserValidator;
}
#region IAdminUserValidator Members
public AdminUser ValidateAndReturnAdminUser(string username, string clearTextPassword, string domain)
{
_activeDirectoryUserValidator.Validate(username, clearTextPassword, domain);
return _adminUserFetcher.GetAdminUser(1);
}
#endregion
}
And this class actually verifies that the username/password combination exist in Active Directory:
public class ActiveDirectoryUserValidator : IActiveDirectoryUserValidator
{
public void Validate(string username, string clearTextPassword, string domain)
{
using (var principalContext = new PrincipalContext(ContextType.Domain, domain))
{
// validate the credentials
bool isValid = principalContext.ValidateCredentials(username, clearTextPassword);
if (!isValid)
throw new Exception("Invalid username or password.");
}
}
}
My question is I wanna apply strategy pattern to save log
from Image above
I will use replace my class to image
Context = Log Class
Istrategy = ILog
concreteStrategyA = CreateSituation
concreteStrategyB = NameSituation
concreteStrategyC = StatusSituation
[Additional from image]
concreteStrategyD = PriceSituation
concreteStrategyE = DiscountSituation
In ILog have these method
SaveLog(Log log);
each concreteStrategyA,B,C,D,E implement ILog
public class CreateSituation : ILog
{
public void SaveLog(Log log)
{
using(var context = new ProductContext())
{
log.Message = "This product is created";
context.productLog.InsertonSubmit(log);
context.submitChange();
}
}
}
public class NameSituation : ILog
{
public void SaveLog(Log log)
{
using(var context = new ProductContext())
{
log.Message = "this produce has updated name from \"oldProduct\" to \"newProduct\"";
context.productLog.InsertonSubmit(log);
context.submitChange();
}
}
}
public class StatusSituation : ILog
{
public void SaveLog(Log log)
{
using(var context = new ProductContext())
{
log.Message = "this produce has updated status from \"In stock\" to \"Sold Out\"";
context.productLog.InsertonSubmit(log);
context.submitChange();
}
}
}
public class PriceSituation : ILog
{
public void SaveLog(Log log)
{
using(var context = new ProductContext())
{
log.Message = "this produce has updated price from $10 to $150";
context.productLog.InsertonSubmit(log);
context.submitChange();
}
}
}
public class DiscountSituation : Ilog
{
public void SaveLog(Log log)
{
using(var context = new ProductContext())
{
log.Message = "this product has updated discount price from $0 to $20";
context.productLog.InsertonSubmit(log);
context.submitChange();
}
}
}
In the future can have more than present situation
and these classes that I show i a good way to solve these problem
Yes, this is a scenario that could use a strategy pattern.
However, to me it seems to be a just little bit too much to use a completely different logger instance per logging scenario. But without knowing your architecture this is only my personal gut feeling.
As an alternative approach I could imagine implementing a factory for just assembling the appropriate logging message, depending on the logging situation.
[EDIT]
Additionally I would recommend using some framework like log4net that make logging very easy and intuitive. log4net, for example, differentiates between several logging levels like WARN, INFO, DEBUG, ERROR,orFATAL that allow you to fine-tune your output.