Log collector – logging on steroids

Logging is a necessary part of any well-designed system. In this article, I would like to present a component that will make it easier to work with any application logs. In one of my projects, it was the perfect approach to analyze the program scope. It is easy to use and ideally suited for creating all kinds of reports. This solution is based on ILogCollector interface. Main features of component:

  • contains three groups of collections (infos, warnings, errors)
  • notify observers when message is added
  • enables the processing of logs at any time and in different ways
  • can be transmitted deep into the nested classes by reference
  • contains methods used to format messages
 
   public interface ILogCollector
    {
        List Infos { get; }
        List Warnings { get; }
        List Errors { get; }

        int GetAllLogsCount { get; }
        string GetTotalResult { get; }

        void AddInfo(string info);
        void AddInfo(string info, params object[] args);
        void AddWarning(string warning);
        void AddWarning(string warning, params object[] args);
        void AddError(string error);
        void AddError(string error, params object[] args);
        void AddError(string message, Exception ex);
        void Clear();

        IEnumerable GetAllLogsSortedAsFriendlyStrings();

        event LogCollector.CollectorChangeEventHandler CollectionChange;
    }

Implementation:

 
     public class LogCollector : ILogCollector
    {
        private List _infos;
        private List _warnings;
        private List _errors;

        public List Infos { get { return _infos; } }
        public List Warnings { get { return _warnings; } }
        public List Errors { get { return _errors; } }

        public LogCollector()
        {
            _infos = new List();
            _warnings = new List();
            _errors = new List();
        }

        public void AddInfo(string info)
        {
            _infos.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Info,
                Text = info
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Info, info));
        }

        public void AddInfo(string info, params object[] args)
        {
            _infos.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Info,
                Text = string.Format(CultureInfo.InvariantCulture, info, args)
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Info, string.Format(CultureInfo.InvariantCulture, info, args)));
        }

        public void AddWarning(string warning)
        {
            _warnings.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Warning,
                Text = warning
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Warning, warning));
        }

        public void AddWarning(string warning, params object[] args)
        {
            _warnings.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Warning,
                Text = string.Format(CultureInfo.InvariantCulture, warning, args)
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Warning, string.Format(CultureInfo.InvariantCulture, warning, args)));
        }

        public void AddError(string error)
        {
            _errors.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Warning,
                Text = error
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Error, error));
        }

        public void AddError(string error, params object[] args)
        {
            _errors.Add(new LogItem
            {
                DateTime = DateTime.Now,
                LogItemType = LogItemType.Warning,
                Text = string.Format(CultureInfo.InvariantCulture, error, args)
            });

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Error, string.Format(CultureInfo.InvariantCulture, error, args)));
        }

        public void AddError(string message, Exception ex)
        {
            if (ex == null) throw new ArgumentNullException("No error defined");

            if (CollectionChange != null)
                CollectionChange(new CollectorChangeEventArgs(LogItemType.Error, string.Format("{0}{1}{2}", message, ex.Message, ex.StackTrace)));
        }

        public void Clear()
        {
            _infos.Clear();
            _warnings.Clear();
            _errors.Clear();
        }

        public IEnumerable GetAllLogsSortedAsFriendlyStrings()
        {
            var allItems = new List();
            allItems.AddRange(Infos);
            allItems.AddRange(Warnings);
            allItems.AddRange(Errors);
            return
                allItems.OrderBy(x => x.DateTime)
                    .Select(x => string.Format("{0} {1}: {2}", x.Text, x.DateTime, x.Text));
        }

        public int GetAllLogsCount
        {
            get { return Infos.Count() + Warnings.Count() + Errors.Count(); }
        }

        public string GetTotalResult
        {

            get
            {
                var sbTotal = new StringBuilder();

                sbTotal.AppendFormat(" Total Log count: {0}{1}", GetAllLogsCount, Environment.NewLine);
                sbTotal.AppendFormat(" Errors: {0}{1}", Errors.Count, Environment.NewLine);
                sbTotal.AppendFormat(" Warnings: {0}{1}", Warnings.Count, Environment.NewLine);
                sbTotal.AppendFormat(" Infos: {0}{1}", Infos.Count, Environment.NewLine);

                return sbTotal.ToString();
            }
        }

        public event CollectorChangeEventHandler CollectionChange;

        public delegate void CollectorChangeEventHandler(CollectorChangeEventArgs args);

    }

For the purpose of the example let’s create a simple class and track the import work with our new component.
Let the import class perform some standard operations including:

  • Reading
  • Validation
  • Process
  • Finish

Every time when log is added, LogCollector fires an event to subscribers so it’s easy to use in different ways:

  • Console output
  • Write to file, database
  • Send email via nLog /log4net
  • Push notification with SignalR
public class BasicImport : IImport
    {
        public ILogCollector LogCollector { get; set; }

        public void ReadContent()
        {
            LogCollector.AddInfo(" Start BasicImport.ReadContent() on file {0}", "C:\\example.csv");
            LogCollector.AddWarning(" Low disk space");
            Thread.Sleep(1500);
            LogCollector.AddInfo(" End BasicImport.ReadContent()");
        }

        public void Validate()
        {
            LogCollector.AddInfo(" Start BasicImport.Validate()");
            Thread.Sleep(2000);
            LogCollector.AddInfo(" End BasicImport.Validate()");
        }

        public void Process()
        {
            LogCollector.AddInfo(" Start Process.Process()");
            Thread.Sleep(2000);
            LogCollector.AddInfo(" End BasicImport.Process()");
        }

        public void Finish()
        {
            LogCollector.AddInfo(" Start BasicImport.Finish()");
            Thread.Sleep(1500);
            LogCollector.AddError(" Database {0} is not avaliable, retry count {1}", "[SomeDataBase]", 1);
            Thread.Sleep(2500);
            LogCollector.AddInfo(" Database {0} is avaliable", "[SomeDataBase]");
            Thread.Sleep(2000);
            LogCollector.AddInfo(" End BasicImport.Finish() OK");
        }
    }

Now it’s time to inject the LogController property into import class with Unity IoC

Unity install

   container.RegisterType<IImport, BasicImport>(new InjectionProperty("LogCollector")).
                    RegisterType<ILogCollector, LogCollector>();

Putting all together we obtain

 static void Main(string[] args)
        {
            Console.CursorVisible = false;
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("\n Log Collector Example");

            using (IUnityContainer container = new UnityContainer())
            {
                /* Inject LogCollector instance to import class */
                container.RegisterType<IImport, BasicImport>(new InjectionProperty("LogCollector")).
                    RegisterType<ILogCollector, LogCollector>();

                var basicImport = container.Resolve();
                /* Grab the event */
                basicImport.LogCollector.CollectionChange += LogCollector_CollectionChange;

                basicImport.ReadContent();
                basicImport.Validate();
                basicImport.Process();
                basicImport.Finish();

                Console.ForegroundColor = ConsoleColor.White;

                Console.WriteLine("\n ----- TOTAL STATS ------");
                Console.WriteLine(basicImport.LogCollector.GetTotalResult);
            }

            Console.ReadKey();
        }

        /* You can use any output source: log4Net or nLog to write log to database, file or send email */
        private static void LogCollector_CollectionChange(CollectorChangeEventArgs args)
        {
            if (args.LogItemType == LogItemType.Info)
                Console.ForegroundColor = ConsoleColor.Green;

            if (args.LogItemType == LogItemType.Warning)
                Console.ForegroundColor = ConsoleColor.Yellow;

            if (args.LogItemType == LogItemType.Error)
                Console.ForegroundColor = ConsoleColor.Red;

            Console.WriteLine(args.Message);
        }

Output

Log Collector Piotr Łuksza

Source code is avaliable here

Polskie community Umbraco
Umbra Talk
Umbra Talk Polska
Tanie laptopy białystok

You may also like...

2 Responses

  1. Your implementation have some disadvantages, I hope it is only sample one:
    1) Clear do not notifies observers – it changes collection – maybe you should name you should change event name.
    2) It is not thread safe at all, so It will cause weird exceptions it in any app which use more than one thread: Web API, WCF Service, ASP.Net (both Web Forms and MVC), and even most of Desktop apps. You can solve it by building separate instance per call (or per thread) but it can broke your statistics (It wouldn’t contains whole scope of events that have happend), you can also use locking collections or build lock by hand but it make your code slower. Event raising is not thread save to.
    3) You stores logs in memory it can exhaust all memory especially that you are logging Infos and Warnings (I think it is lot of it) and your applications runs for very long time (e.g. ASP.Net app).

    • Piotr says:

      Hi Wojtek,
      thanks for replay and good points. I agree, above solution for async case should be modified. Memory is also an important issue, gc not always cleans strings as we wish, so we should monitor memory performance. Hava a nice day!

Leave a Reply

Your email address will not be published. Required fields are marked *