3
\$\begingroup\$

I have finally assembled an attempt to create a thread safe logging class and also ensured by file flags that file should not be accessed by different processes at same time. This classed will be inside DLL. Below is the implementation,

   static class GMLogger
    {

       private static List<string> _buffer = new List<string>();
       private static int _maxBufferSize = 25;  // number of messages  permitted in the buffer at a time
       public static string _logDir = "C:\\Logs";
       private static readonly object _syncObject = new object();

       // Log message
        public static  void Log(string logMessage)
        {
            try
            {
                lock (_syncObject)
                {
                    _buffer.Add(logMessage);

                    Save(_buffer);
                }
            }
            catch (Exception ex)
            {
                throw;
            }
        }

       // Save buffer if needed
        private static void Save(List<string> buffer)
        {
            if (buffer.Count > _maxBufferSize)
            {
                // Write to file
                if (!_logDir.EndsWith("\\")) _logDir += "\\"; 
                DirectoryInfo di = Directory.CreateDirectory(_logDir);
                var todaysLogFilePath = Path.Combine(_logDir,
                                                       ("Log" + DateTime.Now.ToString("yyyy-MMMM-dd") + ".txt"));
                using (FileStream aFile = new FileStream(todaysLogFilePath, FileMode.Append, FileAccess.Write, FileShare.None))
                using (StreamWriter sw = new StreamWriter(aFile))
                {
                    for (int i = 0; i < buffer.Count(); i++)
                    {
                        sw.WriteLine(buffer[i]);
                    }
                }

                // Clear buffer
                buffer.Clear();
            }



    }
}

Please note I am a beginner first of all, and if this class achieves the simple functionality of writing simple lines to file I am OK with it.

Of course you can comment also about other things which are in the context of CodeReview site. But please not too advanced feedback since as I said I am beginner.

Second, here are some specific questions I have about it:

  1. Have I used the lock correctly?

  2. This class maybe used by many threads, will I have performance problems because of thread waiting for each other? NOTE: I log only ERRORS so even if many threads are using it, not many errors should be occurring right? Also note how because of performance I have a cache for log messages.

  3. Finally, only problem I see if different programs use this class to write to same file, how can I distinguish inside logs which program or process made an entry? How is this solved typically? (I assume one process can write to this file if another finished writing, isn't it?)

PS. This is all the functionality I want, I don't plan to extend this class. So please no recommendations about 3rd party libs. If my class achieves above points well, I am ok with it. Or if you see some problem with it, please tell.

And in general I am interested if its safe to use my class because some people always say to not create your own logging class. Is it ok if I use this class? I see people creating log classes such as here http://www.codeproject.com/Tips/585796/Simple-Log, so what is problem if I created one as above?

\$\endgroup\$
1

4 Answers 4

7
\$\begingroup\$

One of the things you can do to get some performance back (albeit not a lot, but still more than none) is rewrite Save to have one responsibility.

As it stands now, Save has several responsibilities. It's responsible for:

  1. Checking if the buffer is of sufficient length to be saved.
  2. Validating that _logDir ends with a \.
  3. Creating the _logDir directory.
  4. Coming up with the filename for the log file.
  5. Saving the buffer to disk.
  6. Clearing the buffer after saving.

The Save method should only be responsible for saving (and maybe clearing) the buffer. If you move the if check outside to the Log method, you remove a (minor) bit of overhead.

A better style would be to create a method SaveIfFull which does the if-check, then calls Save which actually saves the buffer.

You have a:

public static string _logDir = "C:\\Logs";

Which is generally frowned upon. The best-practice for public members is PascalCase with no leading characters. So, instead, it should be:

public static string LogDir = "C:\\Logs";

Never omit braces from if statements, and never one-line them. Adding braces won't prevent bugs, but it will help prevent bugs.

if (!_logDir.EndsWith("\\")) _logDir += "\\";

Should be rewritten as:

if (!_logDir.EndsWith("\\"))
{
    _logDir += "\\";
}

Likewise, you should make _logDir (which we to LogDir) a property, with a private backing field.

private static string _logDir = "C:\\Logs\\";
public static string LogDir
{
    get
    { 
        return _logDir;
    }
    set
    { 
        if (string.IsNullOrWhiteSpace(value))
        { 
            throw new ArgumentException("The specified value must not be null, whitespace, or an empty string.", "value");
        }

        _logDir = value;

        if (!_logDir.EndsWith("\\"))
        {
            _logDir += "\\";
        }
    }
}

You can also add other validation logic in there as you need, but this way it helps separate responsibilities further.

To get rid of some of the other additional responsibilities Save has, we can consider a method GetLogFileName(DateTime loggedAt) which would return the filename, then Save would call string todayFileName = GetLogFileName(DateTime.Now) or string todayFileName = GetLogFileName(DateTime.UtcNow) as appropriate.


If you have C#6.0 a few of these tasks become easier/simpler:

  • Path.Combine(_logDir, ("Log" + DateTime.Now.ToString("yyyy-MMMM-dd") + ".txt")); simplifies to: Path.Combine(_logDir, $"Log{DateTime.Now.ToString("yyyy-MMMM-dd")}.txt");
  • You can replace "value" at the end of ArgumentException with nameof(value). (Which allows you to refactor that message cleanly.)

As far as your primary concerns:

  1. Yes, the idea of the lock is to mark a shared resource as "in-use" so that threads know to wait on each other before they can proceed. You could, however, replace it with a bool instead of an object for smaller overhead.
  2. You could have performance implications by the lock, but that's the downside of multithreading: somewhere you will have a bottleneck from context-switching, et al.
  3. That is beyond the scope of Code Review.

Another concern: if there is a substantial delay between messages, it may occur that the log is never written to before the application is closed (or messages are left in the buffer), this can be alleviated by scheduling a Timer to save the log at periodic intervals as well, so that if there is a substantial delay the log is saved anyway.

\$\endgroup\$
15
  • 2
    \$\begingroup\$ @Quser, no one can tell you if your class is as safe to use as NLog or another widely used logging library because it's not widely used. As a beginner, this is hard to understand but eventually you will see that the simplest, safest looking code can cause completely unforeseeable problems. If you are just making something for fun then go ahead, but if other people's money/data/reputation is on the line you should go with a tested solution. \$\endgroup\$ Commented Oct 21, 2015 at 17:15
  • 3
    \$\begingroup\$ @Quser If you have made changes to the code, then you should post a follow-up, iterative review question. \$\endgroup\$ Commented Oct 21, 2015 at 18:32
  • 2
    \$\begingroup\$ @Quser What happens if you get 24 messages in the buffer over a period of several minutes, and then the programme get's closed? All those messages are lost. By instituting a timer to write the log every n time, it guarantees that you don't end up with a situation where messages don't get written just because the buffer didn't fill up. \$\endgroup\$ Commented Oct 21, 2015 at 19:04
  • 2
    \$\begingroup\$ @Quser As stated before by Josh Rumbut, none of us can guarantee it's safety. The best we can do is point out potential areas it may have shortcomings in (as done in several of these answers). The best you can do is take our advice, improve your code, and then use your code. If you had no intention of actually using this class, why did you submit it as a question here? The only way you'll learn and improve as a programmer is to fail, and learn from those failures. I recommend you use this as a chance to learn and improve. :) \$\endgroup\$ Commented Oct 21, 2015 at 19:08
  • 6
    \$\begingroup\$ @Quser Please respect the right for Code Review members who have volunteered to review your code to decline to provide further help. EBrown has pointed out some concerns, but in the end, you are the owner of your code. \$\endgroup\$ Commented Oct 21, 2015 at 19:42
5
\$\begingroup\$

Some quick remarks:

  • _buffer can be made readonly.
  • _maxBufferSize can be a const.
  • _logDir is public and thus should be PascalCased.
  • What the point of catch (Exception ex) when all you do is throw?
  • What's the point of if (!_logDir.EndsWith("\\")) _logDir += "\\";?
  • You never use di, so why do you assign it?
  • Why do you use for (int i = 0; i < buffer.Count(); i++) instead of the simpler foreach (var line in buffer)?
  • Your comments are superfluous and merely add noise. // Log message isn't telling me anything I cannot know by merely reading Log(string logMessage). Comments should be used to tell why, not what.
\$\endgroup\$
2
  • \$\begingroup\$ Thanks your comments are not major - I will wait for other opinions too, but still your comments make sense. But in other cases do you think it is OK to use my class for logging? Some people are always complaining how you shouldn't create one yourself. But I want to use this class, and not 3rd party, because I am not sure 3rd party class has all functionality I need(e.g. create log file names same as current date), and some have hassle installing \$\endgroup\$
    – user87512
    Commented Oct 21, 2015 at 15:02
  • 2
    \$\begingroup\$ @Quser I wouldn't roll my own logging class, no. NLog for instance offers plenty of options. I don't see anything in your class that isn't available in most logging frameworks. \$\endgroup\$
    – BCdotWEB
    Commented Oct 21, 2015 at 15:06
5
\$\begingroup\$
  1. Your class is not thread safe. By exposing _logDir field, you introduce race condition. Your Save() method will fail if some other thread changes _logDir right after Directory.CreateDirectory, but before Path.Combine. You must either hide this field or replace it with property with "locked" setter.
  2. Your class is not optimized. What happens if your hard drive is busy? Or if you specify network path and your network connection lags? In those cases it might take seconds (in some rare cases - even minutes) to write to file. If your application is not yet frozen at this point, it will freeze while waiting for lock, if you happen to call Log method again from UI thread. Your class is also not optimized for cases, where Log method is called often.
  3. Your class is buggy. It won't write anything to log, if there are less than 25 elements in the buffer. So for example, the last error, which is also usually the most crucial one, is almost guranteed to be lost.

so what is problem if I created one as above?

So, basically the problem is that you are most likely lack both knowledge and experience needed to implement logging in a way, which would work correctly in every possible scenario. Same goes for me, for example. That is why I use logging frameworks, which were written by experts and were tested by other developers. You certainly can implement your own logging if you wish to do so. The only thing I can guarantee though, is that you will fail multiple times, before you will get it right. It will be good learning experience, but it will also take time, which you might not have.

\$\endgroup\$
2
  • \$\begingroup\$ Your point 1 is not a big deal sure I can hide it. Point 3 makes more sense, do you have some idea how to fix it? Even then I can change that method and set the buffer length to 1 - Plus I said I only log ERRORS, so there should not be much logging going on?? \$\endgroup\$
    – user87512
    Commented Oct 22, 2015 at 15:22
  • \$\begingroup\$ That is why I use logging frameworks, which were written by experts and were tested by other developers. I think they are no more experts then me and you. They just wrote something and no one tested it anyway like the famous TrueCrypt story. Although open source until it died no one was interested in looking at the source code actaully. I don't think the popular loggers do any magic. \$\endgroup\$
    – t3chb0t
    Commented Oct 22, 2015 at 18:42
4
\$\begingroup\$

A comment about your Log(string) method. The lock keyword is just syntactic sugar for a call to the Monitor class. See here. Here is what lock becomes:

bool lockWasTaken = false;
var temp = obj;
try 
{ 
    Monitor.Enter(temp, ref lockWasTaken); 
    { 
        //body 
    } 
}
finally 
{ 
    if (lockWasTaken) Monitor.Exit(temp); 
}

So your Log(string) method is actually this:

public static void Log(string logMessage)
{
    try
    {
        bool lockWasTaken = false;
        var temp = _syncObject;
        try
        {
            Monitor.Enter(temp, ref lockWasTaken); 
            {
                _buffer.Add(logMessage);
                Save(_buffer);
            }
        }
        finally 
        { 
            if (lockWasTaken) Monitor.Exit(temp); 
        }
    }
    catch (Exception ex)
    {
        throw;
    }
}

Did you put in your catch and throw statement because you were worried that a possible exception will get eaten somewhere? You can see from this that that's not the case.

\$\endgroup\$
2
  • \$\begingroup\$ I've been using C# for some time already but I had never seen lock explained like that ;-) It's good to know what it actualy is. \$\endgroup\$
    – t3chb0t
    Commented Oct 21, 2015 at 17:07
  • \$\begingroup\$ @t3chb0t lol, just a bit of C# trivia for your Wednesday. The only other feature of C# I can think of that's like this is the using keyword, but that's much more well known. \$\endgroup\$ Commented Oct 21, 2015 at 17:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.