76
votes

I am totally new to Log4net. I have managed to get something going by adding a config file and simple logging. I have hardcoded the value to be "C:\temp\log.txt" but this is not good enough.

The logs must go to the special folders

path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);

and this path changes depending whether you are using Windows Server 2008 or Windows XP or Vista etc...

How can I just change the location of the file in log4net programmatically?

This is what I have done:

<configSections>
<section name="log4net"
         type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net"/>
</configSections>
<log4net>         
    <root>
        <level value="DEBUG" />
        <appender-ref ref="LogFileAppender" />
    </root>
    <appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
        <param name="File" value="C:\temp\log.txt" />
        <param name="AppendToFile" value="true" />
        <rollingStyle value="Size" />
        <maxSizeRollBackups value="10" />
        <maximumFileSize value="10MB" />
        <staticLogFileName value="true" />
        <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%-5p%d{yyyy-MM-dd hh:mm:ss} – %m%n" />
        </layout>
    </appender>
</log4net>

class Program
{
    protected static readonly ILog log = LogManager.GetLogger(typeof(Program));

    static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();
        log.Warn("Log something");

        path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);


        // How can I change where I log stuff?
    }
}

Just need to figure out how I can change to log stuff to where I want to.

Any suggestions? Thanks a lot

11
You need to dig through your logger's IAppenders and set the FileAppender.File to your required output path. Here's a really good example.Cam Soper

11 Answers

89
votes

log4net can handle this for you. Any appender property of type string can be formatted, in this case, using the log4net.Util.PatternString option handler. PatternString even supports the SpecialFolder enum which enables the following elegant config:

<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender" >
    <file type="log4net.Util.PatternString" 
        value="%envFolderPath{CommonApplicationData}\\test.txt" />
    ...
</appender>

Here's a unit test that proofs the pudding:

[Test]
public void Load()
{
    XmlConfigurator.Configure();
    var fileAppender = LogManager.GetRepository()
        .GetAppenders().First(appender => appender is RollingFileAppender);

    var expectedFile = 
        Path.Combine(
            Environment.GetFolderPath(
                Environment.SpecialFolder.CommonApplicationData),
                "test.txt");

    Assert.That(fileAppender, 
        Is.Not.Null & Has.Property("File").EqualTo(expectedFile));
}

The following test verifies that log4net actually writes to disk (which basically makes this an "integration" test, not a unit test, but we'll leave it at that for now):

[Test]
public void Log4net_WritesToDisk()
{
    var expectedFile = 
        Path.Combine(
            Environment.GetFolderPath(
                Environment.SpecialFolder.CommonApplicationData),
                "test.txt");

    if (File.Exists(expectedFile))
        File.Delete(expectedFile);

    XmlConfigurator.Configure();

    var log = LogManager.GetLogger(typeof (ConfigTest));
    log.Info("Message from test");

    LogManager.Shutdown();

    Assert.That(File.ReadAllText(expectedFile), 
        Text.Contains("Message from test"));
}

NB: I strongly suggest using the compact property syntax demonstrated in the above sample. Removing all those "<property name=" makes your config that much more readable.

48
votes

I found a mutation of this code in the interwebs:

XmlConfigurator.Configure();
log4net.Repository.Hierarchy.Hierarchy h =
(log4net.Repository.Hierarchy.Hierarchy) LogManager.GetRepository();
foreach (IAppender a in h.Root.Appenders)
{
    if (a is FileAppender)
    {
        FileAppender fa = (FileAppender)a;
        // Programmatically set this to the desired location here
        string logFileLocation = @"C:\MySpecialFolder\MyFile.log";

        // Uncomment the lines below if you want to retain the base file name
        // and change the folder name...
        //FileInfo fileInfo = new FileInfo(fa.File);
        //logFileLocation = string.Format(@"C:\MySpecialFolder\{0}", fileInfo.Name);

        fa.File = logFileLocation;
        fa.ActivateOptions();
        break;
    }
}

This works for me. Our application needs to put the log file in a folder that contains the version number of the app based on the AssemblyInfo.cs file.

You should be able to set the logFileLocation programmatically (e.g. you can use Server.MapPath() if this is a web application) to suit your needs.

16
votes

Looks like Peter's answer doesn't work for Log4net v1.2.10.0. An alternative method is described here.

Basically the method is to implement a custom pattern converter for the log4net config file.

First add this class to your project:

public class SpecialFolderPatternConverter : log4net.Util.PatternConverter
{
    override protected void Convert(System.IO.TextWriter writer, object state)
    {
        Environment.SpecialFolder specialFolder = (Environment.SpecialFolder)Enum.Parse(typeof(Environment.SpecialFolder), base.Option, true);
        writer.Write(Environment.GetFolderPath(specialFolder));
    }
}

Then set up the File parameter of your FileAppender as follows:

<file type="log4net.Util.PatternString">
    <converter>
      <name value="folder" />
      <type value="MyAppName.SpecialFolderPatternConverter,MyAppName" />
    </converter>
    <conversionPattern value="%folder{CommonApplicationData}\\SomeOtherFolder\\log.txt" />
  </file>

Basically the %folder tells it to look at the converter called folder which points it to the SpecialFolderPatternConverter class. It then calls Convert on that class, passing in the CommonApplicationData (or whatever) enum value.

7
votes

How about a simple:

XmlConfigurator.LogFullFilename = @"c:\ProgramData\MyApp\Myapp.log";

Why is it so complex to do a really simple thing?

5
votes

This worked for me:

  <log4net>
    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
..
      <file value="${APPDATA}\MyApp\MyApp Client\logs\Log.txt"/>
..
  </log4net>

If needing to write to special folders I found help here (2nd and 3rd example).

Edit:

To answer OP.. This works for 'all users' area:

      ...
      <file value="${ALLUSERSPROFILE}\MyApp\MyApp Client\logs\Log.txt"/>
      ...

Which is normally "C:\ProgramData" in newer versions of Windows.

See these too:
How to specify common application data folder for log4net? == https://stackoverflow.com/a/1889591/503621 and comments
&
https://superuser.com/q/405097/47628
https://stackoverflow.com/a/5550502/503621

4
votes

To also change the error log's path (based on JackAce's answer):

private static void SetLogPath(string path, string errorPath)
{
    XmlConfigurator.Configure();
    log4net.Repository.Hierarchy.Hierarchy h =
    (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository();
    foreach (var a in h.Root.Appenders)
    {
        if (a is log4net.Appender.FileAppender)
        {
            if (a.Name.Equals("LogFileAppender"))
            { 
                log4net.Appender.FileAppender fa = (log4net.Appender.FileAppender)a;                    
                string logFileLocation = path; 
                fa.File = logFileLocation;                   
                fa.ActivateOptions();
            }
            else if (a.Name.Equals("ErrorFileAppender"))
            {
                log4net.Appender.FileAppender fa = (log4net.Appender.FileAppender)a;
                string logFileLocation = errorPath;
                fa.File = logFileLocation;
                fa.ActivateOptions();
            }
        }
    }
}
3
votes

JackAce's answer, just more concise using Linq:

C#

XmlConfigurator.Configure();
var appender = (LogManager.GetRepository() as Hierarchy).Root.Appenders
    .OfType<FileAppender>()
    .First();

appender.File = logPath;
appender.ActivateOptions();

VB.NET

XmlConfigurator.Configure()
Dim appender = CType(LogManager.GetRepository(), Hierarchy).Root.Appenders _
    .OfType(FileAppender)() _
    .First()

appender.File = logPath
appender.ActivateOptions()
2
votes

Great use case for LINQs OfType<T> filter:

/// <summary>
/// Applies a transformation to the filenames of all FileAppenders.
/// </summary>
public static void ChangeLogFile(Func<string,string> transformPath)
{
    // iterate over all FileAppenders
    foreach (var fileAppender in LogManager.GetRepository().GetAppenders().OfType<FileAppender>())
    {
        // apply transformation to the filename
        fileAppender.File = transformPath(fileAppender.File);
        // notify the logging subsystem of the configuration change
        fileAppender.ActivateOptions();
    }
}

If the filename in the app.config is log.txt this will change the log output to logs/some_name_log.txt:

ChangeLogFile(path => Path.Combine("logs", $"some_name_{Path.GetFileName(path)}"));

To Answer the OPs original problem that would be:

ChangeLogFile(path => Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), path));
1
votes

As an alternative to doing this programatically, you can use environment variables and customizable patterns in the configuration file. See this response to a similar question.

Look at "PatternString for pattern based configuration" in the Log4Net V1.2.10 release notes.

Also if you are thinking of writing to a directory such as Enviroment.SpecialFolder.CommonApplicationData you need to consider:

  • Will all instances of your application for all users have write access to the log file? E.g. I don't believe non-administrators will be able to write to Enviroment.SpecialFolder.CommonApplicationData.

  • Contention if multiple instances of your application (for the same or different users) are attempting to the same file. You can use the "minimal locking model" (see http://logging.apache.org/log4net/release/config-examples.html to allow multiple processes to write to the same log file, but there probably will be a performance impact. Or you could give each process a different log file, e.g. by including the process id in the filename using a customizable pattern.

1
votes

In the current version of Log4Net (2.0.8.0) you could simply use <file value="${ProgramData}\myFolder\LogFiles\" /> for C:\ProgramData\.. and ${LocalAppData} for C:\Users\user\AppData\Local\

0
votes

If you have to deploy to unknown systems and you want to use the simple solution by Philipp M even with different special folders you can retrieve the special folder path you want and set a custom env variable before loading the log4net configuration. string localData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); Environment.SetEnvironmentVariable("MY_FOLDER_DATA", localData); XmlConfigurator.Configure( ...

... just to be sure the env variable exists and has the value you want.