I am trying to figure out how to get Serilog to log the class name and method/line number, like I would see with log4cxx in C++.
I tried my best to grab all the relevant bits out of the real code I am working on and come up with a minimal example.
I've also been Googling Serilog left and right, but I am not finding good documentation. I suppose it is because there are so many libraries on top of the base serilog and each needs there own docs to tell me how to do things.
I can see the basics on configuration at https://github.com/serilog/serilog/wiki/Configuration-Basics , but this seems to use a TextWriter sink from a seperate Serilog library and a custom formatter, both of which I don't really understand.
I can also find examples on stack overflow that use the simple configuatation and the enrich call to log the class and method names.
C# ASP.NET Core Serilog add class name and method to log
I am not able to get it to log them. How can I get this to log the class and method name or line number while still using the custom formatter and TextWriter?
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Serilog;
using Serilog.Events;
using Serilog.Formatting;
namespace SerilogDemo {
// Someone else made this, I just changed the name to protect the innocent
public class SomeonesLogTextFormatter : ITextFormatter
{
public void Format(LogEvent logEvent, TextWriter output)
{
output.Write(logEvent.Level);
output.Write(": ");
logEvent.MessageTemplate.Render(logEvent.Properties, output);
output.WriteLine();
if (logEvent.Exception != null)
{
output.WriteLine(logEvent.Exception);
}
}
}
public class SomeClass
{
private Serilog.ILogger _log = Serilog.Log.ForContext<SomeClass>();
public SomeClass()
{
_log.Debug("SomeClass has been instantiated");
}
public void Foo()
{
_log.Debug("Foo has been called");
}
}
class Program
{
static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.TextWriter(textWriter: Console.Out, formatter: new SomeonesLogTextFormatter())
.CreateLogger();
var poop = new SomeClass();
poop.Foo();
}
}
}
The Serilog way of adding more information to a log message is by adding properties to the LogContext either manually or by using an Enricher that does that for you. Read more about Serilog Enrichment.
Serilog by default does not capture that information, and that can be quite expensive if you do for every single message, but the way to do it is by using C#'s Caller Information feature such as CallerMemberName, CallerFilePath, CallerLineNumber.
Here is an example, also copied below:
public static class SerilogWithCallerAttributes
{
public static void Main()
{
Serilog.Log.Logger = new LoggerConfiguration()
.WriteTo.ColoredConsole()
.CreateLogger();
GoDoSomething();
}
public static void GoDoSomething()
{
int score = 12;
Log.Information("Player scored: {Score}", CallerInfo.Create(), score);
}
}
public static class Log
{
public static void Information(string messageTemplate, CallerInfo callerInfo, params object[] propertyValues)
{
Serilog.Log.Logger
.ForHere(callerInfo.CallerFilePath, callerInfo.CallerMemberName, callerInfo.CallerLineNumber)
.Information(messageTemplate, propertyValues);
}
}
public static class LoggerExtensions
{
public static ILogger ForHere(
this ILogger logger,
[CallerFilePath] string callerFilePath = null,
[CallerMemberName] string callerMemberName = null,
[CallerLineNumber] int callerLineNumber = 0)
{
return logger
.ForContext("SourceFile", callerFilePath)
.ForContext("SourceMember", callerMemberName)
.ForContext("SourceLine", callerLineNumber);
}
}
public class CallerInfo
{
public string CallerFilePath { get; private set; }
public string CallerMemberName { get; private set; }
public int CallerLineNumber { get; private set; }
private CallerInfo(string callerFilePath, string callerMemberName, int callerLineNumber)
{
this.CallerFilePath = callerFilePath;
this.CallerMemberName = callerMemberName;
this.CallerLineNumber = callerLineNumber;
}
public static CallerInfo Create(
[CallerFilePath] string callerFilePath = null,
[CallerMemberName] string callerMemberName = null,
[CallerLineNumber] int callerLineNumber = 0)
{
return new CallerInfo(callerFilePath, callerMemberName, callerLineNumber);
}
}
Related
I´m using Autofac in my WPF application for dependency injection and can´t resolve this problem.
I have created this abstract class ListItemViewModelBase from which two classes PasswordItemViewModel and CardItemViewModel inherits.
ListItemViewModelBase.cs
public abstract class ListItemViewModelBase
{
protected readonly IMessenger _messenger;
public string Id { get; }
public string Name { get; }
public string Notes { get; }
protected ListItemViewModelBase(IMessenger messenger, string id, string name, string notes)
{
_messenger = messenger;
Id = id;
Name = name;
Notes = notes;
}
public abstract void SeeDetails();
}
PasswordItemViewModel.cs
public partial class PasswordItemViewModel : ListItemViewModelBase
{
public string UserName { get; }
public string Password { get; }
public string Website { get; }
public PasswordItemViewModel(IMessenger messenger, string id, string name, string userName, string password, string website, string notes) : base(messenger, id, name, notes)
{
UserName = userName;
Password = password;
Website = website;
}
[RelayCommand]
public override void SeeDetails()
{
_messenger.Send(new PasswordDetailMessage(this));
}
}
CardItemViewModel.cs
public partial class CardItemViewModel : ListItemViewModelBase
{
public string CardNumber { get; }
public int ExpMonth { get; }
public int ExpYear { get; }
public CardItemViewModel(IMessenger messenger, string id, string name, string cardNumber, int expMonth, int expYear, string notes) : base(messenger, id, name, notes)
{
CardNumber = cardNumber;
ExpMonth = expMonth;
ExpYear = expYear;
}
[RelayCommand]
public override void SeeDetails()
{
_messenger.Send(new CardDetailMessage(this));
}
}
My App.xaml.cs where the Autofac is configured looks like this:
App.xaml.cs
public partial class App : Application
{
public static IServiceProvider ServiceProvider { get; private set; }
[STAThread]
public static void Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
CreateGenericHost(host);
InitAppAndRun();
}
private static void InitAppAndRun()
{
var app = new App();
app.InitializeComponent();
app.MainWindow = ServiceProvider.GetRequiredService();
app.MainWindow!.Visibility = Visibility.Visible;
app.Run();
}
#region Host builder
private static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureServices(ConfigureServices)
.ConfigureContainer(ConfigureAutofacBuilder);
}
private static void ConfigureAutofacBuilder(HostBuilderContext ctx, ContainerBuilder builder)
{
builder.RegisterModule>();
builder.RegisterModule>();
var config = new ConfigurationBuilder();
config.AddJsonFile("autofac.json");
var module = new ConfigurationModule(config.Build());
builder.RegisterModule(module);
}
private static void CreateGenericHost(IHost host)
{
host.Start();
ServiceProvider = host.Services;
}
private static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
services.AddSingleton();
}
#endregion
}
When I try to start the application, program fails on using IHost host = CreateHostBuilder(args).Build(); and throws this error message:
Autofac.Core.Activators.Reflection.NoConstructorsFoundException: 'No accessible constructors were found for the type 'TestApp.Bases.ListItemViewModelBase'.'
Seems like Autofac can´t resolve non-public constructor of ListItemViewModelBase class. Is there a way to make Autofac resolve these type of non-public constructors?
Thanks,
Tobias
Response to reply from Orenico:
Here is autofac.json which contains basically nothing.
{
"modules": [],
"components": []
}
There are some typos in the sample code, like builder.RegisterModule>(), which won't compile and indicates there are registrations and other stuff going on that you're not showing us.
However:
You can't directly instantiate an abstract class. Like, you can't do new ListItemViewModelBase. That means you can't register it. You can register derived classes As it, but even if Autofac could find the constructor you'd still hit a brick wall because it's not a concrete class.
If Autofac is resolving a type by reflection it doesn't go all the way down the chain looking at constructors, it only looks at the thing you're resolving. That's why it sounds like you probably tried to register that abstract class.
If I had to guess, you have some assembly scanning registrations in those modules you're not showing us where you try registering all the view models and you didn't exclude the base class from the registrations.
In a WinForms app, there is Logger class that is a form designed for logging, so that any class can call it.
There is a static Configuration class, inside which a Logger lives.
Previous implementation
Various classes would call the logger like so:
public class ImportController
{
public void import()
{
try
{
// do the work...
}
catch (Exception ex)
{
Configuration.logger.log("Something failed");
Configuration.logger.log(ex);
}
}
}
Current implementation
The logger implements the following interface, which was extracted from it as part of refactoring to enable unit testing calling classes through dependency injection:
public interface ILogger
{
void (string message, [CallerMemberName] string member = "", [CallerLineNumberAttribute] int lineNumber = -1, string fileName = "");
void (Exception ex, [CallerMemberName] string member = "", [CallerLineNumberAttribute] int lineNumber = -1, string fileName = "");
}
As can be seen, the idea is to have it automatically log the calling class name and source file path.
The following is an example of an attempt to inject a logger into all classes that use it, in this instance the ImportController from above:
public class ImportControllerLogger
{
public void log(string message, [CallerMemberName] string member = "", [CallerLineNumber] int line_num = -1, string filename = "")
{
Configuration.log.log(string message, "ImportController", lineNumber, #"Controllers\ImportController.cs");
}
public void log(Exception exception, [CallerMemberName] string member = "", [CallerLineNumber] int line_num = -1, string filename = "")
{
Configuration.log.log(exception, "ImportController", lineNumber, #"Controllers\ImportController.cs");
}
}
public class ImportController
{
ILogger _logger;
public ImportController(ILogger logger)
{
this._logger = logger;
}
public void import()
{
try
{
// do the work...
}
catch (Exception ex)
{
_logger.log("Something failed");
_logger.log(ex);
}
}
}
Questions
Is this the correct approach to decouple the logger from all classes that use it?
It seems it might be better to create a single "LoggerHelper" class, that abstracts away the logger so that any class can make a call to it, instead of creating such a class for every calling class. How can the name of the calling class and source file path for the calling class be logged, in a proper way, without resorting to manually specifying it for each class? It worked in the previous implementation with the attributes.
I also had to implement something like that.
The code is simplified.
ILogger
public interface ILogger
{
event EventHandler<LogEventArgs> OnLogAdded;
Type Type { get; }
void Log(string message);
}
Logger
public class Logger : ILogger
{
public Type Type { get; }
public Logger(Type type)
{
Type = type;
}
public event EventHandler<LogEventArgs> OnLogAdded;
public void Log(string message)
{
EventHandler<LogEventArgs> handler = OnLogAdded;
handler?.Invoke(this, new LogEventArgs(message));
}
}
LogProvider
public static class LogProvider
{
private static List<ILogger> loggers = new List<ILogger>();
public static ILogger CreateLogger<T>()
{
if (loggers.Select(x => x.Type.Equals(typeof(T))).Count() > 0)
{
throw new Exception($"There is allready a logger for the type {typeof(T)}");
}
ILogger logger = new Logger(typeof(T));
logger.OnLogAdded += OnLogAdded;
loggers.Add(logger);
return logger;
}
private static void OnLogAdded(object sender, LogEventArgs e)
{
//add log to your config
}
}
And you can use it like this:
public class SampleView
{
private ILogger logger = LogProvider.CreateLogger<SampleView>();
public SampleView()
{
logger.Log("TestLog");
}
}
I don't know if this is the best implementation, but it works like a charm.
I'm having a bit of trouble with using Azure storage. I have an implementation at the moment which is fine but I want to expand it so that I am able to use multiple storage accounts/containers in one solution. I can't get my head around how to do that and still allow for dependency injection. I also need to be able to pass in settings which define the connection string and container name
This is how I'm doing it at the moment:
builder.Services.AddSingleton<IAzureStorageClient, AzureStorageClient>();
builder.Services.Configure<AzureStorageSettings>(configuration.GetSection("AzureStorageSettings"));
and then in the constructor
public AzureStorageClient(IOptions<AzureStorageSettings> options)
{
var azureStorageSettings = options.Value;
var cloudStorageAccount = GetCloudStorageAccount(azureStorageSettings.ConnectionString);
_blobClient = cloudStorageAccount.CreateCloudBlobClient();
_blobContainer = GetBlobContainer(azureStorageSettings.ContainerName);
}
I've read a lot of similar posts which mention using named registrations but I am using the built in IoC container and it doesn't allow for that. I've also seen posts saying to use a factory which looks good but I am hoping to package this logic and share it among different solutions and the factory solution would require a lot of configuration which I would like to avoid to make the package easy to consume.
Update:
I made the settings an interface to force it to be implemented each time it is required and I used the generic T to pass that into my storage client as follows:
public sealed class AzureStorageClient<T> : IAzureStorageClient<T> where T : class, IAzureStorageSettings, new()
public AzureStorageClient(IOptions<T> options)
{
var azureStorageSettings = options.Value;
var cloudStorageAccount = GetCloudStorageAccount(azureStorageSettings.ConnectionString);
_blobClient = cloudStorageAccount.CreateCloudBlobClient();
_blobContainer = GetBlobContainer(azureStorageSettings.ContainerName);
}
Then it could be injected like this:
builder.Services.AddSingleton<IAzureStorageClient<SpecificStorageSettings>, AzureStorageClient<SpecificStorageSettings>>();
Just an example, you can use settings like this:
Settings.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace UseDifferentSettings
{
public abstract class Settings
{
public abstract string connectingstring {
get;
}
public abstract string containername {
get;
}
public abstract string blobname {
get;
}
}
}
Setting1.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace UseDifferentSettings
{
class Setting1 : Settings
{
string _connectingstring = "DefaultEndpointsProtocol=https;AccountName=xxx;EndpointSuffix=core.windows.net";
string _containername = "video1";
string _blobname = "test.txt";
public override string connectingstring
{
get { return _connectingstring; }
}
public override string containername
{
get { return _containername; }
}
public override string blobname
{
get { return _blobname; }
}
}
}
Setting2.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace UseDifferentSettings
{
class Setting2 : Settings
{
private string _connectingstring = "DefaultEndpointsProtocol=https;AccountName=xxx;EndpointSuffix=core.windows.net";
private string _containername = "test";
private string _blobname = "test.txt";
public override string connectingstring
{
get { return _connectingstring; }
}
public override string containername
{
get { return _containername; }
}
public override string blobname
{
get { return _blobname; }
}
}
}
UploadToStorage.cs
using Azure.Storage.Blobs;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace UseDifferentSettings
{
public class UploadToStorage
{
Settings setting;
public UploadToStorage(Settings setting) {
this.setting = setting;
}
public void GoUpload() {
string connectingstring = setting.connectingstring;
string containername = setting.containername;
string blobname = setting.blobname;
string filecontent = "This is my test file content";
byte[] array = Encoding.ASCII.GetBytes(filecontent);
MemoryStream filestream = new MemoryStream(array);
BlobServiceClient blobServiceClient = new BlobServiceClient(connectingstring);
BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containername);
BlobClient blobClient = containerClient.GetBlobClient(blobname);
blobClient.Upload(filestream);
}
}
}
Program.cs(The main method class)
using System;
namespace UseDifferentSettings
{
class Program
{
static void Main(string[] args)
{
Settings setting1 = new Setting1();
Settings setting2 = new Setting2();
UploadToStorage uploadtostorage = new UploadToStorage(setting1);
uploadtostorage.GoUpload();
Console.WriteLine("Hello World!");
}
}
}
I'm trying to simplify my json config file handler. I want to 'expose' the properties defined in the configuration class. I can load propierties like:
string s = (ConfigurationHandler.Load(path).SomeStringValue);
now i want to achieve a similar behavior like commented in the code.
using Newtonsoft.Json;
using System.IO;
namespace JsonConfigHandler
{
class Program
{
static void Main(string[] args)
{
Configuration configuration = new Configuration { SomeBoolValue = true, SomeIntValue = 100, SomeStringValue = "Hello" };
string path = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
ConfigurationHandler.Save(configuration, $#"{path}\config.json");
// now i only want to update one configuration element like SomeStringValue and then save it - how can i simplify this?
// in this sample i do already have the Configuration loaded. But in a complex solution, whenever i quickly want to update one/multiple settings
// i always have to load the configuraiton first, update the loaded configuration.key/element, set the value for the key/element, save it again.
// how can i 'expose' these setting-elements/keys to easily update them in one line of code like:
// ConfigurationHandler.UpdateConfigSetting("Key", "Value");
configuration.SomeStringValue = "Goodbye";
ConfigurationHandler.Save(configuration, $#"{path}\config.json");
}
}
public static class ConfigurationHandler
{
public static Configuration Load(string path)
{
return JsonConvert.DeserializeObject<Configuration>(File.ReadAllText(path));
}
public static void Save(Configuration conf, string path)
{
using (StreamWriter streamWriter = new StreamWriter(path))
{
streamWriter.Write(JsonConvert.SerializeObject(conf, Formatting.Indented));
}
}
}
public class Configuration
{
public string SomeStringValue { get; set; }
public int SomeIntValue { get; set; }
public bool SomeBoolValue { get; set; }
}
}
I would suggest to hold the current configuration in the configuration handler and use reflection for updating the values.
See: https://dotnetfiddle.net/8ToVkK
The UpdateConfig method would look something like this then
// Change config for a single key value pair.
public static void UpdateConfig(string key, object value)
{
UpdateConfigSetting(key, value);
Save(Config);
}
// Change config for a set of key value pairs.
public static void UpdateConfig(Dictionary<string, object> configChanges)
{
foreach (var configChange in configChanges)
{
UpdateConfigSetting(configChange.Key, configChange.Value);
}
Save(Config);
}
// Use reflection to change the value in the configuration instance.
private static void UpdateConfigSetting(string key, object value)
{
try
{
var property = Config.GetType().GetProperty(key);
property.SetValue(Config, value, null);
}
catch (Exception ex)
{
// That probably happens, if the given property does not exist on
// Configuration or the type is different and cannot be converted.
// Think about how to react in that case depending on your application.
throw ex;
}
}
Edit: Matt, that does indeed solves some (most) of my problems, thank you. Now the only lingering issue of how do I do this in WPF? I have a custom part based off of a UserControl but there is no way in WPF to do :
[Import]<my:SomeCustomControl>
so the cascade doesn't work in this instance.
/Edit
I am having an issue [Import]ing various MEF components in my project. Do I have to use a CompositionContainer in every class I use? In the code below, a null reference exception is thrown in the method Helper.TimesTwo() but when I call logger.Log() in the Program class, everything works. Any help would be greatly appreciated.
(this will compile and run as a console app).
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
var p = new Program();
p.Run();
}
[Import]
private ILog logger { get; set; }
public void Run()
{
var catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);
var batch = new CompositionBatch();
batch.AddPart(this);
container.Compose(batch);
logger.Log("hello");
var h = new Helper();
logger.Log(h.TimesTwo(15).ToString());
Console.ReadKey();
}
}
class Helper
{
[Import]
private IDouble doubler { get; set; }
private Helper()
{
// do I have to do all the work with CompositionContainer here again?
}
public double TimesTwo(double d)
{
return doubler.DoubleIt(d);
}
}
interface ILog
{
void Log(string message);
}
[Export(typeof(ILog))]
class MyLog : ILog
{
public void Log(string message)
{
Console.WriteLine("mylog: " + message);
}
}
interface IDouble
{
double DoubleIt(double d);
}
[Export(typeof(IDouble))]
class MyDoubler : IDouble
{
public double DoubleIt(double d)
{
return d * 2.0;
}
}
}
I think the trick is to make use of the fact that MEF will cascade its imports. So if you import your Helper instance rather than declaring it as a local variable, any imports that the Helper requires will be satisfied.
[Import]
public Helper MyHelper { get; set; }
public void Run()
{
var catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);
var batch = new CompositionBatch();
batch.AddPart(this);
container.Compose(batch);
logger.Log("hello");
logger.Log(MyHelper.TimesTwo(15).ToString());
Console.ReadKey();
}
I'm sure there's a way to have it satisfy any imports in a local variable, but I like using the "cascaded imports" feature like that.
No you can't do that. You could look into using attached properties though for this. With an attached property you can have the container compose the element that the attached property is added to. Another option would be markup extensions.
Glenn
Try changing
[Import]
private ILog logger { get; set; }
to
[Import]
public ILog logger { get; set; }
It might work.