Custom PatternLayoutConverter with log4net.Ext.Json? - log4net

I have the following log4net configuration:
<log4net>
<appender name="Console" type="log4net.Appender.ConsoleAppender">
<layout type='log4net.Layout.SerializedLayout, log4net.Ext.Json'>
<renderer type='log4net.ObjectRenderer.JsonDotNetRenderer, log4net.Ext.Json.Net'>
<DateFormatHandling value="IsoDateFormat" />
<NullValueHandling value="Ignore" />
</renderer>
<converter>
<name value="preparedMessage" />
<type value="JsonLogs.CustomLayoutConverter" />
</converter>
<default />
<remove value='message' />
<remove value='ndc' />
<member value='message:messageObject' />
<member value='details:preparedMessage' />
</layout>
</appender>
<appender name="Console2" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<converter>
<name value="preparedMessage" />
<type value="JsonLogs.CustomLayoutConverter" />
</converter>
<conversionPattern value="%level %thread %logger - %preparedMessage%newline" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="Console" />
<appender-ref ref="Console2" />
</root>
</log4net>
with the following implementation of my custom PatternLayoutConverter:
namespace JsonLogs
{
using System.IO;
using log4net.Core;
using log4net.Layout.Pattern;
public class CustomLayoutConverter : PatternLayoutConverter
{
#region Methods
protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
{
if (loggingEvent.MessageObject is string stringMessage)
{
writer.Write(new { message = stringMessage });
}
else
{
writer.Write(loggingEvent.RenderedMessage);
}
}
#endregion
}
}
For some reason, the converter works perfectly fine with the Console2 appender(which is not JSON driven) but it doesn't work with the Console appender whose output is JSON.
Example of the output:
Console -> {"date":"2018-12-09T12:25:28.0529041+03:00","level":"INFO","appname":"JsonLogs.exe","logger":"JsonLogs.Program","thread":"1","message":"Test","details":"preparedMessage"}
Console2 -> INFO 1 JsonLogs.Program - { message = Test }
My goal is to have details always in JSON that's why I introduced my own converter to catch primitive values and wrap them in a custom object.
Is my configuration wrong? Or I'm missing something? Could you help me, please, to figure this out?
Thank you

The issue seems to be a bug of log4net.Ext.Json. I'm going to report it on their GitLab.
So far, I ended up with my custom log4net layout which looks like this
public class CustomLayout : PatternLayout
{
#region Public Methods and Operators
public override void Format(TextWriter writer, LoggingEvent loggingEvent)
{
var message = loggingEvent.MessageObject.GetType().IsPrimitive || loggingEvent.MessageObject is string || loggingEvent.MessageObject is decimal || loggingEvent.MessageObject is BigInteger
? new { message = loggingEvent.MessageObject }
: loggingEvent.MessageObject;
writer.WriteLine(JsonConvert.SerializeObject(new
{
timestamp = loggingEvent.TimeStampUtc,
threadId = loggingEvent.ThreadName,
details = message,
logger = loggingEvent.LoggerName,
level = loggingEvent.Level.DisplayName,
user = loggingEvent.UserName
}));
}
#endregion
}
it meets my needs and does exactly what I want.

The exact place of this problem is AddMember Method and its implementation. Here is SerializedLayout source code for that:
public virtual void AddMember(string value)
{
var arrangement = log4net.Util.TypeConverters.ArrangementConverter.GetArrangement(value, new ConverterInfo[0]);
m_arrangement.AddArrangement(arrangement);
}
As you can see the second parameter of GetArrangment is empty array of ConverterInfo, Though there must be our custom attached ones (by AddConverter method or by xml).
As the solution you can implement your own subclass that will derive from SerializedLayout with overridden AddMember like this:
public override void AddMember(string value)
{
var customConverter = new ConverterInfo("lookup", typeof(CustomPatternConverter));
var arrangement = log4net.Util.TypeConverters.ArrangementConverter.GetArrangement(value, new ConverterInfo[] { customConverter });
m_arrangement.AddArrangement(arrangement);
}
Hope it helps as it did with my case!

Related

Setting up Log4Net Colored Console Made Easy

I am looking for a Log4Net viewer that I can watch in realtime in a separate console window not in the Visual Studio output window where its mixed with hundreds of other messages.
I see there is a ManagedColoredConsoleAppender and ColoredConsoleAppender out of the box, but unsure how to get the output to be directed to an external console window. Ideally, when you debug the console window would launch, but not a requirement.
I have used the OutputDebugger back in the day, but have not found an easy way to get all this working.
If somebody could share how to get all this wired up and working quickly that would be much appreciated.
This would mainly be used for development locally, but would be nice if we could redirect errors from staging and qa to a nice little window on my machine to see whats going on in realtime!
The ColoredConsoleAppender (and it's successor, ManagedColoredConsoleAppender) won't start a console for you.
However, if you start a console up manually, they will use it - here's a sample:
class Program {
[DllImport("kernel32.dll", EntryPoint = "GetStdHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", EntryPoint = "AllocConsole", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern int AllocConsole();
private const int STD_OUTPUT_HANDLE = -11;
private const int MY_CODE_PAGE = 437;
private static readonly object lockObj = new object();
static void Main(string[] args) {
AllocConsole();
IntPtr stdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
SafeFileHandle safeFileHandle = new SafeFileHandle(stdHandle, true);
FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write);
Encoding encoding = Encoding.GetEncoding(MY_CODE_PAGE);
StreamWriter standardOutput = new StreamWriter(fileStream, encoding) { AutoFlush = true };
Console.SetOut(standardOutput);
XmlConfigurator.Configure();
var log = LogManager.GetLogger("test");
log.Debug("Starting Program");
log.Error("Oh no, an error");
// etc
This is the output:
The configuration is really simple:
<log4net>
<appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
<mapping>
<level value="ERROR" />
<foreColor value="White" />
<backColor value="Red, HighIntensity" />
</mapping>
<mapping>
<level value="DEBUG" />
<backColor value="Green" />
</mapping>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="ColoredConsoleAppender" />
</root>

Logging only top-most ThreadContext stack item

We're using Log4net's ThreadContext.Stacks and it is mostly working well. My problem comes if there have been multiple ThreadContext.Stacks["key"].Push(...).
With a simple ConversionPattern:
<param name="ConversionPattern value="... topProp=%properties{key} ..."/>
I see log entries like:
... topProp=first second third ...
I'd really like to see only the most recently pushed value rather than all the values. I had hoped I could put something like the following in my appender/layout/ConversionPattern:
<param name="ConversionPattern value="... topProp=%properties{key}{1} ..."/>
but that doesn't work. I can kludge it by assuming/requiring all values to be the same length (say 5) and doing:
<param name="ConversionPattern value="... topProp=%5.5properties{key} ..."/>
But that isn't real attractive. Any ideas?
Thanks!
[Edit to add very simple example]
using System;
using System.IO;
using log4net;
using log4net.Config;
namespace ThreadLocalExample {
class Program {
private const string PropJobId = "Example:JobId";
static void Main() {
XmlConfigurator.Configure(new FileInfo("log4net.cfg"));
var log = LogManager.GetLogger(typeof(Program));
ThreadContext.Stacks[PropJobId].Push("Old");
log.Debug("Enter using");
using (ThreadContext.Stacks[PropJobId].Push("New")) {
log.Debug("stuff");
}
log.Debug("Out of using");
log.Debug("done.");
Console.ReadKey();
}
}
}
With the log4net configuration:
<appender name="Console" type="log4net.Appender.ConsoleAppender">
<threshold value="ALL" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="[jobId=%P{Example:JobId}]: %m%n" />
</layout>
</appender>
Produces:
[jobId=Old]: Enter using
[jobId=Old New]: stuff
[jobId=Old]: Out of using
[jobId=Old]: done.
But I'd like:
[jobId=Old]: Enter using
[jobId=New]: stuff
[jobId=Old]: Out of using
[jobId=Old]: done.
I had the same problem, and it was not only about 'bad formatting', because I used database appender (AdoNetAppender) which expects integers. So after joining all stacked values together the result is not an integer any more. Consider an appender like this:
<appender name="DbAppender" type="log4net.Appender.AdoNetAppender">
...
<commandText value="INSERT INTO Log ([Id]) VALUES (#Id)" />
<parameter>
<parameterName value="#Id" />
<dbType value="Int32" />
<layout type="log4net.Layout.PatternLayout" value="%P{someId}" />
</parameter>
This appender will not accept any log message, where 'someId' is stacked twice or more - no logs in database ...
So, to solve this problem I gave up with stacks and turned back to flat properties.
I've coded some short extension:
public static class Log4NetExt {
public static IDisposable ThreadContextPush(string key, object value) {
object oldVal = ThreadContext.Properties[key];
ThreadContext.Properties[key] = value;
var topMostCleaner = new DispCleaner();
topMostCleaner.EvDispose += () => {
// Pop = restore old value
ThreadContext.Properties[key] = oldVal;
};
return topMostCleaner;
}
private class DispCleaner : IDisposable {
public event Action EvDispose;
public void Dispose() {
if (EvDispose != null) EvDispose();
}
}
}
And now, instead of:
using (ThreadContext.Stacks[PropJobId].Push("New")) {
write:
using (Log4NetExt.ThreadContextPush(PropJobId, "New")) {
and it works ok ;)
(This short code does not follow all best practises of building disposable objects, deleting event handlers and all this stuff, but it is short and I think it is safe in this simple case).
I have Done it via Filip solution modified as
public static class Log4NetExt
{
public static IDisposable ThreadContextSet(string key, object value)
{
//object oldVal = ThreadContext.Properties[key];
ThreadContext.Properties[key] = value;
var topMostCleaner = new DispCleaner();
topMostCleaner.EvDispose += () => {
// Pop = restore old value
//ThreadContext.Properties[key] = oldVal;
ThreadContext.Properties[key] = null;
};
return topMostCleaner;
}
private class DispCleaner : IDisposable
{
public event Action EvDispose;
public void Dispose()
{
if (EvDispose != null)
{
EvDispose();
}
}
}
}
And used it like
using (Log4NetExt.ThreadContextSet("ContextId", "Feed"))
using (Log4NetExt.ThreadContextPush("ContextValue", objFeed.FeedId))
{
}

event is not generated in windows event

I am using log4net for logging, I have two appenders one file and other eventlog appender.
I have refered here this link
My problem is that the log is generated in file but not in windows event.
My code is:
<log4net>
<logger name="FileLogger">
<level value="ERROR" />
<appender-ref ref="RollingLogFileAppender" />
</logger>
<logger name="EventLogger" additivity="False">
<level value="ALL" />
<appender-ref ref="EventLogAppender" />
</logger>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs\logfile.xml"/>
<appendToFile value="true"/>
<rollingStyle value="Date"/>
<datePattern value="yyyyMMdd"/>
<maxSizeRollBackups value="10"/>
<maximumFileSize value="5MB"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="<Event><Date>%date</Date><Message>%message</Message><Stack>%exception</Stack></Event>%newline" />
</layout>
</appender>
<appender name="EventLogAppender" type="log4net.Appender.EventLogAppender">
<!--<param name="MvcApplication1" value="eventlog" />-->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender></log4net>
code side is:
public class LogManager
{
private static readonly ILog eventLogger = log4net.LogManager.GetLogger("EventLogAppender");
private static readonly ILog fileLogger = LogManager.GetLogger("FileLogger");
public void Error(Exception ex)
{
eventLogger.Logger.Log(eventLogger.GetType(), Level.Error, ex.Source, ex);
}
public void Info(string message)
{
LoggingEventData e = new LoggingEventData()
{
Level = Level.Fatal,
Message = message
};
eventLogger.Logger.Log(new LoggingEvent(e));
fileLogger.Logger.Log(new LoggingEvent(e));
}
}
I am using application_Error for global level error
protected void Application_Error(object sender, EventArgs e)
{
var httpContext = ((MvcApplication)sender).Context;
var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
var currentController = " ";
var currentAction = " ";
if (currentRouteData != null)
{
if (currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
{
currentController = currentRouteData.Values["controller"].ToString();
}
if (currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
{
currentAction = currentRouteData.Values["action"].ToString();
}
}
var ex = Server.GetLastError();
var controller = new ErrorController();
var routeData = new RouteData();
var action = "Index";
if (ex is HttpException)
{
var httpEx = ex as HttpException;
switch (httpEx.GetHttpCode())
{
case 404:
action = "NotFound";
break;
case 401:
action = " UnAuthorized";
break;
// others if any
default:
action = "Index";
break;
}
}
new LogManager().Error(ex);
httpContext.ClearError();
httpContext.Response.Clear();
httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
httpContext.Response.TrySkipIisCustomErrors = true;
routeData.Values["controller"] = "Error";
routeData.Values["action"] = action;
controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
Please help where I am wrong why error and Info is not logged in windows event
I think you have not referenced the EventLogger correctly in code:
ILog eventLogger = log4net.LogManager.GetLogger("EventLogAppender");
Should be changed to:
ILog eventLogger = log4net.LogManager.GetLogger("EventLogger");

How to use aspnet-session pattern layout?

I have adonet appender and I defined additional column. I want to get the userId from the asp.net session and do log.
According to this page there is %aspnet-session{key} pattern which I use like this:
<parameter>
<parameterName value="#userId" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%aspnet-session{current_member}" />
</layout>
</parameter>
and I got the following result in the database:
/LM/W3SVC/1/ROOT/trunk-1-129718741958458380spnet-session{current_member}
What I am doing wrong here?
As of log4net version 1.2.11.0, you can use %aspnet-request{ASP.NET_SessionId} of the ASP.NET pattern layout converters like so:
<conversionPattern value="%date %level %logger [%aspnet-request{ASP.NET_SessionId}] - %message%newline" />
See the release notes:
[LOG4NET-87] - Support ASP.Net related PatternConverters to allow items from the HttpContext.Current.Session, Cache, Request, etc. to be captured.
/LM/W3SVC/1/ROOT/trunk-1-129718741958458380spnet-session{current_member}
is getting logged because in your log4net definition you have something like this right?
<parameter>
<parameterName value="#user" />
<dbType value="String" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%aspnet-session{current_memeber}" />
</layout>
</parameter>
and log4net is taking "%a" out of "%aspnet-session{current_memeber}" and thinking you want the application domain. %a is the log4net pattern that converts to the application domain. This is really annoying and i recently ran into this and do not know a way around it.
See here:
https://logging.apache.org/log4net/log4net-1.2.13/release/sdk/log4net.Layout.PatternLayout.html
Found a solution to this problem. If you use log4net 1.2.11 or greater declaring your parameter like this should work.
<parameter>
<parameterName value="#session" />
<dbType value="String" />
<size value="2147483647"/>
<layout type="log4net.Layout.PatternLayout">
<converter>
<name value ="AspNetSessionPatternConverter"/>
<type value="log4net.Layout.Pattern.AspNetSessionPatternConverter"/>
</converter>
<conversionPattern value="%aspnet-session{gwsession}" />
</layout>
</parameter>
I don't know if %a being short circuited to appdomain is a bug or if you're supposed to declare your params like this with the aspnetsessionpatternconverter, but it would be nice if the log4net documentation was updated. Hope this helps someone else.
You need to use log4net 1.2.11 or higher to get access to the ASP.NET pattern converters.
You'll get the LM/W3SVC/1/ROOT/trunk-1-129718741958458380spnet-session{current_member} message when you use an older version of log4net.
I found a solution to my problem.
I just refactored it to serve my needs:
public class Log4NetAspNetProperty
{
private const string PropertyNamePrefix = "log4net_app_";
private const string PropertyDefaultValue = null;
private readonly string propertyName;
private readonly object propertyValue;
public string Name { get { return propertyName; } }
private Log4NetAspNetProperty(string propertyName, object propertyValue)
{
if (String.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullException("propertyName");
this.propertyName = propertyName;
this.propertyValue = propertyValue;
if (HttpContext.Current != null)
HttpContext.Current.Items[GetPrefixedPropertyName()] = propertyValue;
}
public override string ToString()
{
if (HttpContext.Current == null)
return PropertyDefaultValue;
var item = HttpContext.Current.Items[GetPrefixedPropertyName()];
return item != null ? item.ToString() : PropertyDefaultValue;
}
private static string GetPrefixedPropertyName()
{
return String.Format("{0}{1}", PropertyNamePrefix, PropertyDefaultValue);
}
public static void CurrentUserId(object userId)
{
var property = new Log4NetAspNetProperty("CurrentUserId", userId);
log4net.ThreadContext.Properties[property.Name] = property;
}
public static void CurrentUrl(object url)
{
var property = new Log4NetAspNetProperty("CurrentUrl", url);
log4net.ThreadContext.Properties[property.Name] = property;
}
}

log4net: Logging two messages in one row in the database?

I'm trying to log the input and output of a particular method to the database. I'd like to have this information in separate columns. I've investigated the PatternLayout and it seems that it only caters for a single %message parameter, meaning that if you do:
log.Debug("This is a message");
then log4net sees "This is a message" as the message to be logged. I want to do something like:
log.Debug(request, response);
Is this possible using log4net? Keep in mind that my goal is to have "request" and "response" in separate columns.
Your PatternConverter way is a step in the right direction, though the use of the static Input and Output properties makes it all a bit shaky (thread-safety wise).
The trick here is to realize that the message parameter on logger.Debug(...) is object and that you can pass in whatever you like.
You could define a custom message type
public class InputOutput
{
public string Input {get;set;}
public string Output {get;set;}
}
and then let your converters read either property
public class InputPatternConverter : PatternConverter
{
protected override void Convert(System.IO.TextWriter writer, object state)
{
var msg = ((LoggingEvent)state).MessageObject as InputOutput;
if (msg != null)
writer.Write(msg.Input);
}
}
public class OutputPatternConverter : PatternConverter
{
protected override void Convert(System.IO.TextWriter writer, object state)
{
var msg = ((LoggingEvent)state).MessageObject as InputOutput;
if (msg != null)
writer.Write(msg.Output);
}
}
the logging then becomes much cleaner
logger.Debug(new InputOutput { Input = ..., Output = ...});
your config would be the same.
A tip though is to subclass the PatternLayout and add the converters in the constructor of that class. That way you can also trim down your config. This will not cause you to loose the %message token, your %input and %output tokens will come in addition to all the tokens that PatternLayout supports. So you could actually have a pattern like this:
"%date %message %newline%newline %input %newline%newline %output
Here's a quick implementation of a custom pattern layout:
public class InputOutputPatternLayout : PatternLayout
{
public InputOutputPatternLayout()
{
AddConverter("input", typeof(InputPatternConverter));
AddConverter("output", typeof(OutputPatternConverter));
}
}
I've come up with one way to do this using custom PatternConverters
public class InputPatternConverter : PatternConverter
{
private static string _input;
public static string Input
{
get { return _input; }
set { _input = value; }
}
protected override void Convert(System.IO.TextWriter writer, object state)
{
writer.Write(Input);
}
}
public class OutputPatternConverter : PatternConverter
{
private static string _output;
public static string Output
{
get { return _output; }
set { _output = value; }
}
protected override void Convert(System.IO.TextWriter writer, object state)
{
writer.Write(Output);
}
}
Appender Specification:
<appender name="ADONetAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="1" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="data source=servername;initial catalog=database;Integrated Security=SSPI;" />
<commandText value="INSERT INTO RequestLog ([input], [output]) VALUES (#input, #output)" />
<parameter>
<parameterName value="#input" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout">
<converter>
<name value="input" />
<type value="InputPatternConverter, ApplicationName" />
</converter>
<conversionPattern value="%input" />
</layout>
</parameter>
<parameter>
<parameterName value="#output" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout">
<converter>
<name value="output" />
<type value="OutputPatternConverter, ApplicationName" />
</converter>
<conversionPattern value="%output" />
</layout>
</parameter>
</appender>
Call it using:
InputPatternConverter.Input = inputString;
OutputPatternConverter.Output = outputString;
XmlConfigurator.Configure();
ILog logger = LogManager.GetLogger(typeof(ApplicationClassName));
logger.Debug("");

Resources