log4net complex type logging - c#

I want to log complex type object in c# and to use log4net for logging. What is the correct approach to do this?I use filelogappender.So I want to see my entity in log.Should I do it to use stringbuilder class and convert my entity to string or serialize it to json or is log4net has a ability to this?
public class Person
{
Name {get;set;}
Surname{get;set;}
}
Person personobject=new Person("MyName","MySurname");
log4net.Log.Warn(personobject);

You cannot log complex types, the reason is very simple:
The logging mechanism perform an hard enough work - you have to open stream and close stream every time you log information.
The data you stream is mostly text, there is no easy way to parse a complex object into text representation so what you see, is the 'shallow' text representation of the type: 'Object object'.
The easy workaround is to serialize the object manually as JSON string, this way:
log4net.Log.Warn(Newtonsoft.Json.JsonConvert.SerializeObject(personobject));
This will serialize your type as string, not as object and so, you will be able to log any complex type you want.

I'm with the first comment on this if you wanna log complex objects to file or sql, the best way would be via a json conversion string using the newtonsoft library (nuget package)
you can then wrap and unwrap objects at leisure or change data on the fly (json will save you alot of time and effort.) you can also pass it back to the front end for console logging or debugging and see the object you just logged.

Like #micah I suggest JSON, this time with an existing solution: log4net.Ext.Json. Once you install the package, you can just add a SerializedLayout to any appender:
<appender...>
<layout type='log4net.Layout.SerializedLayout, log4net.Ext.Json'>
</layout>
</appender>

You can create a custom layout that formats log message:
StructLayout
public class StructLayout : PatternLayout
{
public override void Format(TextWriter writer, LoggingEvent loggingEvent)
{
LoggingEvent newLoggingEvent;
var message = loggingEvent.MessageObject;
if (MessageObjectBuilder.TryBuildMessageObject(message, out object newMessage))
{
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var callerStackBoundaryDeclaringType = loggingEvent.GetType().GetField("m_callerStackBoundaryDeclaringType", bindingFlags)?.GetValue(loggingEvent);
newLoggingEvent =
new LoggingEvent(callerStackBoundaryDeclaringType as Type,
loggingEvent.Repository,
loggingEvent.LoggerName,
loggingEvent.Level,
newMessage as SystemStringFormat,
loggingEvent.ExceptionObject);
}
else
{
newLoggingEvent = loggingEvent;
}
base.Format(writer, newLoggingEvent);
}
}
MessageObjectBuilder - formatting the message using Newtonsoft :
public class MessageObjectBuilder
{
public static bool TryBuildMessageObject(object messageObject, out object newMessage)
{
var message = messageObject;
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
object[] args = message.GetType().GetField("m_args", bindingFlags)?.GetValue(message) as object[];
if (args == null || args.Length == 0) // not a formatted message
{
newMessage = message;
return false;
}
var argsAsJson = SerializeArgs(args);
var provider = message.GetType().GetField("m_provider", bindingFlags)?.GetValue(message);
var format = message.GetType().GetField("m_format", bindingFlags)?.GetValue(message);
newMessage = new SystemStringFormat(provider as IFormatProvider, format as string, argsAsJson);
return true;
}
private static object[] SerializeArgs(object[] args)
{
var newArgs = new List<string>();
foreach (var arg in args)
{
newArgs.Add(Newtonsoft.Json.JsonConvert.SerializeObject(arg));
}
return newArgs.ToArray();
}
}
And finally add this to log4net.config instead of your current layout:
<appender name="StructLogAppender" type="log4net.Appender.RollingFileAppender">
<file value="StructLogAppender\StrcutLog.log" />
<layout type="Namespace.StructLayout, AssmbllyName">
<conversionPattern value="%date{M/d/yyyy H:mm:ss.fff} - %message%newline" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMax" value="ERROR" />
</filter>
<appendToFile value="true" />
</appender>
Note:
this code using reflection so it could be a bit heavy, so its not
recommended if performance is an issue.
make sure to custom Namespace and AssmbllyName in log4net.config

Related

log4net managedcoloredconsole not working with dynamic creation

I have a program that has a different set of modules called based an input parameter. Modules like Orders, Shipments, Pricing etc. I wrote a logging class with log4net being the foundation though there is a need for some custom logging as well. What I want is to have each module have its own logging file and to that point, I was able to get log4net to dynamically create the appenders for each file.
I was also able to get a console display for the times when it may be run manually, but what I lost (and cannot figure how to get it to work is the Colored Console appender. I found the basic solution here for creating appenders and I then used this link to figure out how to create console and ManagedColoredConsole appenders, but while it still writes to the console, I do not get color.
There is something missing, but I I don't know what. I wrote a small testing program to try and figure this out and this is the logging class:
using log4net;
using log4net.Appender;
using log4net.Layout;
using log4net.Repository.Hierarchy;
using System;
using System.Linq;
namespace TestLogging
{
public class Logging
{
// Since the current version of logging will require more custom fields passed into the logging table
// I'm going to set up a wrapper around the log for net processing. This should simplify the way we call it in
// the main program sections so we don't have to keep adding constants like pid and we can deal with variables
// like item, order number, shipping numbers
public static ILog log = null;
public string transType = "";
public string pid = "0";
private string logModule = "main";
private string path = "";
public Logging(string LogModule)
{
logModule = LogModule; // set up to default to main then pass in the specific log file name for log4net
SetLevel("Log4net.MainForm", "ALL");
path = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
string execPath = AppDomain.CurrentDomain.BaseDirectory;
if (log.Logger.Repository.GetAppenders().Count() == 0)
{
//CreateConsoleAppender();
CreateManagedColorConsoleAppender();
}
AddAppender(LogModule, CreateFileAppender(logModule, execPath + "\\logs\\" + logModule + ".log"));
}
public void Info(string message, string sohnum = null, string itmref = null, string sdhnum = null, double processtime = 0.0)
{
setCustom(sohnum, itmref, sdhnum, processtime);
log.Info(message);
}
private void setCustom(string sohnum = null, string itmref = null, string sdhnum = null, double processtime = 0.0)
{
log4net.ThreadContext.Properties["TransType"] = transType;
log4net.ThreadContext.Properties["sohnum_0"] = sohnum;
log4net.ThreadContext.Properties["itmref_0"] = itmref;
log4net.ThreadContext.Properties["sdhnum_0"] = sdhnum;
log4net.ThreadContext.Properties["processtime"] = processtime.ToString();
log4net.ThreadContext.Properties["pid"] = pid;
}
// Set the level for a named logger
public static void SetLevel(string loggerName, string levelName)
{
log = LogManager.GetLogger(loggerName);
Logger l = (Logger)log.Logger;
l.Level = l.Hierarchy.LevelMap[levelName];
}
// Add an appender to a logger
public static void AddAppender(string loggerName, IAppender appender)
{
log = LogManager.GetLogger(loggerName);
Logger l = (Logger)log.Logger;
l.Repository.Configured = true;
l.AddAppender(appender);
}
// Create a new file appender
public static IAppender CreateFileAppender(string name, string fileName)
{
FileAppender appender = new
FileAppender();
appender.Name = name;
appender.File = fileName;
appender.AppendToFile = true;
PatternLayout layout = new PatternLayout();
layout.ConversionPattern = "%d [%t] %-5p %c [%x] - %m%n";
layout.ActivateOptions();
appender.Layout = layout;
appender.ActivateOptions();
return appender;
}
public static IAppender CreateConsoleAppender()
{
ConsoleAppender appender = new ConsoleAppender();
appender.Name = "console";
PatternLayout layout = new PatternLayout();
layout.ConversionPattern = "%d [%t] %-5p %c [%x] - %m%n";
layout.ActivateOptions();
appender.Layout = layout;
appender.ActivateOptions();
var hierarchy = (Hierarchy)LogManager.GetRepository();
hierarchy.Configured = true;
hierarchy.Root.AddAppender(appender);
return appender;
}
public static IAppender CreateManagedColorConsoleAppender()
{
ManagedColoredConsoleAppender appender = new ManagedColoredConsoleAppender();
ManagedColoredConsoleAppender.LevelColors mapping = new ManagedColoredConsoleAppender.LevelColors();
appender.Name = "ManagedColoredConsoleAppender";
mapping.Level = log4net.Core.Level.Debug;
mapping.ForeColor = ConsoleColor.Blue;
appender.AddMapping(mapping);
mapping.Level = log4net.Core.Level.Info;
mapping.ForeColor = ConsoleColor.Green;
appender.AddMapping(mapping);
mapping.Level = log4net.Core.Level.Error;
mapping.ForeColor = ConsoleColor.Yellow;
appender.AddMapping(mapping);
mapping.Level = log4net.Core.Level.Fatal;
mapping.ForeColor = ConsoleColor.Red;
appender.AddMapping(mapping);
PatternLayout layout = new PatternLayout();
layout.ConversionPattern = "%d [%t] %-5p %c [%x] - %m%n";
layout.ActivateOptions();
appender.Layout = layout;
appender.ActivateOptions();
var hierarchy = (Hierarchy)LogManager.GetRepository();
hierarchy.Root.AddAppender(appender);
hierarchy.Configured = true;
hierarchy.Root.Level = log4net.Core.Level.Info;
return appender;
}
}
}
It is rough, but this is just for testing and learning.
This is the main program:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestLogging
{
public class Program
{
//private Logging logging = new Logging("file");
private static Logging logit = new Logging("main");
static void Main(string[] args)
{
logit.Info("This is the main program");
ordersClass orders = new ordersClass();
orders.callMe();
shipments shipit = new shipments();
shipit.shipMe();
}
}
}
and one of the classes that writes to a different log file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestLogging
{
public class ordersClass
{
private Logging logit = new Logging("orders");
public void callMe()
{
logit.Info("Just placed an order");
}
}
}
When I set a break point to look at the log object I can see managed color is there as a root appender and the others added when first created. there is not a lot of info on using log4net programmatically, but I am hoping someone got this to work.
As I was reading more I discovered how to turn on internal logging for log4net. Put that in the app.config file and yes, it helps for it showed me how to fix my issue though why it does not work dynamically still alludes me.
<appSettings>
<add key="log4net.Internal.Debug" value="true"/>
</appSettings>
What I found was a few things:
Log4net does not need a config file to work if you are setting things up programmatically. Found that out because I had not set the 'copy to output directory' to other then 'do not copy' so no config file was being put in the the exe folder. This showed me you don't need a config file to do logging, but it still did not answer why no color.
If you decide to use a config file, but don't put in a appender that is referenced in the root, log4net logs the error, but keeps working. I had this
<appender-ref ref="ManagedColoredConsoleAppender" />
but no appender in the file. I added the ManagedColorConsole Appender and now I am getting both colored console messages AND logging into multiple files. This is a solution, but does not explain why I could add the color appender dynamically, but not have it work. If there is an answer please post. In the mean time this is a solved question.
<log4net>
<root>
<level value="ALL" />
<appender-ref ref="ManagedColoredConsoleAppender" />
</root>
<appender name="ManagedColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
<mapping>
<level value="INFO" />
<foreColor value="Green, HighIntensity" />
</mapping>
<mapping>
<level value="DEBUG" />
<foreColor value="Green" />
</mapping>
<mapping>
<level value="ERROR" />
<foreColor value="Yellow, HighIntensity" />
</mapping>
<mapping>
<level value="FATAL" />
<foreColor value="Red, HighIntensity" />
</mapping>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
</log4net>

log4net config file for net.core with UDP Append remote port dynamically

I have a log4net.config file in which I want to be able to inject the RemotePort variable dynamically via Program.cs file. But I am getting the following error :
System.FormatException: Input string was not in a correct format.
I was able to pass RemoteAddress dynamically with this strategy :
log4net config file for net.core with UDP Append remote address dynamically
The code in the log4net file
<appender name="UdpAppender" type="log4net.Appender.UdpAppender">
<RemoteAddress value="%property{RemoteAddress}" />
<RemotePort value="%property{RemotePort}" />
<encoding value="utf-8"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level - %property{log4net:HostName} - %message%newline" />
</layout>
</appender>
The code in Program.cs
log4net.GlobalContext.Properties["RemotePort"] = 514;
The same concept as shown in the referenced article is also applicable here.
You need a translation from a PatternString to an Int32 which requires a custom type converter implementing IConvertFrom.
Reusing the implementation of a PatternString the %property{RemotePort} will be replaced by the value passed in via log4net.GlobalContext.Properties["RemotePort"] = 514, which will result into 514.
public class NumericConverter : IConvertFrom
{
public NumericConverter()
{}
public Boolean CanConvertFrom(Type sourceType)
{
return typeof(String) == sourceType;
}
public Object ConvertFrom(Object source)
{
String pattern = (String)source;
PatternString patternString = new PatternString(pattern);
String value = patternString.Format();
return Int32.Parse(value);
}
}
Register this type converter at startup as shown below
log4net.Util.TypeConverters.ConverterRegistry.AddConverter(typeof(int), new NumericConverter());

Using default constructor and parameterised constructor in unity c#

I have an application that uses unity FW to resolve objects throughout.
I have done some change to the framework and the classes which can be seen in the code comment as "NEW CHANGE"
The wrapper class looks like
public static class ContractResolver
{
public static T Resolve<T>() //This is been used in many places in application
{
IUnityContainer container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
return container.Resolve<T>();
}
//NEW CHANGE: This is the new function that suppose to return the instance of parameterised constructor
public static T Resolve<T>(ParameterOverride[] parameterOverrides)
{
IUnityContainer container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
return container.Resolve<T>(parameterOverrides);
}
}
The configuration looks like
<unity>
<containers>
<container>
<types>
<type type ="UnityTest.IImageRepositoryService, UnityTest" mapTo="UnityTest.ImageRepositoryService, UnityTest"/>
</types>
</container>
</containers>
</unity>
The classes and interface looks like
public interface IImageRepositoryService
{
bool Exists(string imageName);
}
public class ImageRepositoryService : IImageRepositoryService
{
private readonly string mFilterName = "StandardImageFilter";
//[InjectionConstructor]
public ImageRepositoryService()
{
DatabaseQueryProvider.Query("Image", mFilterName);
}
//NEW CHANGE. A CONSTRUCTOR THAT ACCEPTS A PARAMETER
//[InjectionConstructor]
public ImageRepositoryService(string filterName)
{
mFilterName = filterName;
DatabaseQueryProvider.Query("Image", filterName);
}
public bool Exists(string imageName)
{
Console.WriteLine("The image " + imageName + " found in filter " + mFilterName);
return true;
}
}
The usage looks like
var serviceDefault = ContractResolver.Resolve<IImageRepositoryService>();
serviceDefault.Exists("myimage.bmp");
The new changes breaks the old usage. i.e.
var serviceDefault = ContractResolver.Resolve<IImageRepositoryService>();
Throws exception
Resolution of the dependency failed, type = "UnityTest.IImageRepositoryService", name = "(none)".
Exception occurred while: while resolving.
Exception is: InvalidOperationException - The type String cannot be constructed. You must configure the container to supply this value.
I would like to have the new functionality at the same time do not want to break the old functionality.
var serviceDefault = ContractResolver.Resolve<IImageRepositoryService>();
serviceDefault.Exists("myimage.bmp");
Should display the message in the console "The image myimage.bmp found in filter StandardImageFilter"
var parameterOverride1 = new ParameterOverride("filterName", "filter1");
var servicefilter1 = ContractResolver.Resolve<IImageRepositoryService>(new[] { parameterOverride1 });
servicefilter1.Exists("myimage.bmp");
Should display the message in the console "The image myimage.bmp found in filter filter1"
var parameterOverride2 = new ParameterOverride("filterName", "filter2");
var servicefilter2 = ContractResolver.Resolve<IImageRepositoryService>(new[] { parameterOverride2 });
servicefilter2.Exists("myimage.bmp");
Should display the message in the console "The image myimage.bmp found in filter filter2"
How solve this problem?
If you want to resolve the same type (in this case IImageRepositoryService) but have different calls to Resolve invoke different constructors then you will need to use named registrations.
In your case you can do this in the XML configuration:
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<container>
<register type ="UnityTest.IImageRepositoryService, UnityTest" mapTo="UnityTest.ImageRepositoryService, UnityTest">
<constructor />
</register>
<register name="ParameterizedRepository"
type="UnityTest.IImageRepositoryService, UnityTest"
mapTo="UnityTest.ImageRepositoryService, UnityTest">
<constructor>
<param name="filterName" value="dummyValue" />
</constructor>
</register>
</container>
</unity>
Note, that I've used the Unity 2 (and 3) configuration style.
So this tells Unity that when resolving using the name "ParameterizedRepository" to invoke the constructor with the parameter named "filterName". I've used a dummy value here because we are going to override the value at runtime anyway:
var imageRepositoryService = container.Resolve<IImageRepositoryService>(
"ParameterizedRepository",
new ParameterOverride("filterName", "filter2"));
So that's how to get what you want using Unity so in terms of your wrapper class you should add a name parameter:
public static class ContractResolver
{
//NEW CHANGE: This is the new function that suppose to return the instance of parameterised constructor
public static T Resolve<T>(string name, params ParameterOverride[] parameterOverrides)
{
IUnityContainer container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
return container.Resolve<T>(name, parameterOverrides);
}
}
A few unsolicited comments (in the spirit of trying to be helpful):
It looks like you are using Unity version 1. If so, you might want to consider upgrading (version 3 was released recently) and if you are not using Unity version 1, then you might want to consider changing the XML configuration syntax to use the newer approach as well as using the LoadConfiguration() extension method.
I'm not sure why every call to ContractResolver.Resolve() creates a new Unity container and then loads the configuration. This could be a bit of a performance hit. Usually, you would create a container and load the configuration once and use that instance for the lifetime of the application.
I can understand how you would be tempted to hide the container implementation behind the ContractResolver but with the addition of ParameterOverride (which are Unity specific) the abstraction is becoming a bit leaky.

Can you configure log4net in code instead of using a config file?

I understand why log4net uses app.config files for setting up logging - so you can easily change how information is logged without needing to recompile your code. But in my case I do not want to pack a app.config file with my executable. And I have no desire to modify my logging setup.
Is there a way for me to set up logging in code rather than using the app.config?
Here is my simple config file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs\EventLog.txt" />
<appendToFile value="false" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="5" />
<maximumFileSize value="1GB" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="MemoryAppender" type="log4net.Appender.MemoryAppender">
</appender>
<root>
<level value="Info" />
<appender-ref ref="RollingLogFileAppender" />
<appender-ref ref="MemoryAppender" />
</root>
</log4net>
</configuration>
EDIT:
To be completely clear: It is my goal to have no XML file. Not even as an embedded resource that I turn into a stream. My goal was to define the logger completely programmatically. Just curious if it's possible and if so where I might find an example of the syntax.
FINAL SOLUTION:1
For anyone who may stumble upon this in the future, here is what I did. I made the static class below:
using log4net;
using log4net.Repository.Hierarchy;
using log4net.Core;
using log4net.Appender;
using log4net.Layout;
namespace Spectrum.Logging
{
public class Logger
{
public static void Setup()
{
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
PatternLayout patternLayout = new PatternLayout();
patternLayout.ConversionPattern = "%date [%thread] %-5level %logger - %message%newline";
patternLayout.ActivateOptions();
RollingFileAppender roller = new RollingFileAppender();
roller.AppendToFile = false;
roller.File = #"Logs\EventLog.txt";
roller.Layout = patternLayout;
roller.MaxSizeRollBackups = 5;
roller.MaximumFileSize = "1GB";
roller.RollingStyle = RollingFileAppender.RollingMode.Size;
roller.StaticLogFileName = true;
roller.ActivateOptions();
hierarchy.Root.AddAppender(roller);
MemoryAppender memory = new MemoryAppender();
memory.ActivateOptions();
hierarchy.Root.AddAppender(memory);
hierarchy.Root.Level = Level.Info;
hierarchy.Configured = true;
}
}
}
And then all I had to do was replace the code where I called the XML file with the following call:
//XmlConfigurator.Configure(new FileInfo("app.config")); // Not needed anymore
Logger.Setup();
1(this answer was edited into the question by the OP, I took the liberty to make it a community answer, see here why)
You can also escape XML completely, I wrote a sample with minimal programmatic configuration here.
In a nutshell, here is what you need
var tracer = new TraceAppender();
var hierarchy = (Hierarchy)LogManager.GetRepository();
hierarchy.Root.AddAppender(tracer);
var patternLayout = new PatternLayout {ConversionPattern = "%m%n"};
patternLayout.ActivateOptions();
tracer.Layout = patternLayout;
hierarchy.Configured = true;
Yes, you can configure log4net by calling:
log4net.Config.XmlConfigurator.Configure(XmlElement element)
See the log4net documentation.
Alternatively you could create a custom attribute that inherits from log4net.Config.ConfiguratorAttribute and hard-code you configuration there:
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Layout;
using log4net.Repository;
using log4net.Repository.Hierarchy;
using System;
using System.Reflection;
namespace ConsoleApplication1
{
[AttributeUsage(AttributeTargets.Assembly)]
public class MyConfiguratorAttribute : ConfiguratorAttribute
{
public MyConfiguratorAttribute()
: base(0)
{
}
public override void Configure(Assembly sourceAssembly, ILoggerRepository targetRepository)
{
var hierarchy = (Hierarchy)targetRepository;
var patternLayout = new PatternLayout();
patternLayout.ConversionPattern = "%date [%thread] %-5level %logger - %message%newline";
patternLayout.ActivateOptions();
var roller = new RollingFileAppender();
roller.AppendToFile = false;
roller.File = #"Logs\EventLog.txt";
roller.Layout = patternLayout;
roller.MaxSizeRollBackups = 5;
roller.MaximumFileSize = "1GB";
roller.RollingStyle = RollingFileAppender.RollingMode.Size;
roller.StaticLogFileName = true;
roller.ActivateOptions();
hierarchy.Root.AddAppender(roller);
hierarchy.Root.Level = Level.Info;
hierarchy.Configured = true;
}
}
}
Then add the following to a .cs file:
[assembly: ConsoleApplication1.MyConfigurator]
For those who don't want to add appender to Root logger, but to current/other logger:
//somewhere you've made a logger
var logger = LogManager.GetLogger("MyLogger");
// now add appender to it
var appender = BuildMyAppender();
((log4net.Repository.Hierarchy.Logger)logger).AddAppender(appender);
logger.Debug("MyLogger with MyAppender must work now");
// and remove it later if this code executed multiple times (loggers are cached, so you'll get logger with your appender attached next time "MyLogger")
((log4net.Repository.Hierarchy.Logger)logger).RemoveAppender(sbAppender);
Although the accepted answer works in most cases, It has a few drawbacks.
It only keeps 5 last logs.
Log size is set to 1GB which is too large for most notepad applications to open.
Since it locks the log file, it is not suitable for multi-threaded apps such as web applications.
Since date is prefixed to the file name it is not convenient to use in windows
It overwrites the log each time application re-starts which is again not suitable if you are planning to keep the logs.
As mentioned in comments, it needs some modifications to make it work correctly in some cases.
Thus the little more extensive configuration. I created a class which makes logging a little easier. Obviously you can just pick the configuration part if you wish.
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Layout;
using log4net.Repository.Hierarchy;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Text;
using System.Web;
public enum LogType { Info, Debug, Warn, Error, Fatal };
public class Logger {
private const string LOG_NAMESPACE = "APP_NAME";
private const string LOG_FILENAME_PREFIX = #"D:\Logs\";
private static readonly Level LOG_LEVEL = Level.Debug;
private static readonly ILog log = LogManager.GetLogger(LOG_NAMESPACE);
#region static Logger()
// Static constructor is called automatically to initialize the class before the first instance is created or any static members are referenced
static Logger() {
Hierarchy hierarchy = (Hierarchy) LogManager.GetRepository();
hierarchy.Root.RemoveAllAppenders(); // Clear all previously added repositories.
hierarchy.Root.Level = LOG_LEVEL; // Set Log level
PatternLayout layout = new PatternLayout() { ConversionPattern = "%d{yyyy-MM-dd HH:mm:ss.fff} %4t %-5p %m%n" }; // Log line format: Include millisecond precision, thread ID, Log type,
layout.ActivateOptions(); // Apply Configuration
RollingFileAppender RFA = new RollingFileAppender();
RFA.Name = LOG_NAMESPACE; // Set name of appender
RFA.File = LOG_FILENAME_PREFIX + LOG_NAMESPACE; // Set file name prefix
RFA.LockingModel = new FileAppender.MinimalLock(); // Minimum lock time required, makes file available for reading
RFA.AppendToFile = true; // Do not overwrite existing logs, append to them.
RFA.DatePattern = ".yyyy.MM.dd'.log'"; // Add file extension here, to preserve the file extension
RFA.Encoding = Encoding.UTF8; // Set format of file to UTF8 for international characters.
RFA.CountDirection = 1; // Increment file name in bigger number is newest, instead of default backward.
RFA.MaximumFileSize = "100MB"; // Maximum size of file that I could open with common notepad applications
RFA.RollingStyle = RollingFileAppender.RollingMode.Composite; // Increment file names by both size and date.
RFA.StaticLogFileName = false;
RFA.MaxSizeRollBackups = -1; // Keep all log files, do not automatically delete any
RFA.PreserveLogFileNameExtension = true; // This plus extension added to DatePattern, causes to rolling size also work correctly
RFA.Layout = layout;
RFA.ActivateOptions(); // Apply Configuration
hierarchy.Root.AddAppender(RFA);
BasicConfigurator.Configure(hierarchy); // Apply Configuration
}
#endregion
#region public static int Log(...)
public static void Log(string Description, LogType logtype = LogType.Info) {
switch (logtype) {
case LogType.Debug:
log.Debug(Description);
break;
case LogType.Info:
log.Info(Description);
break;
case LogType.Warn:
log.Warn(Description);
break;
case LogType.Error:
log.Error(Description);
break;
case LogType.Fatal:
log.Fatal(Description);
break;
}
}
#endregion
#region public static int Log(...)
public static void Log(string Message, Exception ex) {
log.Fatal(Message, ex);
}
#endregion
}
And then to log messages and exceptions call it like following
Logger.Log("I was here", LogType.Debug);
Logger.Log("I am info message");
Logger.Log("An error", LogType.Error);
Logger.Log("An Exception", ex); // ex is of type Exception
Due to the weird way log4net adds rolling data to file name (added after file extension) files loose windows explorer association. To fix that .log was added to DatePattern instead. It will also correctly adds file increment before extension (maybe because of a bug) Tested on version 1.2.11.0
Notes:
No external call for initialization is required, this will initialize on application start (or when you call Logger.Log for the first time
You can move all class constants out to your own config file and make this class more generic and reusable.
Note that no namespace is specified, this will make Logger class available in all namespaces.
Log4net provides 5 methods to log messages which is a little inconvenient to remember. Thus Logger.Log method by default uses info.
An Unrelated note: If your application is running on a server or web, keep your log files off the OS drive and application folder.
The accepted answer works after I found two caveats:
It was not working for me at first, but after using a full absolue path for the roller.File property, it started work.
I had to use this in F# (in a fsx script), so had some issues when converting it from C#. If you're interested in the end result (including a way to download log4net nuget package), see below:
nuget_log4net.fsx:
#!/usr/bin/env fsharpi
open System
open System.IO
open System.Net
#r "System.IO.Compression.FileSystem"
open System.IO.Compression
type DummyTypeForLog4Net () =
do ()
module NetTools =
let DownloadNuget (packageId: string, packageVersion: string) =
use webClient = new WebClient()
let fileName = sprintf "%s.%s.nupkg" packageId packageVersion
let pathToUncompressTo = Path.Combine("packages", packageId)
if (Directory.Exists(pathToUncompressTo)) then
Directory.Delete(pathToUncompressTo, true)
Directory.CreateDirectory(pathToUncompressTo) |> ignore
let fileToDownload = Path.Combine(pathToUncompressTo, fileName)
let nugetDownloadUri = Uri (sprintf "https://www.nuget.org/api/v2/package/%s/%s" packageId packageVersion)
webClient.DownloadFile (nugetDownloadUri, fileToDownload)
ZipFile.ExtractToDirectory(fileToDownload, pathToUncompressTo)
let packageId = "log4net"
let packageVersion = "2.0.5"
NetTools.DownloadNuget(packageId, packageVersion)
let currentDirectory = Directory.GetCurrentDirectory()
// https://stackoverflow.com/a/19538654/6503091
#r "packages/log4net/lib/net45-full/log4net"
open log4net
open log4net.Repository.Hierarchy
open log4net.Core
open log4net.Appender
open log4net.Layout
open log4net.Config
let patternLayout = PatternLayout()
patternLayout.ConversionPattern <- "%date [%thread] %-5level %logger - %message%newline";
patternLayout.ActivateOptions()
let roller = RollingFileAppender()
roller.AppendToFile <- true
roller.File <- Path.Combine(currentDirectory, "someLog.txt")
roller.Layout <- patternLayout
roller.MaxSizeRollBackups <- 5
roller.MaximumFileSize <- "1GB"
roller.RollingStyle <- RollingFileAppender.RollingMode.Size
roller.StaticLogFileName <- true
roller.ActivateOptions ()
let hierarchy = box (LogManager.GetRepository()) :?> Hierarchy
hierarchy.Root.AddAppender (roller)
hierarchy.Root.Level <- Level.Info
hierarchy.Configured <- true
BasicConfigurator.Configure(hierarchy)
let aType = typedefof<DummyTypeForLog4Net>
let logger = LogManager.GetLogger(aType)
logger.Error(new Exception("exception test"))

Custom Trace Listener with the Enterprise Application Blocks

The project I'm currently working on uses Enterprise Libraries V3.1 framework for logging.
I need to take the log file that's generated and archive it off at specific points. The built in Trace Listeners seem to keep the file open in-between logging events. I've set up a custom Trace Listener which will append to a file and close it, so that the file is always shiftable.
It looks like this (minus error handling for clarity):
[ConfigurationElementType(typeof(CustomTraceListenerData))]
public class AlwaysClosedTextFileTraceListener : CustomTraceListener
{
private string logFilePath;
public AlwaysClosedTextFileTraceListener ()
{
logFilePath = #"hardcodedpath\log.txt";
}
public override void Write(string message)
{
using (StreamWriter logFile = File.AppendText(logFilePath))
{
logFile.Write(message);
logFile.Flush();
logFile.Close();
}
}
public override void WriteLine(string message)
{
using (StreamWriter logFile = File.AppendText(logFilePath))
{
logFile.WriteLine(message);
logFile.Flush();
}
}
public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
{
if (data is LogEntry && this.Formatter != null)
{
WriteLine(this.Formatter.Format(data as LogEntry));
}
else
{
WriteLine(data.ToString());
}
}
}
This works fine, but I'd much rather be passing in the path as a parameter somehow, rather than hardcoding it.
For fun, I tried adding it to the constructor, to see what happens:
public LogFolderTraceListener(string logFilePath)
{
this.logFilePath = logFilePath;
}
When I do this, I get returned an error message hinting towards what I'm doing wrong:
System.InvalidOperationException : The type 'AlwaysClosedTextFileTraceListener' specified for custom trace listener named 'MyLogFile' does not a default constructor, which is required when no InitData is specified in the configuration.
From here on in, my investigations have very much come to, the opposite of dead ends, infinite probability problems.
I have found this thumbing through the source code for the inbuilt RollingTraceListener
There is a class RollingFlatFileTraceListenerData : TraceListenerData which seems to contain all the settings passed into the constructor
Camped out at the bottom of the file for RollingFlatFileTraceListenerData is the class RollingTraceListenerAssembler : TraceListenerAsssembler which seems to be a factory
There is another class SystemDiagnosticsTraceListenerNode : TraceListenerNode which seems to make the Data class presentable to the configuration application
My question is this: how do I create a CustomTraceListener with a configurable parameter of path?
The CustomTraceListener derives from TraceListener, this has a StringDictionary called Attributes.
This will contain all the attributes in the configuration line for your TraceListener and can be gotten out by name, eg.
string logFileName= Attributes["fileName"]
I suspect that perhaps the Enterprise Application Blocks although (probably) wonderful, seem unnecessarily complicated and ultimately more trouble than their worth for this kind of customisation.
the problem is typical microsoft .. (add your own adjectives here) ..
1) when you add a custom trace listener, the 'raw' app.config statement added is:
name="Custom Trace Listener" initializeData="" formatter="Text Formatter" />
2) notice the 'initializeData' - this is what the cryptic error message is calling'InitData'.
3) So what its all saying is that you need to have a constructor that accepts initialization data - in vb parlance:
sub new (byval initstuff as string)
4) OR remove the 'initializeData=""' and have a default constructor:
sub new()
I suspect the P&P folks live in a bubble.
riix.
For what it is worth this is how I implemented it. In my this.buildCurrPath() I can read from a config file or in this case I just get the "launch pad" for the web app. But it works fine for me. I have not put it into any production code yet, but it should go out soon.
[ConfigurationElementType(typeof(CustomTraceListenerData))]
public class CustomListener: CustomTraceListener
{
#region Fields (3) 
private int logSize;
StreamWriter sw;
#endregion Fields 
#region Constructors (1) 
public CustomListener ():base()
{
string startPath = this.buildCurrPath();
sw = new StreamWriter(startPath + "\\Logs\\test.log");
sw.AutoFlush = true;
}
I have just had the same issue (except with Enterprise Library v4.1).
The solution I've found is to remove the default constructor and the only have a constructor with a string parameter for the filename i.e.
public AlwaysClosedTextFileTraceListener (string pathParameter)
{
logFilePath = pathParameter;
}
Then in the app.config put your path in the initializeData parameter
<add ... initializeData="C:\Logs\myLog.log" />
Whilst this isn't recognised by the Entriprise Library configuration editor and isn't as neat as it could be, it works as long as there is only one parameter.
If someone works out how to do it properly, please post and let us know - it's not supposed to be this difficult, surely.

Categories

Resources