Before I begin showing code, I have two main questions which I feel could help me.
- What is a service or factory, I've heard of them but why use them?
- Should I use one shared instance of ILogger throughout my whole application, I only plan on persisting errors and I can get the reporter from the stacktrace.
So,
I decided to write a simple logger for my dotnet core application. This logger will write messages to the console, and optionally save them to a file. I intend on only saving errors to a file.
For every class that needs a logger, I implement ILogger like so
var typeOfClass = typeof(myClass);
var myClass = new MyClass(
new HybridLogger(
new ConsoleLogger(typeOfClass),
new FileLogger(typeOfClass)
)
)
I thought that was too much, so I simplified it
public static class LogProvider
{
public static ILogger CreateLogger(Type type)
{
return new HybridLogger(new ConsoleLogger(type), new FileLogger(type));
}
}
This feels much better
var myClass = new MyClass(LogProvider.GetLogger(typeof(MyClass));
I have an interface, ILogger which all implementations implement. This ensures all implementations have the correct methods and I can use ILogger to encapsulate the concrete implementation
public interface ILogger
{
void Trace(string message, bool logToFile = false);
void Warning(string message, bool logToFile = false);
void Debug(string message, bool logToFile = false);
void Success(string message, bool logToFile = false);
void Error(string message, bool logToFile = false);
void Exception(Exception e, bool logToFile = true);
}
Let's start with the first out of the 3 implementations, ConsoleLogger. This one Console.WriteLine's messages to the console.
The implementation has different methods for different LogLevel's and will use different ConsoleColor's depending on the LogLevel chosen.
You can think of my LogType enum as LogLevel when reading the code below.
public class ConsoleLogger : ILogger
{
private readonly Type _owner;
public ConsoleLogger(Type owner)
{
_owner = owner;
}
private readonly Dictionary<LogType, ConsoleColor> _colorsForLogType = new Dictionary<LogType, ConsoleColor>() {
{LogType.Trace , ConsoleColor.White},
{LogType.Success , ConsoleColor.Green},
{LogType.Warning , ConsoleColor.Yellow},
{LogType.Debug , ConsoleColor.Cyan},
{LogType.Error , ConsoleColor.Red},
};
public void Trace(string message, bool logToFile = false)
{
Log(message, LogType.Trace, logToFile);
}
public void Warning(string message, bool logToFile = false)
{
Log(message, LogType.Warning, logToFile);
}
public void Debug(string message, bool logToFile = false)
{
Log(message, LogType.Debug, logToFile);
}
public void Success(string message, bool logToFile = false)
{
Log(message, LogType.Success, logToFile);
}
public void Error(string message, bool logToFile = false)
{
Log(message, LogType.Error, logToFile);
}
public void Exception(Exception e, bool logToFile = true)
{
Log("An error occurred: " + Environment.NewLine + e, LogType.Error, logToFile);
}
private void Log(string message, LogType type, bool logToFile = false)
{
var oldColor = Console.ForegroundColor;
var newColor = _colorsForLogType[type];
Console.ForegroundColor = newColor;
Console.WriteLine($"[{DateTime.Now:MM/dd HH:mm:ss}] " + message);
Console.ForegroundColor = oldColor;
}
}
Now here is the FileLogger - this is used to persist errors logs so that I can read them later, I use this library in a server so its unwatched for most of its lifetime.
public class FileLogger : ILogger
{
private readonly Type _owner;
public FileLogger(Type owner)
{
_owner = owner;
}
private readonly Dictionary<LogType, string> _fileNameForLogType = new Dictionary<LogType, string>() {
{LogType.Trace , "trace.log"},
{LogType.Success , "success.log"},
{LogType.Warning , "warn.log"},
{LogType.Debug , "debug.log"},
{LogType.Error , "error.log"},
};
public void Trace(string message, bool logToFile = false)
{
// Should only need the Log method in this class but
// I need to comply to ILogger, I'll keep them until
// I come up with a solution, an ideal one would be
// I just call Log(message, type) from the HybridLogger
// But thinking, is extra methods better? Encapsulation for LogType...
LogToFile(message, LogType.Trace);
}
public void Warning(string message, bool logToFile = false)
{
LogToFile(message, LogType.Warning);
}
public void Debug(string message, bool logToFile = false)
{
LogToFile(message, LogType.Debug);
}
public void Success(string message, bool logToFile = false)
{
LogToFile(message, LogType.Success);
}
public void Error(string message, bool logToFile = false)
{
LogToFile(message, LogType.Error);
}
public void Exception(Exception e, bool logToFile = true)
{
LogToFile(e.ToString(), LogType.Exception);
}
private void LogToFile(string message, LogType type)
{
FileUtilities.WriteToFile(
Path.GetFullPath(FileUtilities.GetStoragePath(), "/logging/" + _fileNameForLogType[type]),
$"Occurred at [{DateTime.Now:MM/dd HH:mm:ss}] in [{_owner.FullName}]: " + Environment.NewLine + message + Environment.NewLine
);
}
}
Lastly, I thought maybe I want to write to the console AND persist it to a file, so maybe an implementation that does two in one? I feel like there could be a better way to merge implementations, so if anyone has any bright ideas you can let me know.
public class HybridLogger : ILogger
{
private readonly ILogger _consoleLogger;
private readonly ILogger _fileLogger;
public HybridLogger(ILogger consoleLogger, ILogger fileLogger)
{
_consoleLogger = consoleLogger;
_fileLogger = fileLogger;
}
public void Trace(string message, bool logToFile = false)
{
_consoleLogger.Trace(message, logToFile);
if (logToFile)
{
_fileLogger.Success(message);
}
}
public void Warning(string message, bool logToFile = false)
{
_consoleLogger.Warning(message, logToFile);
if (logToFile)
{
_fileLogger.Success(message);
}
}
public void Debug(string message, bool logToFile = false)
{
_consoleLogger.Debug(message, logToFile);
if (logToFile)
{
_fileLogger.Success(message);
}
}
public void Success(string message, bool logToFile = false)
{
_consoleLogger.Success(message, logToFile);
if (logToFile)
{
_fileLogger.Success(message);
}
}
public void Error(string message, bool logToFile = false)
{
_consoleLogger.Error(message, logToFile);
if (logToFile)
{
_fileLogger.Success(message);
}
}
public void Exception(Exception e, bool logToFile = true)
{
_consoleLogger.Exception(e, logToFile);
if (logToFile)
{
_fileLogger.Success(e.ToString());
}
}
}
NLog. \$\endgroup\$