I'm building a web UI to help automate our deployment process and am going to write a powershell script to do the deployment and would like it's Write-Debug (or any statement to log, just let me know which to use :) ) statements to be logged to the deployed package's database variable Log. I haven't really used log4net before so please don't laugh if I'm doing this completely wrong.
I figure since the location is dynamic, I'd have to code the log4net appenders, but would it be easier/better to do all of the log4net stuff inside of the powershell script? I read this and found I should use ps.Streams.Debug.DataAdded += new EventHandler<DataAddedEventArgs>(delegate(object sender, DataAddedEventArgs e) to get the write-debug information.
Here is what I have so far:
public static void Test(Package pkg)
{
//Do roll_out
//Creates a cmd prompt
PowerShell ps = PowerShell.Create();
string myCommand = #"C:\Users\evan.layman\Desktop\test.ps1";
ps.AddCommand(myCommand);
ps.Streams.Debug.DataAdded += new EventHandler<DataAddedEventArgs>(delegate(object sender, DataAddedEventArgs e)
{
PSDataCollection<DebugRecord> debugStream = (PSDataCollection<DebugRecord>)sender;
DebugRecord record = debugStream[e.Index];
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
hierarchy.Root.RemoveAllAppenders(); /*Remove any other appenders*/
AdoNetAppender appender = new AdoNetAppender();
appender.ConnectionString = ConfigurationManager.ConnectionStrings["DeploymentConnectionString"].ConnectionString;
appender.CommandText = "with cte as (SELECT * FROM Package PackageID =" + pkg.PackageID + ") UPDATE cte SET (Log) VALUES (?logText)";
AdoNetAppenderParameter param = new AdoNetAppenderParameter();
param.DbType = System.Data.DbType.String;
param.ParameterName = "logText";
param.Layout = new log4net.Layout.RawTimeStampLayout();
appender.AddParameter(param);
BasicConfigurator.Configure(appender);
ILog log = LogManager.GetLogger("PowerShell");
log.Debug(record.Message);
//log.DebugFormat("{0}:{1}", DateTime.UtcNow, record);
//log.Warn(record, new Exception("Log failed"));
});
Collection<PSObject> commandResults = ps.Invoke();
Hopefully I can get this working :)
I would keep as much log4net config out of your code as possible. In your code, the config is being recreated on each debug statement, which is inefficient.
It's possible to do what you want using event context properties in log4net. I've blogged about log4net event context a bit on my blog.
Here's a quick example that's close to your existing codebase....
This C# code shows how to use log4net global properties to store custom event context data; note the setting of the "PackageID" global property value before the pipeline is executed...
using System;
using System.Management.Automation;
using log4net;
// load log4net configuration from app.config
[assembly:log4net.Config.XmlConfigurator]
namespace ConsoleApplication1
{
class Program
{
private static PowerShell _ps;
private static ILog Log = log4net.LogManager.GetLogger(typeof (Program));
static void Main(string[] args)
{
string script = "write-debug 'this is a debug string' -debug";
for (int packageId = 1; packageId <= 5; ++packageId)
{
using (_ps = PowerShell.Create())
{
_ps.Commands.AddScript(script);
_ps.Streams.Debug.DataAdded += WriteDebugLog;
// set the PackageID global log4net property
log4net.GlobalContext.Properties["PackageID"] = packageId;
// sync invoke your pipeline
_ps.Invoke();
// clear the PackageID global log4net property
log4net.GlobalContext.Properties["PackageID"] = null;
}
}
}
private static void WriteDebugLog(object sender, DataAddedEventArgs e)
{
// get the debug record and log the message
var record = _ps.Streams.Debug[e.Index];
Log.Debug(record.Message);
}
}
}
And here is the app.config that drops the logs into the database; note the custom PackageID parameter in the SQL, and how the value is pulled from the log4net property stack:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<log4net>
<appender name="Ado" type="log4net.Appender.AdoNetAppender">
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="data source=vbox-xp-sql;initial catalog=test1;integrated security=false;persist security info=True;User ID=test1;Password=password" />
<commandText value="INSERT INTO Log ([Message],[PackageID]) VALUES (#message, #packageid)" />
<parameter>
<parameterName value="#message" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout" value="%message" />
</parameter>
<parameter>
<parameterName value="#packageid" />
<dbType value="Int32" />
<size value="4" />
<!-- use the current value of the PackageID property -->
<layout type="log4net.Layout.PatternLayout" value="%property{PackageID}" />
</parameter>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="Ado" />
</root>
</log4net>
</configuration>
Hope this helps.
Related
In App.Config I declare a config section for my console application:
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
And configure a ConsoleAppender:
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
We also declare a RollingFile appender in a logging util class that's part of our standard codebase at app-startup (which is not easily modified):
_hierarchy = (Hierarchy)LogManager.GetRepository();
_rollingFileAppender = new RollingFileAppender();
... //_rollingFileppender options
_rollingFileAppender.ActivateOptions();
_hierarchy.Root.AddAppender(_rollingFileAppender);
_hierarchy.Configured = true;
When I run the application, I see log-entries are written to files as expected but nothing is written to the console.
When I inspect my ILog instance in the debugger, I see no appenders at all in Logger.Appenders but I do see the RollingFileAppender instance as the only element in Root.Appenders.
log4net is something I am used to "it just works" - is there some incompatibility having config and code add appenders, or some line I need to get LogManager to look in app.config?
You just got me curious and i decided to play around a bit.
First observations is: You are right.
After some google i found this this
App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %level [%thread] %logger{1} %username - %message%newline" />
</layout>
<mapping>
<level value="WARN" />
<foreColor value="Yellow, HighIntensity" />
</mapping>
<mapping>
<level value="ERROR" />
<foreColor value="Red, HighIntensity" />
</mapping>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
Setup
using log4net;
using log4net.Appender;
using log4net.Layout;
using log4net.Repository.Hierarchy;
namespace LoggerDemo
{
public static class Program
{
private static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public static void Main(string[] args)
{
SetupFileAppender();
SetupTraceAppender();
Logger.Error("Error");
}
private static void SetupTraceAppender()
{
var patternLayout = new PatternLayout();
patternLayout.ConversionPattern = "%date [%thread] %-5level %logger - %message%newline";
patternLayout.ActivateOptions();
var trace = new TraceAppender();
trace.Layout = patternLayout;
trace.ActivateOptions();
var l = (Logger)Logger.Logger;
l.AddAppender(trace);
}
private static void SetupFileAppender()
{
var patternLayout = new PatternLayout();
patternLayout.ConversionPattern = "%date [%thread] %-5level %logger - %message%newline";
patternLayout.ActivateOptions();
var roller = new RollingFileAppender();
roller.AppendToFile = true;
roller.File = "Log.txt";
roller.Layout = patternLayout;
roller.MaxSizeRollBackups = 5;
roller.RollingStyle = RollingFileAppender.RollingMode.Size;
roller.StaticLogFileName = true;
roller.ActivateOptions();
var l = (Logger)Logger.Logger;
l.AddAppender(roller);
}
}
}
Most Important
In your AssemblyInfo add [assembly: log4net.Config.XmlConfigurator(Watch = true)]
Result
I looked in google about log4net and seems interesting. I looked into some website and
for the output here should be something like log4net.config i guess? The guy showed to press it and in the Properties here should be "Copy to Output Directory": "Copy Always".
Maybe you can somehow figure to use it and print into the console the output. Here is really useful information in google about it you can click Here to read more.
Hope i somehow helped you :)
Is there a way to make the name of the fileAppender variable?
I.e. when I call an action on my controller which takes an object, I would like to write this to a log file.
The name of the file would look something like :
yyyyMMdd_hhmssms_[controller]_[method].json
this is what I have in my config-file:
<appender name="JsonFileAppender" type="log4net.Appender.RollingFileAppender" >
<file value="c:\temp\" />
<datePattern value="yyyyMMdd_hh.mm.ss.ms_%thread{CommonApplicationData}'.json'" />
<staticLogFileName value="false" />
<appendToFile value="true" />
<rollingStyle value="Composite" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="5MB" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%message%newline" />
</layout>
</appender>
This returns the following filename : 20160224_01.30.28.3028_P1rea24{Co30onApplicaPionDaPa}.json
one way is to set an Environment Variable in your code like:
Environment.SetEnvironmentVariable("APPENDER_FILE", "Your File Path");
and then, configure this environment variable in log4net XML:
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="${APPENDER_FILE}"/>
You can access the appenders of your log4net configuration at run-time like so
var repository = (Hierarchy)LogManager.GetRepository();
var appenders = repository.GetAppenders().Where(x => x is FileAppender);
You can get specific appender then by name
var appender = appenders.FirstOrDefault(x => x.Name.Equals("MyAppeader"));
Once you have an appender you can modify it how you like. You want to set the filepath
appender.File = #"c:\folder\yyyyMMdd_hhmssms_[controller]_[method].json";
You should not have the do anything else as log4net should automatically start using the new configuration.
Placing this all into a little helper method, you'd get this
public static void SetAppenderPath(string appender, string path)
{
var repository = (Hierarchy)LogManager.GetRepository();
var appenders = repository.GetAppenders().Where(x => x is FileAppender);
var appender = appenders.FirstOfDefault(x => x.Name.Equals(appender));
if (appender == null)
{
throw new ConfigurationErrorsException("Appender not found (" + appender + ")");
}
appender.File = path;
}
...
LogHelper.SetAppenderPath("MyAppender", #"yyyyMMdd_hhmssms_[controller]_[method].json");
I have database logging in place using the AdoNetAppender. What I'd like to do is log the user identity on each log statement. However, I don't want to use the standard log4net %identity parameter for two reasons:
log4net warn that its extremely slow as it has to look up the context identity.
In some service components the standard identity is a service account but we have already captured the user identity in a variable and I'd like to use that.
I have seen code where some people use the log4net.ThreadContext to add additional properties but I understand that this is 'unsafe' due to thread interleaving (and it is also a performance drain).
My approach has been to extend the AdoNetAppenderParameter class thus:
public class UserAdoNetAppenderParameter : AdoNetAppenderParameter
{
public UserAdoNetAppenderParameter()
{
DbType = DbType.String;
PatternLayout layout = new PatternLayout();
Layout2RawLayoutAdapter converter = new Layout2RawLayoutAdapter(layout);
Layout = converter;
ParameterName = "#username";
Size = 255;
}
public override void Prepare(IDbCommand command)
{
command.Parameters.Add(this);
}
public override void FormatValue(IDbCommand command, LoggingEvent loggingEvent)
{
string[] data = loggingEvent.RenderedMessage.Split('~');
string username = data[0];
command.Parameters["#username"] = username;
}
}
and then programmatically add this to the current appender like so:
ILog myLog = LogManager.GetLogger("ConnectionService");
IAppender[] appenders = myLog.Logger.Repository.GetAppenders();
AdoNetAppender appender = (AdoNetAppender)appenders[0];
appender.AddParameter(new UserAdoNetAppenderParameter());
myLog.InfoFormat("{0}~{1}~{2}~{3}", userName, "ClassName", "Class Method", "Message");
The intention here is to use a standard format for messages and parse the first part of the string which should always be the username. The FormatValue() method of the custom appender parameter should then use only that part of the string so that it can be written to a separate field in the log database.
My problem is that no log statements are written to the database. Oddly, when debugging, a breakpoint in the FormatValue() method is only hit when I stop the service.
I've trawled through loads of stuff relating to this but haven't yet found any answers.
Has anyone managed to do this, or am I on the wrong trail.
P.S. I've also tried extending the AdoNetAppender but it doesnt give you access to set the parameter values.
I also needed to log structured data and liked to use logging interface like this:
log.Debug( new {
SomeProperty: "some value",
OtherProperty: 123
})
So I also wrote custom AdoNetAppenderParameter class to do the job:
public class CustomAdoNetAppenderParameter : AdoNetAppenderParameter
{
public override void FormatValue(IDbCommand command, LoggingEvent loggingEvent)
{
// Try to get property value
object propertyValue = null;
var propertyName = ParameterName.Replace("#", "");
var messageObject = loggingEvent.MessageObject;
if (messageObject != null)
{
var property = messageObject.GetType().GetProperty(propertyName);
if (property != null)
{
propertyValue = property.GetValue(messageObject, null);
}
}
// Insert property value (or db null) into parameter
var dataParameter = (IDbDataParameter)command.Parameters[ParameterName];
dataParameter.Value = propertyValue ?? DBNull.Value;
}
}
Now log4net configuration can be used to log any property of given object:
<?xml version="1.0" encoding="utf-8"?>
<log4net>
<appender name="MyAdoNetAppender" type="log4net.Appender.AdoNetAppender">
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="... your connection string ..." />
<commandText value="INSERT INTO mylog ([level],[someProperty]) VALUES (#log_level,#SomeProperty)" />
<parameter>
<parameterName value="#log_level" />
<dbType value="String" />
<size value="50" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%level" />
</layout>
</parameter>
<parameter type="yourNamespace.CustomAdoNetAppenderParameter, yourAssemblyName">
<parameterName value="#SomeProperty" />
<dbType value="String" />
<size value="255" />
</parameter>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="MyAdoNetAppender" />
</root>
</log4net>
After some experimentation, I finally got this to work. Ensuring that log4net's internal logging helped identify the errors and downloading the log4net source code and reviewing the AdoNetAppenderParameter class showed how the FormatValue() method should be used. So, here's the amended custom appender parameter:
public class UserAdoNetAppenderParameter : AdoNetAppenderParameter
{
public override void FormatValue(IDbCommand command, LoggingEvent loggingEvent)
{
string[] data = loggingEvent.RenderedMessage.Split('~');
string username = string.Empty;
if (data != null && data.Length >= 1)
username = data[0];
// Lookup the parameter
IDbDataParameter param = (IDbDataParameter)command.Parameters[ParameterName];
// Format the value
object formattedValue = username;
// If the value is null then convert to a DBNull
if (formattedValue == null)
{
formattedValue = DBNull.Value;
}
param.Value = formattedValue;
}
}
And to use this, I add it in the log4net config file like this:
<parameter type="MyAssembly.Logging.UserAdoNetAppenderParameter, MyAssembly">
<parameterName value="#username" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout" value="%message" />
</parameter>
And by convention, my log statements will be something like this:
if (log.IsDebugEnabled)
log.DebugFormat("{0}~{1}~{2}", username, someOtherParameter, message);
If you look at the class, it uses data[0] as the username, so it is dependant on following the convention. It does, however, get the username into its own parameter and into a separate field in the log database table, without resorting to stuffing it temporarily into the unsafe ThreadContext.
Yes, thread agility means that you might not get the correct data back. For log4net, you will want to stick it in your HttpContext's Items collection.
The trouble is you have to do some work to get it back out when it's time to write those values to the database because of this I have always used Marek's Adaptive Property Provider class to do the grunt work for me. It's super simple to use it as all you have to do is the following:
log4net.ThreadContext.Properties["UserName"] = AdaptivePropertyProvider.Create("UserName", Thread.CurrentPrincipal.Identity.Name);
The adaptive property will know the appropriate place to retrieve the value when log4net requests it.
Alternative Option
If you're not stuck with log4net, NLog makes logging for ASP.NET websites way more simple because they natively support ASP.NET applications. Usage and even configuration is almost identical to log4net!
When I have following xml configuration to configure log4net for remotingappender, it all works.
<log4net>
<appender name="RemotingAppender" type="log4net.Appender.RemotingAppender" >
<sink value="tcp://localhost:8085/LoggingSink" />
<lossy value="false" />
<bufferSize value="1" />
<onlyFixPartialEventData value="true" />
</appender>
<root>
<level value="ALL" />
<appender-ref ref="RemotingAppender" />
</root>
</log4net>
I want to do the same thing in code. I searched a littlebit and find an example like the following. But i could not get it working.
ILog log = log4net.LogManager.GetLogger("logName");
Repository.Hierarchy.Logger l = (Repository.Hierarchy.Logger)log.Logger;
// set level
l.Level = l.Hierarchy.LevelMap["ALL"];
// create appander
Appender.RemotingAppender remotingAppender = new Appender.RemotingAppender();
remotingAppender.Name = "custom";
remotingAppender.Sink = "tcp://localhost:8085/LoggingSink";
remotingAppender.Lossy = false;
remotingAppender.BufferSize = 1;
//remotingAppender.Fix = log4net.Core.FixFlags.All;
// create pattern
log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout();
layout.ConversionPattern = "%d [%thread] %-5p %c [%a] - %m [%line] [%M]%n";
layout.ActivateOptions();
remotingAppender.Layout = layout;
remotingAppender.ActivateOptions();
// add appender
l.AddAppender(remotingAppender);
// perform logging (doesnt work)
log.Warn("my warning");
log.Error("my error");
What is the missing side in my code?
try one of log4net.Config.BasicConfigurator.Configure(...) oveloads.
I finally get it working. i firstly load and xml configuration which is almost emptpy. Then i put the pattern layout which has same pattern string as remoting listener. My modified xml file is below:
<log4net>
<root>
<level value="ALL" />
</root>
</log4net>
And i load this configuration at the beginning of my application (program.c)
log4net.Config.XmlConfigurator.Configure(file)
I'm writing an application where the user can change (at runtime) the directory where the log4net log is stored. The directory string is stored in the app.config.
When I want to test if the log file is created in the right directory with NUnit, the logfile (and the corresponding directory) is not created.
When looking online for this problem I read that NUnit stops the logging from working because it uses log4net itself. The provided sample tells you to create a additional .config (Test.config) which also contains the log4net sections and to load the configuration inside the testing class, which I did.
There is still no log file created when using the unit test.
When starting the application, the log file is created as it should.
Method to set the log directory:
public void MyMethod()
{
string logDirectory = app.Settings["LogDirectory"].Value;
//assure that the file name can be appended to the path
if (!logDirectory.EndsWith(#"\"))
{
logDirectory += #"\";
}
//find the rolling file appender and set its file name
XmlConfigurator.Configure();
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
foreach (IAppender appender in hierarchy.Root.Appenders)
{
if (appender is RollingFileAppender)
{
RollingFileAppender fileAppender = (RollingFileAppender)appender;
string logFileLocation = string.Format("{0}Estimation_Protocol_{1}.txt",
logDirectory, EstimatorHelper.SoftwareVersionAndDateTime());
fileAppender.File = logFileLocation;
fileAppender.ActivateOptions();
break;
}
}
log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
log.Debug("Logging directory & file name set.");
}
The test class:
class EstimatorTests
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public EstimatorTests()
{
FileInfo fileInfo = new FileInfo(#"%property{LogName}");
log4net.Config.XmlConfigurator.Configure(fileInfo);
}
[Test]
public void TestLoadInputPaths()
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
AppSettingsSection app = config.AppSettings;
string time = DateTime.Now.Ticks.ToString();
app.Settings["ProcessingProtocolDirectory"].Value = "C:\\thisFolderDoesntExist" + time;
Form mainForm = new Form();
Form.MyMethod();
Assert.IsTrue(Directory.Exists("C:\\thisFolderDoesntExist" + time));
//this assert fails!
}
}
The log4net config:
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file type="log4net.Util.PatternString" value="%property{LogName}" />
<appendToFile value="true"/>
<rollingStyle value="Size"/>
<maxSizeRollBackups value="5"/>
<maximumFileSize value="10MB"/>
<staticLogFileName value="true"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %level %logger - %message%newline%exception%newline"/>
</layout>
</appender>
<root>
<level value="DEBUG"/>
<appender-ref ref="RollingFileAppender"/>
</root>
</log4net>
I did not test it but I think you need to remove the call to XmlConfigurator.Configure(); in MyMethod because this will overwrite the configuration you do in the test class.