April 20, 2016

 

A Little Bit More Convenient LogFile Class

All kinds of programs have a prevalent need to write log files. You want your program to basically "keep notes" of it's progress at significant stages, and note various pieces of relevant information (e.g., you read in an input file and you want to know the number of lines [records], and maybe you're counting or summing lines, or pieces of data in those lines, in various categories, and you want the program to note this information in the log file as it is proceeding). Of course, if certain events happen - especially in the case of errors where the program can't proceed to do what it's supposed to do - then you want the program to note this in the log file. This way when your program is finished, or terminates on error, you can look at the information in the log file to see the results, or determine what went wrong so you know what you need to do to fix the problem. (For example, maybe a necessary input file that is produced by some other process hasn't been produced yet, so you have a file existence check, or perhaps some kind of file data integrity check, built into your program which, if triggered, will terminate gracefully and note the problem in the log file.)

(By the way, don't make the mistake that some NASA programmer(s) made with the Spirit rover computer that landed on Mars, which maxed out its data storage space a couple of weeks after it landed because the programmer(s) neglected to put some sort of limiter on log files. This is a topic for another article - I won't be covering it in the sample code here - but in an actual production program you must be sure to have code that implements some sort of method to limit the space that log files take up. For example, a very simple implementation would be to count the number of log files in a log file directory that the program is writing its log file to, and once it exceeds some set number, then delete the oldest log files until you get to less than the limit. Another method would be to sum the actual sizes of the log files, and then if the maximum size taken by all the log files exceeds some set amount, then delete the oldest log files until you get to less than the size limit.)

For my own usage with my C# programs, I dug around on the Internet a bit, and decided to use the "Convenient LogFile Class" by Jonathan Wood. The bulk of the code here is from his original code, but I've made a number of changes to the details, and added a few functions, so I'm calling this "A Little Bit More Convenient LogFile Class

The entirety of the code for the class is shown at the bottom.

Instantiating a LogFile Object

The general context of my discussion here is a Console Application. Obviously, if your context is different, then you'll do things differently. Typically I'm starting up my log file as soon as the program starts, so (for a Console App) I do this as the very first thing in my Main() function, like so:

static int Main(string[] args) {
  LogFile logfile = new LogFile(filename: file_log, keepOpen: true);
  if (!logfile.Status) {
    Console.WriteLine(logfile.StatusMessage);
    return 16;
  }
  logfile.WriteLine("{0} started", progname);

  ...

Note: This LogFile class is designed to automatically prepend what you give to .WriteLine() with a timestamp (see at the bottom the code for the class method WriteLine() - which actually feeds to a Write() method; there you'll see the usage of the DateTime.Now.ToString() function).

In my typical usage, I create this logfile object and pass it around where it needs to be used - and this is why I've turned on "keepOpen" (set it to true). If you don't turn this flag on, then the object will close the file after each write, and reopen the file in append mode before each write after the first one. Obviously, the most efficient operation is to keep the file open. (So note that there are no considerations for asynchronous or parallel processing with this. As described in this article, this is strictly for the context of a synchronous process.)

That "file_log" variable you see, I've defined in some "prefatory" code at the beginning of the Program class before any methods, like so:

namespace LogFileTest {
  class Program {
    const string progname = "LogFileTest";

    const string file_log_base = "log_" + progname + "_";
    const string file_log_ext = ".txt";
    static readonly string datetime_start = DateTime.Now.ToString("yyMMddHHmmss");
    static readonly string file_log = Path.Combine(dir_base_dat, file_log_base + datetime_start + file_log_ext);

    ...

(You need a "using System.IO;" for the "Path" object, if you don't already have it.)

I added some code to the constructor method to catch exceptions related to the path you provide, and if any such exceptions are encountered then the "Status" of the object will be set to false, so immediately after instantiation, you can check this status to see if things are okay with the LogFile object.

Using the LogFile Object

For methods where you are writing to the log file, you would just put in something like "LogFile log" as one of the parameters. Here is a trivial, totally out-of-context example:

static int GetWebData(LogFile log) {
  bool bBad = false;
  IniParser iniparser;
  string file_cfg = @"C:\prog\getwebdata\cfg\getwebdata.ini";
  string x;
  try { iniparser = new IniParser(file_cfg); }
  catch (Exception ex) {
    x = ex.Message;
    logfile.WriteLine("error: INI file parser failed on \"{0}\"", file_cfg);
    logfile.WriteLine("exception message: {0}", x);
    return -1;
  }

  ...

The method call would look something like this:

int rv = GetWebData(logfile);

If you need to flush the stream, which you may want to do at critical points, just do this (where the LogFile object is being referenced by the name "log"):

log.Flush();

When you're finished with the log file, just do this:

logfile.Close();

The LogFile Class

using System;
using System.IO;
using System.Security;

// derived from: http://www.blackbeltcoder.com/Articles/files/a-convenient-logfile-class

namespace UtilBasic {
  public class LogFile : IDisposable {
    protected string _filename;
    protected StreamWriter _writer;
    protected bool _disposed;
    protected bool _status;
    protected string _status_message;

    /// <summary>
    /// Gets the current filename
    /// </summary>
    public string Filename { get { return _filename; } }

    /// <summary>
    /// Gets or sets if the file is closed between each call to Write()
    /// </summary>
    public bool KeepOpen { get; set; }

    /// <summary>
    /// Gets or sets if logging is active or suppressed
    /// </summary>
    public bool IsLogging { get; set; }

    public bool Status { get { return _status; } }

    public string StatusMessage { get { return _status_message; } }

    /// <summary>
    /// Constructs a new LogFile object.
    /// </summary>
    /// <param name="filename">Name of log file to write.</param>
    /// <param name="append">If true, data is written to the end of any
    /// existing data; otherwise, existing data is overwritten.</param>
    /// <param name="keepOpen">If true, performance is improved by
    /// keeping the file open between writes; otherwise, the file
    /// is opened and closed for each write.</param>
    public LogFile(string filename, bool append = true, bool keepOpen = false) {
      _status = true;
      string filepath = null;
      try { filepath = Path.GetFullPath(filename); }
      catch (ArgumentException ex) { _status_message += ex.Message; _status = false; }
      catch (SecurityException ex) { _status_message += ex.Message; _status = false; }
      catch (NotSupportedException ex) { _status_message += ex.Message; _status = false; }
      catch (PathTooLongException ex) { _status_message += ex.Message; _status = false; }

      if (_status == false) {
        _filename = null;
        IsLogging = false;
        KeepOpen = false;
        _disposed = true;
        return;
      }

      _filename = filepath;

      string dir = null;
      if (_filename != null) {
        try { dir = Path.GetDirectoryName(_filename); }
        catch (ArgumentException ex) { _status_message += ex.Message; _status = false; }
        catch (PathTooLongException ex) { _status_message += ex.Message; _status = false; }
      }

      if (_status == false) {
        _filename = null;
        IsLogging = false;
        KeepOpen = false;
        _disposed = true;
        return;
      }

      if (!Directory.Exists(dir)) {
        try { Directory.CreateDirectory(dir); }
        catch (ArgumentNullException ex) { _status_message = ex.Message; _status = false; }
        catch (ArgumentException ex) { _status_message = ex.Message; _status = false; }
        catch (NotSupportedException ex) { _status_message = ex.Message; _status = false; }
        catch (PathTooLongException ex) { _status_message = ex.Message; _status = false; }
        catch (DirectoryNotFoundException ex) { _status_message = ex.Message; _status = false; }
        catch (IOException ex) { _status_message = ex.Message; _status = false; }
        catch (UnauthorizedAccessException ex) { _status_message = ex.Message; _status = false; }
      }

      if (_status == false) {
        _filename = null;
        IsLogging = false;
        KeepOpen = false;
        _disposed = true;
        return;
      }

      _status_message = "Good";
      KeepOpen = keepOpen;
      IsLogging = true;

      _writer = null;
      _disposed = false;

      // Delete existing file if not appending (ignore exceptions)
      if (!append) {
        try {
          File.Delete(_filename);
        }
        catch { }
      }
    }

    /// <summary>
    /// Closes the current log file
    /// </summary>
    public void Close() {
      if (_writer != null) {
        _writer.Dispose();
        _writer = null;
      }
    }

    /// <summary>
    /// Writes a formatted string to the current log file
    /// </summary>
    /// <param name="fmt"></param>
    /// <param name="args"></param>
    public void Write(string fmt, params object[] args) {
      Write(String.Format(fmt, args));
    }

    /// <summary>
    /// Writes a string to the current log file
    /// </summary>
    /// <param name="s"></param>
    public void Write(string s, bool bAddNewLine = false) {
      if (IsLogging) {
        // Establish file stream if needed
        if (_writer == null) _writer = new StreamWriter(_filename, true);

        // Write string with date/time stamp
        if (bAddNewLine) _writer.WriteLine(String.Format("[{0}] {1}", DateTime.Now.ToString("yyMMddHHmmss"), s));
        else _writer.Write(String.Format("[{0}] {1}", DateTime.Now.ToString("yyMMddHHmmss"), s));

        // Close file if not keeping open
        if (!KeepOpen) Close();
      }
    }

    /// <summary>
    /// Writes a formatted string to the current log file
    /// </summary>
    /// <param name="fmt"></param>
    /// <param name="args"></param>
    public void WriteLine(string fmt, params object[] args) {
      WriteLine(String.Format(fmt, args));
    }

    /// <summary>
    /// Writes a string to the current log file
    /// </summary>
    /// <param name="s"></param>
    public void WriteLine(string s) {
      Write(s, true);
    }

    /// <summary>
    /// Flushes the stream
    /// </summary>
    public void Flush() {
      if (IsLogging) {
        if (_writer == null) return;
        _writer.Flush();
      }
    }

    #region IDisposable Members

    public void Dispose() {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
      if (_disposed) { return; }
      // Need to dispose managed resources if being called manually
      if (disposing) Close();
      _disposed = true;
    }

    #endregion
  }
}

Comments: Post a Comment

Links to this post:

Create a Link



<< Home

This page is powered by Blogger. Isn't yours?