Monday, 28 July 2014

Несколько параллельных логов в log4net

Появилась необходимость вести несколько параллельных логов приложения.
Внутри приложения выполняются отдельные не связанные друг с другом задачи, генерирующие большой объём логов. Было решено разделить логи, и вести отдельныйлог для каждой из задач.

Для этого нужно программно создавать логгеры log4net, не полагаясь на конфигурацию, в которой сконфигурирован только один глобальный лог, не привязанный к задаче.

Настройки логирования для каждого нового лога было решено скопировать из глобального лога, изменив только имя файла. Класс TaskLogger инкапсулирует описанную функцинальность:

/// <summary>
    /// Class incapsulating a per-task logging functionality.
    /// </summary>
    internal static class TaskLogger
    {
        private static readonly IDictionary<intILog> loggers = 
            new Dictionary<intILog>();
        private static Hierarchy hier;
        private static readonly RollingFileAppender mainAppender;
 
        static TaskLogger()
        {
            hier = (Hierarchy)LogManager.GetRepository();
            mainAppender = hier.GetAppenders()[0] as RollingFileAppender;
        }
 
        /// <summary>
        /// Creates a logger if there in no logger for a task yet, 
        /// and puts it to cache.
        /// </summary>
        /// <param name="taskId"></param>
        /// <param name="message"></param>
        public static void Log(int taskId, string message)
        {
            if (!loggers.ContainsKey(taskId))
            {
                lock (loggers)
                {
                    if (!loggers.ContainsKey(taskId))
                    {
                        loggers.Add(taskId, CreatePerTaskLogger(taskId));
                    }
                }
            }
            loggers[taskId].Info(message);
        }
 
        /// <summary>
        /// Removes a logger from cache when it's not required.
        /// </summary>
        /// <param name="taskId"></param>
        public static void RemoveLogger(int taskId)
        {
            if (loggers.ContainsKey(taskId))
            {
                lock (loggers)
                {
                    if (loggers.ContainsKey(taskId))
                    {
                        loggers.Remove(taskId);
                    }
                }
            }
        }
 
        /// <summary>
        /// Creates an additional logger to log task events in a separate file.
        /// </summary>
        /// <param name="taskId">Task ID.</param>
        /// <returns>New Logger.</returns>
        private static ILog CreatePerTaskLogger(int taskId)
        {
            FileAppender appender = CreateAppender(mainAppender, taskId);
            Logger logger = new OurLogger(String.Format("Logger_{0}", taskId));
            logger.Level = Level.All;
            //logger.RemoveAllAppenders();
            logger.AddAppender(appender);
            logger.Hierarchy = hier;
            return new LogImpl(logger);
        }
 
        /// <summary>
        /// Creates a new appender for a new logger.
        /// </summary>
        /// <param name="source">Source appender to copy settings from.</param>
        /// <param name="taskId">Id of the task being logged.</param>
        /// <returns></returns>
        private static RollingFileAppender CreateAppender(RollingFileAppender source, int taskId)
        {
            RollingFileAppender result = new RollingFileAppender();
            result.AppendToFile = source.AppendToFile;
            result.Encoding = source.Encoding;
            result.ErrorHandler = source.ErrorHandler;
            string logsFolder = Path.GetDirectoryName(source.File);
            result.File = Path.Combine(logsFolder, String.Format("Task_{0}.log", taskId));
            result.ImmediateFlush = source.ImmediateFlush;
            result.Layout = source.Layout;
            result.LockingModel = new FileAppender.MinimalLock();
            result.Name = String.Format("Appender_{0}", taskId);
            result.SecurityContext = source.SecurityContext;
            result.CountDirection = source.CountDirection;
            result.DatePattern = source.DatePattern;
            result.DateTimeStrategy = source.DateTimeStrategy;
            result.MaxFileSize = source.MaxFileSize;
            result.MaxSizeRollBackups = source.MaxSizeRollBackups;
            result.MaximumFileSize = source.MaximumFileSize;
            result.PreserveLogFileNameExtension = source.PreserveLogFileNameExtension;
            result.RollingStyle = source.RollingStyle;
            result.StaticLogFileName = source.StaticLogFileName;
            return result;
        }
    }

Изменения кэша аппендеров защищены оператором lock. После завершения (удаления) задачи необходимо удалить аппендер из кэша самостоятельно. Следующий тестовый код использует класс, запуская новые потоки в качестве задач: 

public static class Program
    {
        public static void Main(string[] args)
        {
            GlobalLogger.Log("Starting tasks.");
            new Thread(Run).Start();
            new Thread(Run).Start();
            Run();
            GlobalLogger.Log("Finished logging messages from tasks.");
            GlobalLogger.Log("A last message for task!");
        }
 
        private static void Run()
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            log4net.GlobalContext.Properties["ThreadId"] = threadId;
            Thread.Sleep(threadId * threadId);
            TaskLogger.Log(threadId, String.Format("Hello from Task {0}!", threadId));
            GlobalLogger.Log(String.Format("Global message from task {0}", threadId));
            TaskLogger.RemoveLogger(threadId);
        }
    }

Пара вспомогательных классов, используемых в коде: глобальный логгер для событий, не привязанных к конкретной задаче

internal static class GlobalLogger
    {
        private static readonly ILog CommonLogger;
 
        static GlobalLogger()
        {
            XmlConfigurator.Configure();
            CommonLogger = LogManager.GetLogger("TestLogger");
        }
 
        public static void Log(string message)
        {
            CommonLogger.Info(message);
        }
    }

...и наследник логфонетовского логгера:

internal class OurLogger : Logger
    {
        public OurLogger(string name)
            : base(name)
        {
        }
    }