Is it possible with NLog to emit a multi-line message such that each line is formatted according to the current layout? E.g.
2015-12-17 11:37:38.0845 | 64 | INFO | -----------------------------------
2015-12-17 11:37:38.0845 | 64 | INFO | Statistics:
2015-12-17 11:37:38.0845 | 64 | INFO | Crawling Requests 46887 /min
2015-12-17 11:37:38.0845 | 64 | INFO | Stored Documents 9910 /min
2015-12-17 11:37:38.0845 | 64 | INFO | -----------------------------------
Alternatively, is it possible with NLog to emit multiple messages as a single, non-interrupted block in a multithreaded environment?
You can do all this from your config.
<variable name="statData" value="${longdate} | 64 | ${level} | "/>
<variable name="line" value="-----------------------------------"/>
<targets>
<target xsi:type="Console"
name="Console"
layout="
${statData}${line}${newline}
${statData}Statistics:${newline}
${statData} Crawling Requests 46887 /min ${newline}
${statData} Stored Documents 9910 /min ${newline}
${statData}${line}${newline}"/>
Wasn't exactly sure what your 64 was or where you were getting your per minute data. Probably a variable or something your inserting.This should also work if you are logging to a file not the console.
As for your second question, if you are wanting a single log message from multiple threads I think you would have to do that on the code side. You would have to collect your threads, get your log data you want and send it 1 time to nLog. I might be misunderstanding though
There does not appear to be existing functionally to do this yet (as of NLog 4.2.3). A potential solution is to create your own wrapper layout renderer to improve the functionality of the replace-newlines renderer.
So replace-newlines and replace wrappers will not take layout renderers in their replacement strings. Looking at the NLog source for other wrappers, renderers, and targets, a Layout type property can be used to accept a string with (or without) layout renderers. The built in replace wrappers fail when provided a layout renderer because their Replacement property type is string. The xml parser is only looking for plain text but the } of a provided layout renderer prematurely ends the replace-newline wrapper.
The following custom wrapper changes the replacement type from string to Layout. The replacement layout then needs to be rendered by calling its Render method with some context (LogEventInfo). This can be done in the overridden Append method where LogEventInfo is available. The rendered output can be saved off for later usage in the Transform method.
using System;
using System.Text;
using NLog.Config;
using NLog.LayoutRenderers;
using NLog.LayoutRenderers.Wrappers;
using NLog.Layouts;
namespace My.Namespace
{
[LayoutRenderer("replace-newlines-withlayout")]
[ThreadAgnostic]
public class ReplaceNewLinesFormatLayoutRendererWrapper : WrapperLayoutRendererBase
{
private string m_replacementString = " ";
public ReplaceNewLinesFormatLayoutRendererWrapper()
{
// Changed from
// Replacement = " ";
Replacement = Layout.FromString(" ");
}
// Changed from
// public string Replacement { get; set; }
public Layout Replacement { get; set; }
// Override Append in order to render the replacement.
protected override void Append(StringBuilder builder, NLog.LogEventInfo logEvent)
{
// Render...
m_replacementString = Replacement.Render(logEvent);
// The base functionality of append is fine.
base.Append(builder, logEvent);
}
// Called from base.Append()
protected override string Transform(string text)
{
// Changed from
// return text.Replace(Environment.NewLine, Replacement);
// Now just put in the rendered replacement string.
return text.Replace(Environment.NewLine, m_replacementString);
}
}
}
And then, for example, use it as
<target
...
layout=${replace-newlines-withlayout:replacement=${newline}${time}:${message}}
...
/>
In this simplified case, assuming ${message} has line breaks, each new line will be prefixed with a time stamp. Just replace ${time} with the desired prefix layout.
Related
As a step in switching from NLog to Serilog, I want to redirect the standard wiring underlying standard invocations of NLog's LogManager.GetLogger(name) to Bridge any code logging to NLog to forward immediately to the ambient Serilog Log.Logger - i.e. I want to just one piece of config that simply forwards the message, without buffering as Log4net.Appender.Serilog does for Log4net.
Can anyone concoct or point me to a canonical snippet that does this correctly and efficiently please? Requirements I can think of:
Maintain the level, i.e. nlog.Warn should be equivalent to serilog.Warning
It's ok for Serilog to generate the time anew
materializing the message in the appender - i.e., there's no need to maintain any properties associated with the 'message' (the LogEvent in Serilog terms)
no buffering
I don't need any other NLog Target to be used (i.e. mutating/deleting the message would be fine)
I think the best option is indeed a custom NLog target. Something like this: (C#)
using NLog;
using NLog.Targets;
using Serilog;
using Serilog.Events;
namespace MyNamespace
{
[Target("SerilogTarget")]
public sealed class SerilogTarget : TargetWithLayout
{
protected override void Write(LogEventInfo logEvent)
{
var log = Log.ForContext(Serilog.Core.Constants.SourceContextPropertyName, logEvent.LoggerName);
var logEventLevel = ConvertLevel(logEvent.Level);
if ((logEvent.Parameters?.Length ?? 0) == 0)
{
// NLog treats a single string as a verbatim string; Serilog treats it as a String.Format format and hence collapses doubled braces
// This is the most direct way to emit this without it being re-processed by Serilog (via #nblumhardt)
var template = new Serilog.Events.MessageTemplate(new[] { new Serilog.Parsing.TextToken(logEvent.FormattedMessage) });
log.Write(new Serilog.Events.LogEvent(DateTimeOffset.Now, logEventLevel, logEvent.Exception, template, Enumerable.Empty<Serilog.Events.LogEventProperty>()));
}
else
// Risk: tunneling an NLog format and assuming it will Just Work as a Serilog format
#pragma warning disable Serilog004 // Constant MessageTemplate verifier
log.Write(logEventLevel, logEvent.Exception, logEvent.Message, logEvent.Parameters);
#pragma warning restore Serilog004
}
static Serilog.Events.LogEventLevel ConvertLevel(LogLevel logEventLevel)
{
if (logEventLevel == LogLevel.Info)
return Serilog.Events.LogEventLevel.Information;
else if (logEventLevel == LogLevel.Trace)
return Serilog.Events.LogEventLevel.Verbose;
else if (logEventLevel == LogLevel.Debug)
return Serilog.Events.LogEventLevel.Debug;
else if (logEventLevel == LogLevel.Warn)
return Serilog.Events.LogEventLevel.Warning;
else if (logEventLevel == LogLevel.Error)
return Serilog.Events.LogEventLevel.Error;
return Serilog.Events.LogEventLevel.Fatal;
}
}
}
register it in your main() or app_start:
// Register so it can be used by config file parsing etc
Target.Register<MyNamespace.SerilogTarget>("SerilogTarget");
Before any logging takes place, the Target needs to be wired in so LogManager.GetLogger() can actually trigger a call to SerilogTarget.Write
public static void ReplaceAllNLogTargetsWithSingleSerilogForwarder()
{
// sic: blindly overwrite the forwarding rules every time
var target = new SerilogTarget();
var cfg = new NLog.Config.LoggingConfiguration();
cfg.AddTarget(nameof(SerilogTarget), target);
cfg.LoggingRules.Add(new NLog.Config.LoggingRule("*", LogLevel.Trace, target));
// NB assignment must happen last; rules get ingested upon assignment
LogManager.Configuration = cfg;
}
See also: https://github.com/nlog/nlog/wiki/How-to-write-a-custom-target
the optimal way to do this without inducing any avoidable perf impact etc.
This is the optimal way in NLog and has no performance impact on the NLog's site.
what does the TargetAttribute buy me ?
Well in this case you don't need it. The TargetAttribute is used when registering a full assembly, but because we register manually, it's not needed. I think it's best practice, but you could leave it out.
Also what does the Register buy me
This is indeed not needed when using programmatically config. But if you have XML config, you need the register.
I've the habit to write targets that works in all ways (register manually, register by assembly, config from code, config from XML). I could understand that could be confusing.
F# port:
module SerilogHelpers =
let private mapLevel = function
| x when x = NLog.LogLevel.Info -> LogEventLevel.Information
| x when x = NLog.LogLevel.Off || x = NLog.LogLevel.Trace -> LogEventLevel.Verbose
| x when x = NLog.LogLevel.Debug -> LogEventLevel.Debug
| x when x = NLog.LogLevel.Warn -> LogEventLevel.Warning
| x when x = NLog.LogLevel.Error -> LogEventLevel.Error
| _ -> LogEventLevel.Fatal
// via https://stackoverflow.com/a/49639001/11635
[<NLog.Targets.Target("SerilogTarget")>]
type SerilogTarget() =
inherit NLog.Targets.Target()
static member InitializeAsGlobalTarget() =
// sic: blindly overwrite the forwarding rules every time
// necessary as Azure Startup establishes a different config as a bootstrapping step
// see: LogModule.To.target("rollingFile", create, "*", LogLevel.Trace)
let cfg, target = NLog.Config.LoggingConfiguration(), SerilogTarget()
cfg.AddTarget("SerilogTarget", target)
cfg.LoggingRules.Add(NLog.Config.LoggingRule("*", NLog.LogLevel.Trace, target))
// NB assignment must happen last; rules get ingested upon assignment
NLog.LogManager.Configuration <- cfg
override __.Write(logEvent : NLog.LogEventInfo) =
let log = Log.ForContext(Serilog.Core.Constants.SourceContextPropertyName, logEvent.LoggerName)
match logEvent.Parameters with
| xs when isNull xs || xs.Length = 0 ->
// NLog treats a single string as a verbatim string; Serilog treats it as a String.Format format and hence collapses doubled braces
// This is the most direct way to emit this without it being re-processed by Serilog (via #nblumhardt)
let template = MessageTemplate [| Serilog.Parsing.TextToken(logEvent.FormattedMessage) |]
log.Write(new LogEvent(DateTimeOffset.Now, mapLevel logEvent.Level, logEvent.Exception, template, Seq.empty<LogEventProperty>))
| _ ->
// Risk: tunneling an NLog format and assuming it will Just Work as a Serilog format
log.Write(mapLevel logEvent.Level, logEvent.Exception, logEvent.Message, logEvent.Parameters)
I have a widget with list of last news, how to cache only widget output?
OutputCache module caches whole page and for anonymous users, but in fact I need to cache only one shape output.
What solution can be here?
It's not a good idea to cache the Shape object itself, but you can capture the HTML output from a Shape and cache that.
Every Orchard Shape has a corresponding object called the Metadata. This object contains, among other things, some event handlers that can run when the Shape is displaying or after it has been displayed. By using these event handlers, it is possible to cache the output of the Shape on the first call to a driver. Then for future calls to the driver, we can display the cached copy of the output instead of running through the expensive parts of the driver or template rendering.
Example:
using System.Web;
using DemoModule.Models;
using Orchard.Caching;
using Orchard.ContentManagement.Drivers;
using Orchard.DisplayManagement.Shapes;
namespace DemoModule.Drivers {
public class MyWidgetPartDriver : ContentPartDriver<MyWidgetPart> {
private readonly ICacheManager _cacheManager;
private readonly ISignals _signals;
public MyWidgetPartDriver(
ICacheManager cacheManager,
ISignals signals
) {
_cacheManager = cacheManager;
_signals = signals;
}
public class CachedOutput {
public IHtmlString Output { get; set; }
}
protected override DriverResult Display(MyWidgetPart part, string displayType, dynamic shapeHelper) {
return ContentShape("Parts_MyWidget", () => {
// The cache key. Build it using whatever is needed to differentiate the output.
var cacheKey = /* e.g. */ string.Format("MyWidget-{0}", part.Id);
// Standard Orchard cache manager. Notice we get this object by reference,
// so we can write to its field to save our cached HTML output.
var cachedOutput = _cacheManager.Get(cacheKey, ctx => {
// Use whatever signals are needed to invalidate the cache.
_signals.When(/* e.g. */ "ExpireCache");
return new CachedOutput();
});
dynamic shape;
if (cachedOutput.Output == null) {
// Output has not yet been cached, so we are going to build the shape normally
// and then cache the output.
/*
... Do normal (potentially expensive) things (call DBs, call services, etc.)
to prep shape ...
*/
// Create shape object.
shape = shapeHelper.Parts_MyWidget(/*...*/);
// Hook up an event handler such that after rendering the (potentially expensive)
// shape template, we capture the output to the cached output object.
((ShapeMetadata) shape.Metadata).OnDisplayed(displayed => cachedOutput.Output = displayed.ChildContent);
} else {
// Found cached output, so simply output it instead of building
// the shape normally.
// This is a dummy shape, the name doesn't matter.
shape = shapeHelper.CachedShape();
// Hook up an event handler to fill the output of this shape with the cached output.
((ShapeMetadata)shape.Metadata).OnDisplaying(displaying => displaying.ChildContent = cachedOutput.Output);
// Replacing the ChildContent of the displaying context will cause the display manager
// to simply use that HTML output and skip template rendering.
}
return shape;
});
}
}
}
EDIT:
Note that this only caches the HTML that is generated from your shape output. Things like Script.Require(), Capture(), and other side effects that you perform in your shape templates will not be played back. This actually bit me because I tried to cache a template that required its own stylesheet, but the stylesheets would only be brought in the first time.
Orchard supplies a service called the CacheManager, which is awesome and cool and makes caching super easy. It is mentioned in the docs, but it isn't a particularly helpful description of how to use it (http://docs.orchardproject.net/Documentation/Caching). Best place to see examples would be in the Orchard core code and third party modules such as Favicon and the twitter widgets (all of them one would hope).
Luckily other nice people have gone to the effort of searching orchards code for you and writing nice little blog posts about it. The developer of the LatestTwitter widget wrote a neat post: http://blog.maartenballiauw.be/post/2011/01/21/Writing-an-Orchard-widget-LatestTwitter.aspx . So did Richard of NogginBox: http://www.nogginbox.co.uk/blog/orchard-caching-by-time . And of course Bertrand has a helpful post on the subject as well: http://weblogs.asp.net/bleroy/archive/2011/02/16/caching-items-in-orchard.aspx
I would like to change nlog layout so it displays time between two entries. Is such layout possible?
This is my output now, and it would be great if next to the time there is number of seconds elapsed since entry before.
2012-05-20 19:18:41.0924|INFO|HashMatcher.LocalMatcherControl|99 archives discovered
2012-05-20 19:18:56.5267|INFO|HashMatcher.LocalMatcherControl|memory usage BEFORE loading samples 283.06 Mb
2012-05-20 19:18:56.5267|INFO|HashMatcher.LocalMatcherControl|performing sample scan
2012-05-20 19:19:08.9131|INFO|HashMatcher.LocalMatcherControl|memory usage BEFORE creating progress 285.43 Mb
2012-05-20 19:20:16.5804|INFO|HashMatcher.LocalMatcherControl|skipped progress 0
2012-05-20 19:20:17.6100|INFO|HashMatcher.LocalMatcherControl|memory usage BEFORE sorting progress 205.06 Mb
I don't think that there is anything built into NLog that provides this capability "free", but you should be able to implement it yourself pretty easily using a custom LayoutRenderer. You can find many examples here (in NLog's source repository).
You might end up with something like this (neither compiled nor tested by me):
[LayoutRenderer("ElapsedTime")]
[ThreadAgnostic]
public class ElapsedTimeLayoutRenderer : LayoutRenderer
{
DateTime? lastTimeStamp;
public ElapsedTimeLayoutRenderer()
{
}
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
var stamp = logEvent.TimeStamp;
var span = stamp - lastTimeStamp.HasValue ? lastTimeStamp : stamp;
builder.Append(stamp.ToString());
lastTimeStamp = stamp;
}
}
In essence the layout renderer remembers the time stamp of the last message logged. The value of this layout renderer is the difference between the last message logged and the time stamp of the current message. This layout renderer could probably use some enhancements. If you look at the DateTime you can see that it also has properties for CultureInfo and Format. Depending on your needs, you might even want to provide the option to give the elapsed time since application startup (or at least since the first message logged).
Also, since a member variable is being modified (lastTimeStamp), it probably should be protected with a lock statement.
Hopefully this will help you out in getting the capability that you need.
Good luck!
[LayoutRenderer("elapsedtime")]
[ThreadAgnostic]
public class ElapsedTimeLayoutRenderer : LayoutRenderer
{
Stopwatch sw;
public ElapsedTimeLayoutRenderer()
{
this.sw = Stopwatch.StartNew();
}
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
builder.Append(this.sw.ElapsedMilliseconds.ToString().PadLeft(6));
this.sw.Restart();
}
}
Then register this renderer at the begin of the program
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("elapsedtime", typeof (ElapsedTimeLayoutRenderer));
Notes
Before the config line above is run, no logging should occur, so any static logger members like
static Logger logger = LogManager.GetCurrentClassLogger();
that use this renderer are not allowed before this line. If anybody knows how to configure the renderer in the config, please comment or edit.
I have a RazorHelpers.cshtml file in app_code which looks like:
#using Molecular.AdidasCoach.Library.GlobalConstants
#helper Translate(string key)
{
#GlobalConfigs.GetTranslatedValue(key)
}
However, I have a case where I want to use the result as the link text in an #Html.ActionLink(...). I cannot cast the result to a string.
Is there any way to return plain strings from Razor helpers so that I can use them both in HTML and within an #Html helper?
Razor helpers return HelperResult objects.
You can get the raw HTML by calling ToString().
For more information, see my blog post.
I don't think there is a way to make #helper return other types than HelperResult. But you could use a function with a return type of string, e.g.
#functions {
public static string tr(string key) {
return GlobalConfigs.GetTranslatedValue(key);
}
}
then
#Html.ActionLink(tr("KEY"), "action", "controller")
See also http://www.mikesdotnetting.com/article/173/the-difference-between-helpers-and-functions-in-webmatrix
edit: MVC Razor: Helper result in html.actionlink suggests your helper can return a string by using #Html.Raw(GlobalConfigs.GetTranslatedValue(key));
In your case, I think this would also work:
#(GlobalConfigs.GetTranslatedValue(key))
Additional sample:
#helper GetTooltipContent()
{
if(Model.SubCategoryType == SUBCATTYPE.NUMBER_RANGE)
{
#(string.Format("{0} to {1}", Model.SubCategoryMinimum, Model.SubCategoryMaximum))
}
else if(Model.SubCategoryType == SUBCATTYPE.NUMBER_MAXIMUM)
{
#("<= " + Model.SubCategoryMaximum)
}
else if(Model.SubCategoryType == SUBCATTYPE.NUMBER_MINIMUM)
{
#(">= " + Model.SubCategoryMinimum)
}
}
The following statements have been validated against MVC version 5.2.4.0. I am mostly targeting the part with: Is there any way to return plain strings from Razor helpers so that I can use them both in HTML and within an #Html helper?
I did some research on how the built in MVC helpers work and they are actually properties of System.Web.Mvc.WebViewPage class, so they have nothing to do with #helper feature.
Any #helper encodes strings as HTML and works as if the code is copy pasted to the View inside a Razor code block, aka #{ code }. On the other side, #functions are supposed to be used inside Razor blocks.
Well, if a #helper works as if the code is copy pasted, why not use #Html.Raw("<p>cool</p>")? Because the Html property is null inside helpers. Why? I have no idea.
Ok, but we can use a function to return a string and then apply #Html.Raw on the result. Does that work? Yes, it does. The following example creates a <p> element in the DOM:
#functions
{
static string GetString()
{
return "<p>awesome</p>";
}
}
#Html.Raw(GetString())
If you don't understand why #Html.Raw is necessary, please read this fine article from #SLaks about Razor automatic HTML encoding.
What about the approach with the built in properties? Yes, it is possible to create static classes with public methods that work just like that. The only problem is that you have to include the namespace in the View, with the #using keyword. Can that be improved? Yes, by adding the namespace in the Web.config within the Views folder. Example:
Helpers/Global.cs
namespace WebApp.Helpers
{
public static class Global
{
public static IHtmlString GetString()
{
return new HtmlString("Something <b>cool</b>");
}
}
}
Views/Web.config
<?xml version="1.0"?>
<configuration>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.4.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<!-- Add to the end of namespaces tag. -->
<add namespace="WebApp.Helpers" />
Usage
#Global.GetString()
What is the outcome? The text Something and an additional <b> element will be found in the DOM. If you need access to the Request, simply add an HttpContextBase parameter to the helper method and pass the WebViewPage.Context property when calling it.
Can it get better? Yes, as always. The same output can be created with #helper and #functions:
#helper GetString1()
{
#(new HtmlString("Something <b>awesome</b>"))
}
#functions {
public static IHtmlString GetString2()
{
return new HtmlString("Something <b>awesome</b>");
}
}
#MyHelper.GetString1()
#MyHelper.GetString2()
Answer
Regarding OP's question, I recommend #Spikolynn's approach to create a function that returns string. However, if you need to write many lines of C# code in the helper, I suggest using a static class helper.
Is there a way to log an event into the windows event log with a specified eventid per message? I am using log4net v 1.2.10.
Based on what I see in the EventLogAppender source code the following should do the trick:
log4net.ThreadContext.Properties["EventID"] = 5;
Just call this before you write your log messages (if you do not set it for all messages you should remove the "EventID" again from the Properties.
N.B the property key is case sensitive.
When one uses the native .net Event Log APIs in System.Diagnostics, the WriteEntry methods allow setting the eventID and category. In these APIs:
eventID is a 32 bit int, but its value must be between 0 and 65535
category is a 16 bit int, but its value must be positive. If the
event source includes a category resource file, the event viewer will
use the integer category value to lookup a localized “Task category”
string. Otherwise, the integer value is displayed.
The categories must be numbered consecutively, beginning with the
number 1
Log4net supports writing an EventID and a Category, but it isn’t straight forward. When log4net’s EventLogAppender logs an event, it looks at a dictionary of properties. The named properties "EventID" and "Category" are automatically mapped by the EventLogAppender to the corresponding values in the event log. I’ve seen a few good suggested ways to use log4net’s EventLogAppender and set the EventID and Category in the Windows event log.
a. Using log4net’s appender filtering, a filter may be registered that can add the EventID and Category properties. This method has a nice benefit that the standard log4net wrappers are used and so this can be implemented without changing existing logging code. The difficulty in this method is some mechanism has to be created to calculate the EventID and Category from the logged information. For instance, the filter could look at the exception source and map that source to a Category value.
b. Log4net may be extended so custom logging wrappers can be used that can include EventID and Category parameters. Adding EventID is demonstrated in the log4net sample “Extensibility – EventIDLogApp” which is included in the log4net source. In the extension sample a new interface (IEventIDLog) is used that extends the standard ILog interface used by applications to log. This provides new logging methods that include an eventId parameter. The new logging methods add the eventId to the Properties dictionary before logging the event.
public void Info(int eventId, object message, System.Exception t)
{
if (this.IsInfoEnabled)
{
LoggingEvent loggingEvent = new LoggingEvent(ThisDeclaringType, Logger.Repository, Logger.Name, Level.Info, message, t);
loggingEvent.Properties["EventID"] = eventId;
Logger.Log(loggingEvent);
}
}
c. Log4net supports a ThreadContext object that contains a Properties dictionary. An application could set the EventID and Category properties in this dictionary and then when the thread calls a logging method, the values will be used by the EventLogAppender.
log4net.ThreadContext.Properties["EventID"] = 5;
Some helpful references:
Log4net home page
Log4net SDK reference
Log4net samples
Log4net source
Enhancing log4net exception logging
Log4Net Tutorial pt 6: Log Event Context
Customizing Event Log Categories
EventSourceCreationData.CategoryResourceFile Property
Event Logging Elements
EventLog.WriteEntry Method
Well, the solution was to build the extension project "log4net.Ext.EventID" and to use its types: IEventIDLog, EventIDLogImpl and EventIDLogManager.
Another solution is to add a custom Filter as described here: Enhancing log4net exception logging (direct link to the Gist just in case).
As the author points out:
... EventLogAppender uses inline consts to check them. Once they are added they will be used by the mentioned EventLogAppender to mark the given entries with EventId and Category.
The filter implementation will look like the code below (stripped down gist) with the added benefit that if you make GetEventId method public, you can write some tests against it
public class ExceptionBasedLogEnhancer : FilterSkeleton
{
private const string EventLogKeyEventId = "EventID";
public override FilterDecision Decide(LoggingEvent loggingEvent)
{
var ex = loggingEvent.ExceptionObject;
if (ex != null)
{
loggingEvent.Properties[EventLogKeyEventId] = GetEventId(ex);
}
return FilterDecision.Neutral;
}
private static short GetEventId(Exception ex)
{
// more fancy implementation, like getting hash of ex properties
// can be provided, or mapping types of exceptions to eventids
// return no more than short.MaxValue, otherwise the EventLog will throw
return 0;
}
}
Extend ILog.Info() to take an event ID:
public static class LogUtils
{
public static void Info(this ILog logger, int eventId, object message)
{
log4net.ThreadContext.Properties["EventID"] = eventId;
logger.Info(message);
log4net.ThreadContext.Properties["EventID"] = 0; // back to default
}
}
Then call it like this:
using LogUtils;
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
_logger.Info(3, "First shalt thou take out the Holy Pin, then shalt thou count to three.");