How to log with a tree like structure - c#

I want to log with log4net (and I am doing this right now) to a file, but with log4net, there are just simple log outputs with a message each line.
Now I want to log to the same log file, but in a tree like structure. For example I've got the following method calls and each method logs its method name (there should be an indentation):
firstMethod().secondMethod().thirdMethod();
Should print this log for example:
2016-07-26 15:44:56,042 > firstMethod
2016-07-26 15:44:56,043 > secondMethod
2016-07-26 15:44:56,044 > thirdMethod
2016-07-26 15:44:56,045 < thirdMethod
2016-07-26 15:44:56,046 < secondMethod
2016-07-26 15:44:56,047 < firstMethod
The < and > signs are printed within the method at the start and at the end of the method.

It is not very efficient but you can look at using the call stack to get the hierarchy of methods and then pad/indent each log entry based on the depth of the call in the call stack.
StackTrace stackTrace = new StackTrace();
StackFrame[] stackFrames = stackTrace.GetFrames();
Alternatives could include using PostSharp or (possibly) writing a custom appender for log4net. Another thought is to forget about indenting and use the full namespace and method name to track where your log entry is coming from. This way you will still get a tree like view.
var methodInfo = System.Reflection.MethodBase.GetCurrentMethod();
var fullName = methodInfo.DeclaringType.FullName + "." + methodInfo.Name;
All that said, it seems like a very a-typical requirement. Do you really need to do this?

How about adding another method for logging with parameter for enter/exit, and do the padding there :
int curLevel = 0;
public void MyLogInfo( bool enter, [System.Runtime.CompilerServices.CallerMemberName] string callingMethod = "", )
{
string prefix;
if (enter)
{
prefix = "> ";
curLevel++;
}
else
{
prefix = "< ";
curLevel--;
}
logger.info(prefix.PadLeft(curLevel+prefix.Length) + callingMethod);
}
You just call MyLogInfo(true) or MyLogInfo(false) - .NET will set the callingMethod name if you do not specify it yourself.
You would have to ensure that you do always call the method on entry/exit of each method you log - maybe check you don't decrement curLevel below zero.

You can use NDC to achieve similar behavior. But it is deprecated. Now, you can use Contexts and Contexts Stacks. Check out this documentation: https://logging.apache.org/log4net/release/manual/contexts.html
You can define multiple stacks (instead of indentation). One stack for a set of behaviors you want to debug. Let's say you want to log creating a Sales Order and its Items. In this example, this stack is defined in ThreadContext. However you can define it in a GlobalContext , LogicalThreadContext or LoggingEvent.
using(log4net.ThreadContext.Stacks["SalesOrderLogic"].Push("SalesOrder"))
{
log.Info("Created the sales order");
using(log4net.ThreadContext.Stacks["SalesOrderLogic"].Push("Items"))
{
log.Info("Created the sales order items");
}
}
And then you should add it to your configuration:
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger - %property{SalesOrderStatus} - [%level]- %message%newline" />
</layout>
</appender>
DISCLAIMER: I did not test this code.
Note 1: The old NDC will appear in one thread. So if you pushed a message in one thread, it won't appear in another. And it has only one stack unlike the new Context Stack method.
Note 2: Don't forget to checkout Contexts. It is very useful.
Note 3: Also you might find it useful if you research Ninject and log4net working together

Related

NLog: Remove logging rule by name if it was added without name by LoggingConfiguration.AddRule()?

I am new to NLog, but I think I have got a good understand of how it works in the last hours.
After digging deeper into NLog API, several questions about Logging Rules come up. One of them is:
How can I remove a rule by name (using LoggingConfiguration.RemoveRuleByName()) that I have added programmatically by LoggingConfiguration.AddRule() before?
LoggingConfiguration.AddRule() does not provide an argument to set the LoggingRule.RuleName.
LoggingConfiguration.AddRule() does not take a LoggingRule object.
I don't want to check every rule in the LoggingConfiguration.LoggingRules collection, because this would mean checking them by content, because I cannot set a name.
While writing the question, I found the solution, which I want to share here.
The LoggingConfiguration.LoggingRules collection is a IList<LoggingRule> and thus supports Add(), Clear(), etc.
Therefore it is possible to add LoggingRule objects directly into this list. The LoggingRule object can be removed again by IList<>.Remove(), and, if it has a name, by LoggingConfiguration.Remove().
Example for adding named rule:
var loggingRule1 = new NLog.Config.LoggingRule ();
loggingRule1.RuleName = nameof (loggingRule1); // RuleName can also be set in constructor.
loggingRule1.LoggerNamePattern = "*";
loggingRule1.SetLoggingLevels (NLog.LogLevel.Info, NLog.LogLevel.Error);
loggingRule1.Targets.Add (consoleTarget);
loggingConfiguration.LoggingRules.Add (loggingRule1);
var loggingRule2 = new NLog.Config.LoggingRule ("*", NLog.LogLevel.Trace, NLog.LogLevel.Trace, consoleTarget) { RuleName = "loggingRule2" };
loggingConfiguration.LoggingRules.Add (loggingRule2);
logFactory.ReconfigExistingLoggers ();
// or, if config was not yet set to logFactory: logFactory.Configuration = loggingConfiguration;
Example for removing named rule:
loggingConfiguration.RemoveRuleByName (nameof (loggingRule1));
logFactory.ReconfigExistingLoggers ();
IMHO the API is poorly documented. I will suggest some more comprehensive descriptions.

NLog dynamically change filename using NLog.config

How to dynamically change the FileName using a variable from C#? My idea is to create a log file like Log_<UserId_From_DB>_${date:format=yyyy-MM-dd}.log.
Any ideas?
Another option is to use the Global Diagnostic Context - $(GDC):
Set the value in C#
GlobalDiagnosticsContext.Set("UserId_From_DB","42");
In the config (nlog.config):
<target type="file" filename="Log_${gdc:item=UserId_From_DB}_${date:format=yyyy-MM-dd}.log" ..>
Please avoid modifying NLog Variables at runtime (See previous answer below). They should be seen as readonly, because they are not threadsafe. NLog Variables will also be affected if LoggingConfiguration is reloaded.
Previous answer with NLog Variables:
Set the value in C#
LogManager.Configuration.Variables["UserId_From_DB"] = "42";
In the config (nlog.config):
<target type="file" filename="Log_${var:UserId_From_DB}_${date:format=yyyy-MM-dd}.log" ..>
If the value is set again, the filename will automatically changed.
While the posted answer works, it suffers from concurrency issues. That variable is a global variable and you may end up with conflicts.
There is a better solution available. There is a way to pass event properties to NLog.
Link to the relevant NLog documentation.
Let's assume you want to log an error message:
Logger myLog = LogManager.GetLogger(name);
LogLevel level = LogLevel.Error;
string message = "This is an error message!";
You turn this information into a LogEventInfo object:
LogEventInfo logEvent = new LogEventInfo(level , myLog.Name, message);
You can then add properties to this event (the string indexes are free to choose):
logEvent.Properties["MySpecialValue"] = "SPECIAL";
And then you write to the log:
myLog.Log(logEvent);
The interesting thing here is that in your NLog configuration, you can use this custom property in any field that the Nlog documentation refers to as a "Layout" value.
You use ${event-properties:item=MySpecialValue} in the layout to access the property. For example:
<target xsi:type="File"
name="file"
fileName="${basedir}/logs/${event-properties:item=MySpecialValue}/my_${event-properties:item=MySpecialValue}_file.log"
layout="${event-properties:item=MySpecialValue} ${message}" />
Following the posted example, you will get a folder named SPECIAL, inside of which is a log file named my_SPECIAL_file.log in which you find the message SPECIAL This is an error message!. Just to prove the point that you can use this custom value in many different ways and shapes.
I commonly use this to make entity-specific logging (where the filename of the log equals the entity's ID value), which is essentially the same as you want to do here.
As a quick tip, I tend to wrap the NLog Logger in a class of my own:
public class UserLogger
{
private readonly Logger _log;
private readonly User _user;
public UserLogger(User u)
{
_user = u;
_log = LogManager.GetCurrentClassLogger();
}
public void Error(string message)
{
LogEventInfo logEvent =
new LogEventInfo(LogLevel.Error, _log.Name, message);
logEvent.Properties["UserId"] = _user.Id;
_log.Log(logEvent);
}
}
This is just a simple example to get you started. The cool feature I'm using here is that I've defined the log's filename (in the Nlog.Config target) using the UserId value and thus can ensure that each user gets logged to their own unique log file.
This way, you can enforce that the user ID is known when you want to log to the "user log" target. As an additional bonus, it also neatly decouples NLog dependencies from your calling code.
Assuming you have a log file called mylogfile.log in your nlog.config file
FileTarget target = LogManager.Configuration.FindTargetByName("mylogfile.log") as FileTarget;
String customlog = "Log_" + GetUserId(UserId_From_DB) + "_" + DateTime.Now.ToString("yyyy-MM-dd") + ".log";
target.FileName = customlog;

API to resolve %% variables

I am working with the C# Event Log API in Windows (essentially everything in System.Diagnostics.Eventing.Reader).
I have an EventMetadata object and pull its Description property to retrieve the message template for an event.
Generally, these templates look similar to the following:
Network interface reports message %1
These variables are easily replaceable with actual data whenever I receive an event.
(EventLogRecord.Properties match up to the placeholders)
Here is where my problem comes in. The EventLogRecord.Properties sometimes contain different kinds of placeholders. These always begin in %% and I cannot find a way of resolving them.
As an example:
// This method is triggered when a new event comes in
async public static void ListenerEvent(object s, EventRecordWrittenEventArgs args) {
var evt = (EventLogRecord)args.EventRecord;
// This method retrieves the template from a ProviderMetadata object
// And replaces all %n with {n}
// So that we can string.Format on it
var tmp = TemplateCache.TemplateFor(evt);
// Need this since the indices start with 1, not 0
var props = new List<object> {string.Empty};
props.AddRange(evt.Properties.Select(prop => prop.Value));
// Now the message should be human-readable
var msg = string.Format(tmp, props);
}
Using the above example template, the Properties might be ["%%16411"] and now I end up with the following message
Network interface reports message %%16411
I figure my question now is, how do I replace this %%16411?
I have looked into ProviderMetadata and the rest of its properties but none seem to match up.
Any help figuring out how to resolve these placeholders (or even what they are/where they come from) is appreciated.
An event that shows this behaviour is 5152, as found here: http://community.spiceworks.com/windows_event/show/452-microsoft-windows-security-auditing-5152
Thank you.

Log4net output to My Documents

In our C# app, we write files to Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments). Our log4net logfile should go there too, so we've defined application.conf as follows:
<appender name="LogFile" type="log4net.Appender.RollingFileAppender">
<appendToFile value="true"/>
<file value="%USERPROFILE%\My Documents\MyApp\log.txt"/>
...snip...
</appender>
This works, until we run in on a PC which has a non-English Windows. Because then, SpecialFolder.MyDocuments points to the folder Mijn Documenten, while the log still goes to My Documents. Confusion ensues, because now our files are in two places.
I want to write my log to the "real" My Documents folder. How do I do this?
I tried to find an environment variable like %USERPROFILE%, but there doesn't seem to exist one for My Documents.
There's a registry key that defines the true location of My Documents but it's not accessible from application.conf.
I tried to override the File parameter of my appender programmatically, like this:
public static void ConfigureLogger()
{
XmlConfigurator.Configure();
Hierarchy hierarchy = (Hierarchy)log4net.LogManager.GetRepository();
foreach (var appender in hierarchy.Root.Appenders)
{
if (appender is FileAppender)
{
var fileAppender = appender as FileAppender;
var logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyApp");
var pathElements = fileAppender.File.Split(Path.DirectorySeparatorChar);
var logFileName = pathElements.Last();
var fullLogFilePath = Path.Combine(logDirectory, logFileName);
fileAppender.File = fullLogFilePath;
}
}
}
This doesn't work either: when I inspect the internals of my logger, the File property happily reports Mijn Documenten, but in the mean time the logs still go to My Documents.
I'm running out of ideas!
Change the line
<file value="%USERPROFILE%\My Documents\MyApp\log.txt"/>
to
<file type="log4net.Util.PatternString" value="%envFolderPath{MyDocuments}\MyApp\log.txt" />
The accepted answer is out of date.
You should now use:
<file value="${UserProfile}\Documents\log-messages.log" />
(This should work even on Windows 7, where "Documents" is referred to by the alias "My Documents".)
${UserProfile} will map to C:\Users[UserName], even if you don't see this variable explicitly defined in your environment variable list.
There seem to be at least 2 approaches. The simplest is kind of a hack:
Specify a custom environment variable to indicate the root path in you log4net config:
<file value="%MYAPP_USER_ROOTFOLDER%\MyApp\log.txt"/>
At startup, before initializing logging, set this environment variable value:
Environment.SetEnvironmentVariable("MYAPP_USER_ROOTFOLDER", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
The more complicated but recommended approach is explained here: http://marc.info/?l=log4net-user&m=110142086820117&w=2 and here http://ziqbalbh.com/articles/log4net-another-way-to-change-log-file-location-on-runtime/

c# log4net store entries in memory and Email under certain conditions

I currently use log4net with a RollingFileAppender.
As each Log call is made I'd like to store this in memory. At the end of my console application run I'd like to (if an app.config setting is true) take only the Warns and Fatals and send all these messages in an Email. I notice MemoryAppender but not quite sure how to use it. Also see SMTPAppender but not sure it is the right tool, else I'll use MemoryAppender and somehow filter out only events of Levels Warn/Fatal and then email using the SmtpClient class.
How to achieve this?
Thanks
Update
My last part of log4net config now looks like.
<appender name="MemoryAppender" type="log4net.Appender.MemoryAppender" >
<onlyFixPartialEventData value="true" />
<threshold value="WARN" />
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="Console" />
<appender-ref ref="RollingFile" />
<appender-ref ref="MemoryAppender" />
</root>
In code I do:
private static MemoryAppender MemoryAppender
{
get
{
if (memoryAppender == null)
{
Hierarchy h = LogManager.GetRepository() as Hierarchy;
memoryAppender = h.Root.GetAppender("MemoryAppender") as MemoryAppender;
}
return memoryAppender;
}
}
Then when I want the events I call:
MemoryAppender.GetEvents();
I've tried MemoryAppender.GetEvents()[0].RenderedMessage but that is not the correct output, how do I get the message string as it was written to the File/Console logs with the correct pattern and time etc and build myself a StringBuilder? I'll then put this in the body of my Email and send it using the SmtpClient. RenderMessage is just giving me the string that was provided to the Log.Warn() call not what was written to the log. Is this due to not setting a layout pattern on the MemoryAppender?
Thanks
MemoryAppender will only "append" to memory and is thus mostly useful only for development and testing purposes. And there is currently no appender that will only append on application shutdown.
The SMTPAppender is something in between, since it inherits the BufferingAppenderSkeleton. These appenders have a BufferSize property which controls how many messages are kept in memory before they are flushed.
Which messages to pass to the appenders are controlled with the level settings either on the root element or on individual logger elements. In your case use a level of WARN which will let through WARN, ERROR and FATAL. If you don't want the ERROR messages you will have to put a level filter on your appender.
Update: MemoryAppender is not using any layout to "render" message objects. What you get from MemoryAppender is just the raw message objects as they are produced by log4net. You will have to convert those to meaningfull text yourself.
Alternatively, if you require both layout functionality and in-memory appending you could look into subclassing AppenderSkeleton. That way you get the basic Layout support. When implementing the Append method you can do what MemoryAppender does, that is just appending to an internal list of messages.
Update 2: to implement the MemoryAppender alternative I suggest taking the MemoryAppender as a starting point. MemoryAppender is a subclass of AppenderSkeleton and have thus access to the RenderLoggingEvent method. So, we subclass MemoryAppender and add a method that renders the current batch of logging events:
public class RenderingMemoryAppender : MemoryAppender
{
public IEnumerable<string> GetRenderedEvents()
{
foreach(var loggingEvent in GetEvents())
{
yield return RenderLoggingEvent(loggingEvent);
}
}
}
You can use SMTPAppender and look at how the flush functionality works. Log4net keeps all messages in memory until flush is called (if it's setup this way), so the email will be sent when you flush it.
Another thing you can do is create a separate appender (Rolling or FileAppender) with filters WARN and FATAL, then attach this appender to the same logger, and at the end of your run email this file if it's non-empty (and you can choose to send it as an attachment or right in the body of the email). Let me know if you want more details, this is almost the same I'm doing know.
Good luck!
Ricardo.

Categories

Resources