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("");
Related
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!
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))
{
}
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");
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;
}
}
I am struggling to write an AND conditional filter in log4net. Had it been nLog, I could have written it this way:
<logger name="*" minlevel="Info" xsi:type="NLogLoggerRule" writeTo="FooLogger" >
<filters>
<when condition="equals('${event-context:item=UserID}', 'TESTUSER')
and equals('${event-context:item=URL}','/foo/foobar.aspx')"
action="Ignore" />
</filters>
</logger>
I am not sure how to write the same filter in log4net. I have been so far successful, in writing a single condition:
<appender>
....
<filter type="log4net.Filter.PropertyFilter">
<key value="URL" />
<stringToMatch value="/foo/foobar.aspx" />
<acceptOnMatch value="false" />
</filter>
</appender>
How can I write AND conditions using log4net filters? Please help.
A custom filter supporting AND conditions. This class exposes Filter property so existing log4net filters can be used here and also one can have nested AND conditions if required.
public class AndFilter : FilterSkeleton
{
private bool acceptOnMatch;
private readonly IList<IFilter> filters = new List<IFilter>();
public override FilterDecision Decide(LoggingEvent loggingEvent)
{
if (loggingEvent == null)
throw new ArgumentNullException("loggingEvent");
foreach(IFilter filter in filters)
{
if (filter.Decide(loggingEvent) != FilterDecision.Accept)
return FilterDecision.Neutral; // one of the filter has failed
}
// All conditions are true
if(acceptOnMatch)
return FilterDecision.Accept;
else
return FilterDecision.Deny;
}
public IFilter Filter
{
set { filters.Add(value); }
}
public bool AcceptOnMatch
{
get { return acceptOnMatch;}
set { acceptOnMatch = value;}
}
}
Config:
<filter type="Namespace.AndFilter, Assembly">
<filter type="log4net.Filter.PropertyFilter">
<key value="URL" />
<stringToMatch value="/foo/foobar.aspx" />
</filter>
<filter type="log4net.Filter.PropertyFilter">
<key value="UserID" />
<stringToMatch value="TESTUSER" />
</filter>
<acceptOnMatch value="false" />
</filter>
You can create custom filter for your business needs:
public class UserRequestFilter : FilterSkeleton
{
public override FilterDecision Decide(LoggingEvent loggingEvent)
{
if (loggingEvent == null)
throw new ArgumentNullException("loggingEvent");
string userId = (string)loggingEvent.Properties["UserId"];
string url = (string)loggingEvent.Properties["Url"];
if (String.IsNullOrEmpty(UserId) || String.IsNullOrEmpty(Url))
return FilterDecision.Neutral;
if (UserId.Equals(userId) && Url.Equals(url, StringComparison.CurrentCultureIgnoreCase))
return AcceptOnMatch ? FilterDecision.Accept : FilterDecision.Deny;
return FilterDecision.Neutral;
}
public bool AcceptOnMatch { get; set; }
public string UserId { get; set; }
public string Url { get; set; }
}
Configuration will look like this:
<filter type="Namespace.UserRequestFilter, Assembly">
<userId value="TESTUSER"/>
<url value="/foo/foobar.aspx"/>
<acceptOnMatch value="true"/>
</filter>
Also you can create compound filter, but I didn't find way to use it in configuration. Looks like it could be attached only programmatically (which is useless ^_^):
IAppenderAttachable logger = (IAppenderAttachable)_log.Logger;
AppenderSkeleton appender = (AppenderSkeleton)logger.GetAppender("appenderName");
CompoundFilter compoundFilter = new CompoundFilter();
compoundFilter.AddFilter(new PropertyFilter() { Key = "UserId", StringToMatch = "TEST" });
compoundFilter.AddFilter(new PropertyFilter() { Key = "Url", StringToMatch = #"/foo/foobar.aspx" });
appender.AddFilter(compoundFilter);
logger.AddAppender(appender);
[UPDATE]
Here is trick that you can use - create filter, which will check all filters down the filters chain:
public class DenyAllSubsequentFilter : FilterSkeleton
{
public override FilterDecision Decide(LoggingEvent loggingEvent)
{
IFilter nextFilter = Next;
if (nextFilter == null)
return FilterDecision.Accept;
while (nextFilter != null)
{
if (nextFilter.Decide(loggingEvent) != FilterDecision.Deny)
return FilterDecision.Accept;
nextFilter = nextFilter.Next;
}
return FilterDecision.Deny;
}
}
Usage:
<filter type="Namespace.DenyAllSubsequentFilter, Assembly"/>
<filter type="log4net.Filter.PropertyFilter">
<key value="UserId" />
<stringToMatch value="TEST" />
<acceptOnMatch value="false" />
</filter>
<filter type="log4net.Filter.PropertyFilter">
<key value="Url" />
<stringToMatch value="/foo/foobar.aspx" />
<acceptOnMatch value="false" />
</filter>
This filter will deny logging message if all subsequent filters will deny it.