C#中的简单异步记录器

2021-03-20 14:35:49 浏览数 (1)

C#中的简单异步记录器

  • C#中的简单异步记录器
    • 介绍
    • 背景
    • 使用代码
    • 数据封装
    • 编写日志条目
    • 未来的工作
    • 兴趣点
    • 历史
    • 许可证
    • 关于作者
    • 源代码

C#中的简单异步记录器

本文翻译自CodeProject上面的一篇博客A Simple Asynchronous Logger in C#

作者:Toby Patke 2020年5月24日 Ms-PL 6分钟阅读

Clearcove.Logging是一个非常简单的日志记录库,旨在使用直接许可条款来满足大多数日志记录需求。

Download SimpleLogger.zip - 7.2 KB

介绍

我知道您在想什么-世界上真的需要另一个日志记录库吗?

如果您要在.NET中寻找日志记录库,则有很多选择。 这里有NLog,Log4Net,Enterprise Logging,Serilog和Common.Logging,仅举几例。 不难找到由才华横溢的开发人员编写的日志库,这些开发人员花费了大量时间和精力来创建强大而功能丰富的软件。

这个问题还需要解决吗?

背景

好吧,几个月前,我自己在市场中寻找日志记录库。 我是一个通过Internet分发的商业桌面应用程序的作者。 因此,我有三个硬性要求:

  • 1.日志条目应异步写入。 我已经看到太多的应用程序由于同步日志记录而遭受了极端的性能问题。
  • 2.该库应尽可能小。 我不希望我的用户必须下载和加载1 MB的DLL才能使用像日志记录一样简单的功能。 越小越好。
  • 3.我不想增加应用程序许可的复杂性。 目前,我的客户必须同意我的许可条款。 添加带有单独许可条款的第三方组件可能意味着需要额外的工作来评估我的产品。 也许这只是妄想症,但我想保持简单。

我以为这些要求很简单,但是事实证明我找不到满足我需求的任何东西。 特别是,我发现许多日志库的许可条款不可接受,因为我只是不想被迫分发“另一个”许可。

因此,我编写了自己的日志记录库– Clearcove.Logging。 它只有83行代码,非常轻巧。 完整的实现也位于单个.cs文件中,因此无需导入库即可重新使用它。 该代码是使用VS 2017编写的,但是我尝试编写与早期版本兼容的代码。 日志库的目标是.NET 2.0,以吸引更广泛的受众。

我认为这种记录日志的方法对于以下情况是一个不错的选择:

  • 1.没有复杂日志记录要求的应用程序
  • 2.诸如实用程序之类的小型应用程序可以从简化的部署中受益
  • 3.必须将许可复杂性降至最低的情况 那么它是怎样工作的?

使用代码

首先,我考虑了要记录的信息。 我想要一个简单的API,可以用来记录诸如时间戳,日志名称,线程ID和消息之类的信息。 我对Log4Net API非常熟悉,并从中大量借鉴了经验。

要声明和使用日志记录器,可以使用如下语法:

代码语言:javascript复制
var log = new Logger(typeof(Program));    // Class level declaration.
log.Error("My error message", exception); // Logging from within a method.
log.Info("My info message");

如果您过去曾经使用过其他日志记录库,则希望可以熟悉此语法。

数据封装

接下来,我想将日志条目表示为一个简单的对象。 这样做的主要原因是我希望记录器能够引发日志事件。 我有时在创建单元测试和集成测试时会使用这些事件,因为我发现它会有所帮助。 这只是个人喜好。 如果您对引发日志记录事件不感兴趣,则可以简化此代码。 日志事件封装在LogMessageInfo对象中,该对象实现为:

代码语言:javascript复制
public sealed class LogMessageInfo
{
    public readonly DateTime Timestamp;
    public readonly string ThreadId;
    public readonly string Level;
    public readonly string Logger;
    public readonly string Message;
}

编写日志条目

所以现在,我们来谈谈它的实质。 上面讨论的代码的API实现和数据封装部分很冗长,但非常简单。 但是,异步日志记录有点细微差别。 例如,如果引发导致应用程序关闭的异常,会发生什么? 我们如何知道所有日志条目将按照接收顺序写入? 有几种方法可以解决此问题。 Clearcove.Logger以一种简单但有些微不足道的方式解决了它:

代码语言:javascript复制
static void Main(string[] args)
{
      var targetLogFile = new FileInfo("./MyApp.log");
      Logger.Start(targetLogFile); // Loggers will complains if you skip initialization
      try
      {
          Run(args);
      }
      finally
      {
          Logger.ShutDown(); // Removing this line may result in lost log entries.
      }
}

这是Clearcove.Logging与其他实现(例如Log4Net)不同的示例。我们必须告诉记录器何时开始和停止记录。在尝试将任何日志条目写入日志文件之前,我们必须执行此操作。将Logger.ShutDown()调用放在finally语句中,应该使我们的日志记录器有机会在应用程序关闭之前将所有待处理的日志条目写入日志文件。当然,在某些情况下,我们的日志条目将不会被写入。例如,如果机器断电。如果您担心其中的一些极端情况,则可能需要考虑同步日志记录。

Clearcove.Logging通过使用单个System.Thread.Timer实例实现异步日志写入。线程计时器上的时间段未设置,因此计时器将仅触发一次。将所有未决的日志条目成功写入日志文件后,计时器将重置为在下一个间隔触发。此行为将类似于在计时器上设置一个周期,但是会在间隔被延迟的情况下防止多次触发计时器。

最后,使用对File.AppendAllText的简单调用将日志条目写入文件。此调用可能不是对日志文件进行多次写操作的最有效方法,但其选择是基于保持代码尽可能简单的愿望。

所以你有它。一个非常简单的日志记录实现,完全能够满足大多数应用程序的日志记录需求。它对我很有用,可以解决我所有的日志记录问题,同时将依存关系降到最低。

未来的工作

拥有一个简单的记录器的好处之一是它易于理解,可以快速进行定制以满足您的需求。 示例包括回滚日志文件,同步日志记录,外部配置等。这些功能的实现留给读者练习。 玩得开心!

此记录器实现的一个很大的缺点是它仅是.NET。 我计划很快发布此日志记录库的Java实现。

另外,请注意,某些CodeProject用户可能会在下面发布增强功能。 我将尝试合并那些不会增加复杂性的更改,但是如果您发现此记录器不能完全满足您的需求,那么可能值得阅读下面的评论。

兴趣点

使我陷入困境的一件事是对简化软件许可的渴望。 我一直在努力寻找免费的方式来免费释放Clearcove.Logger,而又不会增加许可的复杂性。 根据我的研究,我相信Ms-PL是许可度最大的许可证。 它简单,易于阅读和理解,并且重要的是要求您的二进制分发版应“在符合此许可证的许可证下”发布。 在我看来,此声明易于解释,可为您提供极大的灵活性。 我当然打算尽可能地免费提供该软件,同时仍然为您提供应关注的保护。 如果您有更开放的许可证建议,请告诉我。

历史

  • 0.9-我在生产中使用了它的变体。 本文已进行了一些小的更改,但我认为它们不会引入新的错误。
  • 0.91-删除了字符串插值以使代码更易于编译。 语法更改为文章。
  • 0.92-根据建议已更新为使用ISO-8601日期格式。 对文章的小语法更改。
  • 0.93-修复了记录异常堆栈跟踪的问题。

许可证

本文以及所有相关的源代码和文件,均已获得Microsoft公共许可证(Ms-PL)的许可。

关于作者

Toby Patke Founder Clearcove Limited 英国 我是LogViewPlus的作者-一个专门用于解析和分析应用程序日志文件的日志文件读取器。

源代码

关于Clearcove.Logging日志库的实现代码Logger.cs为:

代码语言:javascript复制
using System;
using System.IO;
using System.Text;
using System.Threading;

namespace Clearcove.Logging
{
    public sealed class Logger
    {
        #region Log File Writing
        public static bool Listening { get; private set; }
        public static FileInfo TargetLogFile { get; private set; }
        public static DirectoryInfo TargetDirectory { get { return TargetLogFile?.Directory; } }

        public static bool LogToConsole = false;
        public static int BatchInterval = 1000;
        public static bool IgnoreDebug = false;

        private static readonly Timer Timer = new Timer(Tick);
        private static readonly StringBuilder LogQueue = new StringBuilder();
        
        public static void Start(FileInfo targetLogFile)
        {
            if (Listening)
                return;

            Listening = true;
            TargetLogFile = targetLogFile;
            VerifyTargetDirectory();

            Timer.Change(BatchInterval, Timeout.Infinite); // A one-off tick event that is reset every time.
        }

        private static void VerifyTargetDirectory()
        {
            if (TargetDirectory == null)
                throw new DirectoryNotFoundException("Target logging directory not found.");

            TargetDirectory.Refresh();
            if (!TargetDirectory.Exists)
                TargetDirectory.Create();
        }

        private static void Tick(object state)
        {
            try
            {
                var logMessage = "";
                lock (LogQueue)
                {
                    logMessage = LogQueue.ToString();
                    LogQueue.Length = 0;
                }

                if (string.IsNullOrEmpty(logMessage))
                    return;

                if (LogToConsole)
                    Console.Write(logMessage);

                VerifyTargetDirectory(); // File may be deleted after initialization.
                File.AppendAllText(TargetLogFile.FullName, logMessage);  
            }
            finally
            {
                if(Listening)
                    Timer.Change(BatchInterval, Timeout.Infinite); // Reset timer for next tick.
            }
        }

        public static void ShutDown()
        {
            if (!Listening)
                return;

            Listening = false;
            Timer.Dispose();
            Tick(null); // Flush.
        }
        #endregion

        public readonly string Name;
        public EventHandler<LogMessageInfo> LogMessageAdded;
        private bool _startedErrorShown = false;

        public const string DEBUG = "DEBUG";
        public const string INFO = "INFO";
        public const string WARN = "WARN";
        public const string ERROR = "ERROR";

        public Logger(Type t) : this(t.Name)
        {
        }

        public Logger(string name)
        {
            Name = name;
        }

        public void Debug(string message)
        {
            if (IgnoreDebug)
                return;

            Log(DEBUG, message);
        }

        public void Info(string message)
        {
            Log(INFO, message);
        }

        public void Warn(string message, Exception ex = null)
        {
            Log(WARN, message, ex);
        }

        public void Error(string message, Exception ex = null)
        {
            Log(ERROR, message, ex);
        }

        public void Log(string level, string message, Exception ex = null)
        {
            if (!CheckListening())
                return;

            if (ex != null)
                message  = string.Format("rn{0}rn{1}", ex.Message, ex.StackTrace);

            var info = new LogMessageInfo(level, Name, message);
            var msg = info.ToString();

            lock (LogQueue)
            {
                LogQueue.AppendLine(msg);
            }

            var evnt = LogMessageAdded;
            if(evnt != null)
                evnt.Invoke(this, info); // Block caller.
        }

        private bool CheckListening()
        {
            if (Listening)
                return true;

            if (!_startedErrorShown)
            {
                Console.WriteLine("Logging has not been started.");
                _startedErrorShown = true; // No need to excessively repeat this message.
            }

            return false;
        }
    }

    public sealed class LogMessageInfo : EventArgs
    {
        public readonly DateTime Timestamp;
        public readonly string ThreadId;
        public readonly string Level;
        public readonly string Logger;
        public readonly string Message;

        public bool IsError { get { return Logging.Logger.ERROR.Equals(Level, StringComparison.Ordinal); } }
        public bool IsWarning { get { return Logging.Logger.WARN.Equals(Level, StringComparison.Ordinal); } }
        public bool IsInformation { get { return Logging.Logger.INFO.Equals(Level, StringComparison.Ordinal); } }
        public bool IsDebug { get { return Logging.Logger.DEBUG.Equals(Level, StringComparison.Ordinal); } }

        public LogMessageInfo(string level, string logger, string message)
        {
            Timestamp = DateTime.UtcNow;
            var thread = Thread.CurrentThread;
            ThreadId = string.IsNullOrEmpty(thread.Name) ? thread.ManagedThreadId.ToString() : thread.Name;
            Level = level;
            Logger = logger;
            Message = message;
        }

        public override string ToString()
        {
            return string.Format("{0:yyyy/MM/dd HH:mm:ss.fff} {1} {2} {3} {4}", 
                Timestamp, ThreadId, Logger, Level, Message);
        }
    }
}

相关的测试代码如下:

代码语言:javascript复制
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Clearcove.Logging;

namespace SimpleLogger
{
    class Program
    {
        static void Main(string[] args)
        {
            var targetLogFile = new FileInfo("./MyApp.log");
            Console.WriteLine("Writing 1000 log entries to:rn"   targetLogFile.FullName);

            Logger.LogToConsole = true;  // Print log entries to console (optional).
            Logger.Start(targetLogFile); // Loggers will complains if you skip initialization
            try
            {
                Run(args);
            }
            finally
            {
                Logger.ShutDown(); // Removing this line may result in lost log entries.
            }

            Console.WriteLine("Done.");
        }

        private static void Run(string[] args)
        {
            var log = new Logger(typeof(Program)); // Create logger with class name.
            var tasks = new List<Task>();

            for (int i = 1; i <= 10; i  )
            {
                var threadId = i;
                log.Info("Starting thread: "   threadId);
                tasks.Add(Task.Factory.StartNew(() => LogMessages(threadId)));
            }

            Task.WaitAll(tasks.ToArray());
        }

        private static void LogMessages(int threadId)
        {
            var random = new Random();
            var log = new Logger("Thread_"   threadId); // Create logger from string.

            for (int i = 1; i < 100; i  )
            {
                log.Info("This is log message "   i);
                Thread.Sleep(random.Next(10, 100)); // sleep to simulate a more realistic execution.
            }
        }
    }
}

0 人点赞