Why is logging to a log4net.ILog appending to multiple logs? - c#

I'm developing a plugin for a third-party application, and for each 'run' of this plugin I want an exclusive log file.
I've built the following class.
public class LogFileRepository
{
private readonly Common.Configuration.Settings _configSettings;
private const string InstanceName = "AutomationPlugin.Logging";
private readonly ILoggerRepository _repository;
public LogFileRepository (Common.Configuration.Settings configSettings)
{
_configSettings = configSettings;
var repositoryName = $"{InstanceName}.Repository";
_repository = LoggerManager.CreateRepository(repositoryName);
}
public ILog GetLog(string name)
{
var logger = LogManager.Exists(_repository.Name, name);
if (logger != null)
{
return logger;
}
var filter = new LevelMatchFilter {LevelToMatch = Level.All};
filter.ActivateOptions();
var appender = new RollingFileAppender
{
AppendToFile = false,
DatePattern = "yyyy-MM-dd",
File = String.Format(_configSettings.Paths.LogFileTemplate, name),
ImmediateFlush = true,
Layout = new PatternLayout("%n%date{ABSOLUTE} | %-7p | %m"),
LockingModel = new FileAppender.MinimalLock(),
MaxSizeRollBackups = 1,
Name = $"{InstanceName}.{name}.Appender",
PreserveLogFileNameExtension = false,
RollingStyle = RollingFileAppender.RollingMode.Once
};
appender.AddFilter(filter);
appender.ActivateOptions();
BasicConfigurator.Configure(_repository, appender);
return LogManager.GetLogger(_repository.Name, name);
}
}
What I intended this function to do is for the GetLog method to return a log file (with the specified name) if the LogManager already has one; if there isn't an existing log file then it should instantiate and return it.
This does happen. On the first run of the plugin a log file is created and written to; on a second run of the plugin a new log file is created and written to, but all messages are also written to the first log file. And on a third run all messages are written to the two existing log files as well as the new third log file.
Why? Is there something in the RollingFileAppender that I've seemingly misunderstood/misconfigured? I want an exclusive log file for each name parameter.

Assuming you've created _repository using LogManager.CreateRepository(), this actually creates a Hierarchy, and when you configure this with your new appender via BasicConfigurator.Configure(_repository, appender); this adds the appender to the Hierarchy's Root appender collection.
All loggers then created from the repository are child loggers of the "Root" and are configured to be "additive" in that they append to all appenders defined directly against them, and any of their parent loggers, all the way up to the Root. In your case the loggers themselves have no appenders of their own, so are just picking up appenders from the Root, which in your case contains all the appenders. As a result all messages get logged to every file.
What you want to do is to attach the appender to its specific logger, and disable additivity so that it doesn't then log to appenders higher in the hierarchy. There doesn't appear to be a "nice" way to do this, but the following worked in my testing:
...
appender.AddFilter(filter);
appender.ActivateOptions();
// Add the appender directly to the logger and prevent it picking up parent appenders
if (LoggerManager.GetLogger(_repository.Name, name) is Logger loggerImpl)
{
loggerImpl.Additivity = false;
loggerImpl.AddAppender(appender);
}
BasicConfigurator.Configure(_repository, appender);
return LogManager.GetLogger(_repository.Name, name);

Related

Two processors can't log the information at the same time by using NLog

I have two projects A and B, both of them use a NLog libiary. Now I have an issue:
if A writes loginfo into the log file first, then the B never logs. And if B writes loginfo into the log file first, then A never logs.
As A and B use a same NLog libiary, so they use the same Nlog Config, but they will be built in two processors, here is the config info.
Does somebody have any good idea on this issue?
//Set NLog Config by:
//https://github.com/nlog/NLog/wiki/Configuration-API
private static Logger GenerateLogInstance()
{
// Step 1. Create configuration object
var config = new LoggingConfiguration();
// Step 2. Create targets
var fileTarget = new FileTarget()
{
FileName = #"C:\Logs\${shortdate}.log",
Layout = #"${longdate} ${uppercase:${level}} ${message}${onexception:${newline}EXCEPTION\: ${exception:format=ToString}}"
};
//var wrapper = new AsyncTargetWrapper(fileTarget, 5000, AsyncTargetWrapperOverflowAction.Discard);
// Step 3. Define rules
config.AddTarget("myprojectLog", fileTarget);
config.LoggingRules.Add(new NLog.Config.LoggingRule("*", NLog.LogLevel.Trace, fileTarget));
// Step 4. Activate the configuration
var factory = new LogFactory(config);
return factory.GetLogger("myprojectLog");
}
I don't use nlog but take a look at the following. You may need to set concurrentWrites="true"
File target
concurrentWrites - Enables support for optimized concurrent writes to
same log file from multiple processes on the same machine-host, when
using keepFileOpen = true. By using a special technique that lets it
keep the files open from multiple processes. If only single process
(and single AppDomain) application is logging, then it is faster to
set to concurrentWrites = False. Boolean Default: True. Note: in UWP
this setting should be false
Could you try this instead:
private static LogFactory GenerateLogFactory()
{
// Step 0. Create isolated LogFactory
var logFactory = new LogFactory();
// Step 1. Create configuration object
var config = new LoggingConfiguration(logFactory);
// Step 2. Create targets
var fileTarget = new FileTarget()
{
FileName = #"C:\Logs\${shortdate}.log",
Layout = #"${longdate} ${uppercase:${level}} ${message}${onexception:${newline}EXCEPTION\: ${exception:format=ToString}}"
};
// Step 3. Define rules
config.AddTarget("myprojectLog", fileTarget);
config.LoggingRules.Add(new NLog.Config.LoggingRule("*", NLog.LogLevel.Trace, fileTarget));
// Step 4. Activate the configuration
logFactory.Configuration = config;
return logFactory;
}
private static Logger GenerateLogInstance()
{
return GenerateLogFactory().GetLogger("myprojectLog");
}
Btw. if two projects in the same solution is using this same method, then you can consider doing this:
Lazy<LogFactory> LazyLogFactory = new Lazy<LogFactory>(() => GenerateLogFactory());
private static Logger GenerateLogInstance(string loggerName = "myprojectLog")
{
return LazyLogFactory.Value.GetLogger(loggerName);
}

Specflow BeforeTestRun Logging

[BeforeFeature]
public static void BeforeFeature()
{
featureTitle = $"{FeatureContext.Current.FeatureInfo.Title}";
featureRollFileAppender = new RollingFileAppender
{
AppendToFile = true,
StaticLogFileName = true,
Threshold = Level.All,
Name = "FeatureAppender",
File = "test.log",
Layout = new PatternLayout("%date %m%newline%exception"),
};
featureRollFileAppender.ActivateOptions();
log.Info("test");
}
I am attempting to use log4net to output a simple string, however, once the file has been generated, it does not contain any data.
No errors are thrown and the test does complete successfully.
It turns out that the previously selected RollingFileAppender was still open and I needed to select another RollingFileAppender. This is one of the issues when using multiple log files. Once this was resolved, the Info() method would output to my desired log file.
I was able to resolve my issue by adding the following code:
BasicConfigurator.Configure(nameRunRollFileAppender);
log = LogManager.GetLogger(typeof(Tracer));
log.Info("Output some data");

It is possible to set %property{PropertyName} dynamically?

I'm using log4net with C# to logging my app.
I know I can do it like this:
GlobalContext.Properties["PropertyName"] = "NewValue";
XmlConfigurator.Configure();
And it works.
But it's not thaaaat dynamic, as I have to call Configure again to set a new value.
Is there a way to set a property value before call ILog.Info?
Something like that:
//here I set a new value for %property{PropertyName}
log.Info("Value to log");
//here I set a another one for %property{PropertyName}
log.Info("Value to log 2");
You can use %property{PropertyName} in the conversionPattern of your PatternLayout, and you will get a new value logged each time you change the property value.
If you use a property for appender configuration properties, such as a filename or directory for a FileAppender, you will of course need to reconfigure after changing the property value.
Log4Net supports various contexts. GlobalContext, as you've found, is one of them. ThreadContext is another, and I think is more appropriate in your scenario:
log4net.ThreadContext["PropertyName"] = "NewValue";
There's no need to call Configure. Properties set in the ThreadContext are available for any calls to the logger from the current thread. Reference the property within the appender config in the normal way:
%property{PropertyName}
Based on #joe comment I've wrote my own appender as follow:
public class MyCustomAppender : RollingFileAppender
{
bool firstRun = true;
string fileNamePattern = null;
protected override void Append(LoggingEvent loggingEvent)
{
CloseFile();
File = fileNamePattern.Replace("__filename__", ThreadContext.Properties["PropertyName"].ToString());
LockingModel.OpenFile(File, true, Encoding.UTF8);
LockingModel.AcquireLock();
OpenFile(File, true);
base.Append(loggingEvent);
DoAppend(loggingEvent);
}
public override string File
{
get
{
if (firstRun)
{
firstRun = false;
fileNamePattern = base.File;
}
return base.File;
}
set
{
base.File = value;
}
}
}
And it works, which doesn't mean is correct. I don't know if it's a good thing inside the overrode Append method close, acquire lock and open file everytime I log something.
Any thoughts?

NLog filter by LogLevel as MappedDiagnosticsContext

Based on the answer here: How Thread-Safe is NLog? I have created a Logger and added two MappedDiagnosticsContext to NLog:
NLog.MappedDiagnosticsContext.Set("Module", string.Format("{0}.{1}", module.ComputerName, module.ModuleType));
NLog.MappedDiagnosticsContext.Set("ModuleCoreLogLevel", string.Format("LogLevel.{0}", module.CoreLogLevel));
I can successfully use the "Module" Context in the NLog config (programmatically) to generate the file name the logger should log to:
${{when:when=length('${{mdc:Module}}') == 0:inner=UNSPECIFIED}}${{when:when=length('${{mdc:Module}}') > 0:inner=${{mdc:Module}}}}.txt
This logs e.g. all messages with a "Module" context of "Test" to a filename called Module.txt
I now want to be able to set the LogLevel for the different Modules using this way, and only Log messages which correspond to this LogLevel (or higher)
I am trying to do this through filters, this means, on the LoggingRule I am trying to add a filter:
rule92.Filters.Add(new ConditionBasedFilter { Condition = "(level < '${mdc:ModuleCoreLogLevel}')", Action = FilterResult.IgnoreFinal });
This however does not seem to filter messages.
If I have for example a message which is emitted using Logger.Trace(), and the LogLevel on "ModuleCoreLogLevel" is set to LogLevel.Debug, I can still the Message in the resulting LogFile.
I solved this by doing this for each level:
rule92.Filters.Add(new ConditionBasedFilter { Condition = "(equals('${mdc:ModuleCoreLogLevel}', 'LogLevel.Trace') and level < LogLevel.Trace)", Action = FilterResult.IgnoreFinal });

log4net: Custom PatternLayoutConverter not being called

Situation: I want to show the method and line number for the code that logs a message. The problem is that I have an intermediate class which calls into the log4net logger. Unfortunately, due to existing architectural issues, I can't do away with this intermediate class. The result is that I always see the method and line number as being in the intermediate class. Not what I want.
So I tried to create a custom PatternLayoutConverter, as per this question:
Does log4net support including the call stack in a log message
I am also programmatically configuring log4net since, again for architectural reasons, using an XML config file is not feasible (I also find it ridiculous that the preferred and only documented way of configuring log4net is through a stupid XML file, but that's a topic for another discussion). So I followed this thread.
How to configure log4net programmatically from scratch (no config)
Everything works fine except that my custom converter is never called. Here is the code for the custom converter:
public class TVPatternLayout : PatternLayout {
public TVPatternLayout() {
this.AddConverter("logsite", typeof(TVPatternLayoutConverter));
}
}
public class TVPatternLayoutConverter : PatternLayoutConverter {
protected override void Convert(TextWriter writer, LoggingEvent loggingEvent) {
StackTrace st = new StackTrace();
int idx = 1;
while(st.GetFrame(idx).GetMethod().DeclaringType.Assembly == typeof(LogManager).Assembly)
idx++;
writer.Write(String.Format("{0}.{1} (line {2})", st.GetFrame(idx).GetMethod().DeclaringType.Name, st.GetFrame(idx).GetMethod().Name,
st.GetFrame(idx).GetFileLineNumber()));
}
}
And here is the code where I configure the logger:
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
hierarchy.Configured = false;
RollingFileAppender appender = new RollingFileAppender();
appender.Name = loggerName;
appender.File = realPath;
appender.AppendToFile = true;
appender.MaximumFileSize = "8000";
appender.MaxSizeRollBackups = 2;
TVPatternLayout patternLayout = new TVPatternLayout();
patternLayout.ConversionPattern = logFormat; // includes %logsite, my custom option
appender.Layout = patternLayout;
appender.ActivateOptions();
hierarchy.Root.AddAppender(appender);
hierarchy.Root.Level = Level.All;
hierarchy.Configured = true;
Problem was that I forgot to call ActivateOptions() on the patternLayout. Naturally, I'd figure that out right after writing up a long question.

Categories

Resources