Click here to Skip to main content
Click here to Skip to main content

Mini Drop-in Replacement for log4net

By , 4 May 2012
 

Introduction 

This is a simple 8KB drop in replacement for complex 198KB Log4net that’s a 96% size reduction. Strange that after looking on the web for a logging solution that fitted my requirements, I couldn't find one.

Background

I have been using log4net for the past 7 years and it is undoubtedly the de-facto standard for logging in the .NET Framework environment. I am also a follower of the minimalistic philosophy, and to this end, I have created a drop in replacement for log4net which is smaller and a lot less complex.

This was done mostly for the reason of size restrictions in a project, i.e., minimizing the deployment footprint of the app and also as a review exercise.

Obviously, it does not have the feature set of the original, but 99% of the time you don't need it in my experience.

What You Get

So what this mini log4net does for you is the following:

  • 200 lines of code
  • Drop-in replace log4net
  • Threaded logger: no blocking of main code to log messages
  • Write to a text files only (you can add other destinations yourself if you need it)
  • Size limit log files and roll the file names with a count number
  • Date roll log files: new file for each new day
  • Simple single method configuration: no XML configuration files
  • Ability to log method name: *performance hit

Using the Code

You can pretty much forget about any changes to your code because it will work as is.

public class someclass
{
    log4net.ILog _log = LogManager.GetLogger(typeof(someclass));

    public void Method()
    {
       _log.Debug("hello world!");
    }
}

The only changes to your code would be in the startup routine for your application where you configure the logger, this would be where you use log4net's DOMConfigurator methods and the XML configuration files.

public static void Main()
{
   LogManager.Configure(
              //where to put the logs : folders will be automatically created
              "LOGS\\log.txt", 
               500,// limit the file sizes to 500kb, 0 = no limiting
               false); // log method call names -> performance hit if enabled

}

Points of Interest

The code is pretty straightforward given it is only 200 lines long, but there is a couple of “gotchas” that I will point out here:

  • AppDomain.CurrentDomain.ProcessExit: For this function to work, you must set the thread IsBackground to True otherwise it will just ignore the process exit and continue the thread. This point took me around 4 hours and a lot of hassle to try to figure out.
  • Rolling filenames: If you look at the code for implementing the rolling functionality for file names, it’s simple and dirty but it works.
  • LOG format output: The log output format is hardcoded to my own preference but you can change it to what you like in the code.
  • Size limit: The size limitation for files is not exact but it does the job in the least lines of code and keeps the message contiguous, so you don’t have to open before and after files to see the messages.
  • Method names: If you set the showmethodnames in the Configuration function, the logger will output the method name for the function in the log file which is a great help for debugging code as it specifies the exact place of the message/error and you don’t have to do anything.

The only problem is the performance hit you get because the code does a stack trace and extracts the method name. I would not recommend using this in production code but then again if you don’t log that many messages, it doesn’t matter.

History

  • Initial release: 2010/12/07 
  • Update v1.1 : 2012/05/05
    • uses a timer instead of a thread
    • added Shutdown() for implicit closing of files
    • optimized string output

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Mehdi Gholam
Architect
United Kingdom United Kingdom
Member
Mehdi first started programming when he was 8 on BBC+128k machine in 6512 processor language, after various hardware and software changes he eventually came across .net and c# which he has been using since v1.0.
He is formally educated as a system analyst Industrial engineer, but his programming passion continues.
 
* Mehdi is the 5th person to get 6 out of 7 Platinums on CodeProject (13th Jan'12)

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionCurrent Version ProblemsmemberDewey28 Jul '12 - 12:47 
I noticed 3 things, only one of which is a real problem
 
1. The sample code uses the old version of the code.
2. The article references only the old code, talking about domains.
3. This is the kicker. In the new code you placed Configure and GetLogger inside an internal static class, which makes them inaccessible to the outside.
 
In other words, you can't configure the logger at all!
 
I haven't tried it yet, but I'm going to change the LogManager class back to public static, and hope it doesn't mess your code up anywhere.
AnswerRe: Current Version ProblemsmvpMehdi Gholam28 Jul '12 - 17:26 
That is embarrassing! Thanks Dewey!
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

Questioncan minilog4net be used in such condition?memberAlenty4 Jun '12 - 14:21 
Dear Gholan, When website users sign up , sign in, delete or send message etc, I want to log it. can minilog4net be suitable or is there better solution? Thank you in advance.
AnswerRe: can minilog4net be used in such condition?mvpMehdi Gholam4 Jun '12 - 18:56 
Yes, by all means, that is what it was created for.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

GeneralMy vote of 5memberHoyaSaxa9316 May '12 - 3:51 
This is good stuff! Thanks for sharing it.
GeneralRe: My vote of 5mvpMehdi Gholam4 Jun '12 - 18:57 
Thanks Hoya!
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

QuestionA very small improvement for performance [modified]memberwmjordan11 May '12 - 15:05 
Since the format of a log entry is invariant.
It is better to use "a fixed length array + String.Concat(array)", instead of StringBuilder, since the former has better performance.
 
For the log parameters (objs in the log method), it is easy to just use one line of code to convert them to a string.
String.Join(Environment.NewLine, Array.ConvertAll (objs, (o)=>{return o == null ? String.Empty : o.ToString ())


modified 12 May '12 - 22:26.

AnswerRe: A very small improvement for performancemvpMehdi Gholam11 May '12 - 18:10 
Thanks WMJordan!
 
You are probably right StringBuilder is a bit heavy on startup, although I would have to test to make sure.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

GeneralRe: A very small improvement for performancememberwmjordan12 May '12 - 16:39 
Thank you for taking time to consider this small issue.
 
I have been told about the heavy configuration about Log4Net and I have never used it before.
 
BTW, I am going to develop my own logger above your work.
 
In production environment, the logging should not be heavy. But sometimes, it is necessary to temporally escalate the log level to address the problem which only occurs there. Simple dynamic configuration might be used. For example, in the configuration file, we simply specify the log level as None, Summary or Debug, then the framework uses a NonLogFormatter, SummaryLogFormatter and DebugLogFormatter respectively after the configuration file is modified.
 
I use my own configuration framework so the modification of the configuration file won't require the application being restarted. The only thing I am going to do is to develop a customizable log formatter interface to support different log levels.
 
The rest stuff in Log4Net, such as multiple log sink, dynamic configurable log format, etc. aren't my concern. Thank you for your neat article and code! It is giving me a good start.
QuestionWhy moving to a timer?memberDewey5 May '12 - 17:02 
I see you now save using a timer, but you don't explain why.
 
Was there a problem with the threading, and would it not have been solved by using TPL(although TPL would limit the .Net version to 4+)?
AnswerRe: Why moving to a timer?mvpMehdi Gholam5 May '12 - 20:09 
Thanks Dewey!
 
Threads have the following problem :
- the take up a lot of time setting up (using a profiler will verify)
- the loop had Thread.Sleep in it which is considered bad
 
The timer way is much cleaner code wise, and simpler to understand.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

GeneralRe: Why moving to a timer?memberDewey14 May '12 - 15:49 
I finally got around to doing some research on this, and you're 100% correct about the threads.
 
They take a lot of cycles setting up, and almost as much winding down. In addition, they allocate quite a bit of memory as well.
 
In my research, it turns out that Thread.Sleep is best used for testing, not production, and if used in production, it should be Thread.Sleep(0), which is almost of no use, LOL!
 
Thanks for the heads up, now I have some of my code to fix up!
QuestionNice jobmemberMike Hankey5 May '12 - 8:29 
Very nice Mehdi looks like a very useful tool.
Thanks for your effort.
VS2010/Atmel Studio 6.0 ToDo Manager Extension
Version 3.0 now available.
There is no place like 127.0.0.1

AnswerRe: Nice jobmvpMehdi Gholam5 May '12 - 8:38 
Thanks Mike!
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

QuestionnUnit problemmemberSohailB17 Sep '11 - 0:30 
I have a test project in my solution which uses nUnit.
For the first test it is OK, but after that any test will fail with:
nUnitTester.Tester.EntitySaveAndLoad:
SetUp : System.IO.IOException : The process cannot access the file \Test\nUnitTester\bin\Debug\logs\Test.log; because it is being used by another process.

AnswerRe: nUnit problemmemberMehdi Gholam17 Sep '11 - 0:40 
NUnit keeps the session/objects active in memory, so the files are still open.
 
Try closing the nunit gui and running again.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

GeneralRe: nUnit problemmemberSohailB17 Sep '11 - 0:44 
Closing the GUI solves the problem, but it is frustrating.
Any way to close the file or dispose the logger I can put in TearDown?
GeneralRe: nUnit problemmemberMehdi Gholam17 Sep '11 - 0:48 
Yes, null the objects you are using.
 
You can also change the private shutdown method to public and call that when you want.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist
If the final destination is death, then we should enjoy every second of the journey.

GeneralMy vote of 4memberSatorArepoOperaRotas18 Jul '11 - 22:47 
nice and small!
QuestionUsing in web environmentmemberSatorArepoOperaRotas18 Jul '11 - 22:45 
Hi Mehdi
 
thanks for this nice piece of code!!
I'm using your logger in a webservice and I noticed it doesn't act properly with date logs: it continues to log on the same file.... the problem is it store the _lastFileDate in memory but if the process is recycled it resets the value and so it doesn't create a new file...
 
Thank you!
AnswerRe: Using in web environmentmemberMehdi Gholam18 Jul '11 - 22:55 
Interesting, I will look into it.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist

GeneralRe: Using in web environmentmemberSatorArepoOperaRotas19 Jul '11 - 0:03 
Here is my modified version that fit my needs: it appends datetime to filename and then compare current datetime with the one extracted from filename in order to change log file; I also added a LogVerbosity to set threshold logging message...
 
using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Threading;
using System.Globalization;
 
namespace MyLogger
{
    public interface ILog
    {
        void Debug(object msg, params object[] objs);
        void Error(object msg, params object[] objs);
        void Info(object msg, params object[] objs);
        void Warn(object msg, params object[] objs);
        void Fatal(object msg, params object[] objs);
    }
 
    public enum LogVerbosity
    {
        FATAL = 0,
        ERROR,
        WARNING,
        INFO,
        DEBUG
    }
 
    internal class FileLogger
    {
        public static readonly FileLogger Instance = new FileLogger();
 
        private FileLogger()
        {
            AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
            AppDomain.CurrentDomain.DomainUnload += new EventHandler(CurrentDomain_ProcessExit);
 
            _worker = new Thread(new ThreadStart(Writer));
            _worker.IsBackground = true;
            _worker.Start();
        }
 
        private Thread _worker;
        private bool _working = true;
        private Queue _que = new Queue();
        private StreamWriter _output;
        private string _baseFilename;
        private string _filename;
        private int _sizeLimit = 0;
        private long _lastSize = 0;
        private DateTime _lastFileDate;
        private bool _showMethodName = false;
        private string _FilePath = "";
 
        private const string dateTimeFormat = "yyyy-MM-dd";
 
        private LogVerbosity _verbosity = LogVerbosity.DEBUG;
 
        public bool ShowMethodNames
        {
            get { return _showMethodName; }
        }
 
        public LogVerbosity Verbosity
        {
            get { return _verbosity; }
            set { _verbosity = value; }
        }
 
        public void Init(string filename, int sizelimitKB, bool showmethodnames, LogVerbosity verbosity)
        {
            _showMethodName = showmethodnames;
            _sizeLimit = sizelimitKB;
            _baseFilename = filename;
            _filename = buildFilename();
            _verbosity = verbosity;
            // handle folder names as well -> create dir etc.
            _FilePath = Path.GetDirectoryName(filename);
            if (_FilePath != "")
            {
                _FilePath = Directory.CreateDirectory(_FilePath).FullName;
                if (_FilePath.EndsWith("\\") == false)
                    _FilePath += "\\";
            }
            _output = new StreamWriter(_filename, true);
            FileInfo fi = new FileInfo(_filename);
            _lastSize = fi.Length;
            _lastFileDate = fi.LastWriteTime;
        }
 
        private string buildFilename()
        {
            return Path.Combine(Path.GetDirectoryName(_baseFilename), string.Format("{0}_{1}{2}", Path.GetFileNameWithoutExtension(_baseFilename), DateTime.Today.ToString(dateTimeFormat), Path.GetExtension(_baseFilename)));
        }
 
        private void shutdown()
        {
            _working = false;
            _worker.Abort();
        }
 
        void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            shutdown();
        }
 
        private void Writer()
        {
            while (_working)
            {
                while (_que.Count > 0)
                {
                    object o = _que.Dequeue();
                    if (_output != null && o != null)
                    {
                        if (_sizeLimit > 0)
                        {
                            // implement size limited logs
                            // implement rolling logs
                            #region [  rolling size limit ]
                            _lastSize += ("" + o).Length;
                            if (_lastSize > _sizeLimit * 1000)
                            {
                                _output.Flush();
                                _output.Close();
                                int count = 1;
                                while (File.Exists(_FilePath + Path.GetFileNameWithoutExtension(_filename) + "." + count.ToString("0000")))
                                    count++;
 
                                File.Move(_filename,
                                    _FilePath +
                                    Path.GetFileNameWithoutExtension(_filename) +
                                    "." + count.ToString("0000"));
                                _output = new StreamWriter(_filename, true);
                                _lastSize = 0;
                            }
                            #endregion
                        }
 
                        string fileDate = _filename.Substring(_filename.LastIndexOf('_') + 1, dateTimeFormat.Length);
                        DateTime fileDateFromName = DateTime.ParseExact(fileDate, dateTimeFormat, CultureInfo.InvariantCulture);
 
                        if (DateTime.Now.Subtract(_lastFileDate).Days > 0 || DateTime.Now.Subtract(fileDateFromName).Days > 0)
                        {
                            // implement date logs
                            #region [  rolling dates  ]
                            _output.Flush();
                            _output.Close();
                            
                            _filename = buildFilename();
                            
                            _output = new StreamWriter(_filename, true);
                            _lastFileDate = DateTime.Now;
                            _lastSize = 0;
                            #endregion
                        }
 
                        _output.Write(o);
                    }
                }
                if (_output != null)
                    _output.Flush();
                Thread.Sleep(500);
            }
            _output.Flush();
            _output.Close();
        }
 
        private string FormatLog(string log, string type, string meth, string msg, object[] objs)
        {
            StringBuilder sb = new StringBuilder();
 
            sb.AppendLine(
                "" + DateTime.Now.ToString("dd'/'MM'/'yyyy HH:mm:ss.fff") +
                "|" + log +
                "|" + Thread.CurrentThread.ManagedThreadId +
                "|" + type +
                "|" + meth +
                "| " + string.Format(msg, objs));
 
            return sb.ToString();
        }
 
        public void Log(string logtype, string type, string meth, string msg, params object[] objs)
        {
            _que.Enqueue(FormatLog(logtype, type, meth, msg, objs));
        }
    }
 

    internal class logger : ILog
    {
        public logger(Type type)
        {
            typename = type.Namespace + "." + type.Name;
        }
 
        private string typename = "";
 
        private void log(string logtype, string msg, params object[] objs)
        {
            string meth = "";
            if (FileLogger.Instance.ShowMethodNames)
            {
                System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace(2);
                System.Diagnostics.StackFrame sf = st.GetFrame(0);
                meth = sf.GetMethod().Name;
            }
            FileLogger.Instance.Log(logtype, typename, meth, msg, objs);
        }
 
        #region ILog Members
 
        public void Debug(object msg, params object[] objs)
        {
            if(FileLogger.Instance.Verbosity >= LogVerbosity.DEBUG)
                log("DEBUG", "" + msg, objs);
        }
 
        public void Error(object msg, params object[] objs)
        {
            if (FileLogger.Instance.Verbosity >= LogVerbosity.ERROR)
                log("ERROR", "" + msg, objs);
        }
 
        public void Info(object msg, params object[] objs)
        {
            if (FileLogger.Instance.Verbosity >= LogVerbosity.INFO)
                log("INFO", "" + msg, objs);
        }
 
        public void Warn(object msg, params object[] objs)
        {
            if (FileLogger.Instance.Verbosity >= LogVerbosity.WARNING)
                log("WARN", "" + msg, objs);
        }
 
        public void Fatal(object msg, params object[] objs)
        {
            if (FileLogger.Instance.Verbosity >= LogVerbosity.FATAL)
                log("FATAL", "" + msg, objs);
        }
        #endregion
    }
 
    public static class LogManager
    {
        public static ILog GetLogger(Type obj)
        {
            return new logger(obj);
        }
 
        public static void Configure(string filename, int sizelimitKB, bool showmethodnames, LogVerbosity verbosity)
        {
            FileLogger.Instance.Init(filename, sizelimitKB, showmethodnames, verbosity);
        }
    }
 
}

GeneralRe: Using in web environmentmemberMehdi Gholam19 Jul '11 - 0:14 
Great job
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist

QuestionDatabasememberD Strauss13 Jul '11 - 20:48 
Can I use this to log to a database table instead of a text file?
AnswerRe: DatabasememberMehdi Gholam13 Jul '11 - 20:55 
Not in this version I'm afraid, you can however write the database code yourself, just implement your own writer method.
Its the man, not the machine - Chuck Yeager
If at first you don't succeed... get a better publicist

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 5 May 2012
Article Copyright 2010 by Mehdi Gholam
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid