Разделить файл журнала и каталог для каждого клиента и дату

У меня есть служба Windows TCP, к которой подключено много устройств, и у клиента может быть одно или несколько устройств.

Требования:

Отдельная папка для каждого клиента с отдельным файлом журнала для каждого устройства.

так что-то вроде этого:

/MyService/25-04-2016/
    Client 1/
        Device1.txt 
        Device2.txt 
        Device3.txt 

    Client 2/
        Device1.txt 
        Device2.txt 
        Device3.txt 

Теперь я не использовал стороннюю библиотеку типа log4net или NLog, у меня есть класс, который обрабатывает это.

public class xPTLogger : IDisposable
{
    private static object fileLocker = new object();

    private readonly string _logFileName;
    private readonly string _logFilesLocation;
    private readonly int _clientId;

    public xPTLogger() : this("General") { }

    public xPTLogger(string logFileName)
    {
        _clientId = -1;
        _logFileName = logFileName;
        _logFilesLocation = SharedConstants.LogFilesLocation; // D:/LogFiles/
    }

    public xPTLogger(string logFileName, int companyId)
    {
        _clientId = companyId;
        _logFileName = logFileName;
        _logFilesLocation = SharedConstants.LogFilesLocation;
    }

    public void LogMessage(MessageType messageType, string message)
    {
        LogMessage(messageType, message, _logFileName);
    }

    public void LogExceptionMessage(string message, Exception innerException, string stackTrace)
    {
        var exceptionMessage = innerException != null
                ? string.Format("Exception: [{0}], Inner: [{1}], Stack Trace: [{2}]", message, innerException.Message, stackTrace)
                : string.Format("Exception: [{0}], Stack Trace: [{1}]", message, stackTrace);

        LogMessage(MessageType.Error, exceptionMessage, "Exceptions");
    }

    public void LogMessage(MessageType messageType, string message, string logFileName)
    {
        var dateTime = DateTime.UtcNow.ToString("dd-MMM-yyyy");

        var logFilesLocation = string.Format("{0}{1}\\", _logFilesLocation, dateTime);

        if (_clientId > -1) { logFilesLocation = string.Format("{0}{1}\\{2}\\", _logFilesLocation, dateTime, _clientId); }


        var fullLogFile = string.IsNullOrEmpty(logFileName) ? "GeneralLog.txt" : string.Format("{0}.txt", logFileName);


        var msg = string.Format("{0} | {1} | {2}\r\n", DateTime.UtcNow.ToString("dd-MMM-yyyy HH:mm:ss"), messageType, message);

        fullLogFile = GenerateLogFilePath(logFilesLocation, fullLogFile);

        LogToFile(fullLogFile, msg);
    }

    private string GenerateLogFilePath(string objectLogDirectory, string objectLogFileName)
    {
        if (string.IsNullOrEmpty(objectLogDirectory))
            throw new ArgumentNullException(string.Format("{0} location cannot be null or empty", "objectLogDirectory"));
        if (string.IsNullOrEmpty(objectLogFileName))
            throw new ArgumentNullException(string.Format("{0} cannot be null or empty", "objectLogFileName"));

        if (!Directory.Exists(objectLogDirectory))
            Directory.CreateDirectory(objectLogDirectory);
        string logFilePath = string.Format("{0}\\{1}", objectLogDirectory, objectLogFileName);
        return logFilePath;
    }

    private void LogToFile(string logFilePath, string message)
    {
        if (!File.Exists(logFilePath))
        {
            File.WriteAllText(logFilePath, message);
        }
        else
        {
            lock (fileLocker)
            {
                File.AppendAllText(logFilePath, message);
            }
        }
    }

    public void Dispose()
    {
        fileLocker = new object();
    }
}

И тогда я могу использовать его вот так:

 var _logger = new xPTLogger("DeviceId", 12);

 _logger.LogMessage(MessageType.Info, string.Format("Information Message = [{0}]", 1));

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

25-Apr-2016 13:07:00 | Error | Exception: The process cannot access the file 'D:\LogFiles\25-Apr-2016\0\LogFile.txt' because it is being used by another process.

Иногда это приводит к сбою службы.

Как заставить класс Logger работать в многопоточных сервисах?

ИЗМЕНИТЬ

Изменения в классе регистратора

public class xPTLogger : IDisposable
{
    private object fileLocker = new object();

    private readonly string _logFileName;
    private readonly string _logFilesLocation;
    private readonly int _companyId;

    public xPTLogger() : this("General") { }

    public xPTLogger(string logFileName)
    {
        _companyId = -1;
        _logFileName = logFileName;
        _logFilesLocation = SharedConstants.LogFilesLocation; // "D:\\MyLogs";
    }

    public xPTLogger(string logFileName, int companyId)
    {
        _companyId = companyId;
        _logFileName = logFileName;
        _logFilesLocation = SharedConstants.LogFilesLocation;
    }

    public void LogMessage(MessageType messageType, string message)
    {
        LogMessage(messageType, message, _logFileName);
    }

    public void LogExceptionMessage(string message, Exception innerException, string stackTrace)
    {
        var exceptionMessage = innerException != null
                ? string.Format("Exception: [{0}], Inner: [{1}], Stack Trace: [{2}]", message, innerException.Message, stackTrace)
                : string.Format("Exception: [{0}], Stack Trace: [{1}]", message, stackTrace);

        LogMessage(MessageType.Error, exceptionMessage, "Exceptions");
    }

    public void LogMessage(MessageType messageType, string message, string logFileName)
    {
        if (messageType == MessageType.Debug)
        {
            if (!SharedConstants.EnableDebugLog)
                return;
        }

        var dateTime = DateTime.UtcNow.ToString("dd-MMM-yyyy");

        var logFilesLocation = string.Format("{0}{1}\\", _logFilesLocation, dateTime);

        if (_companyId > -1) { logFilesLocation = string.Format("{0}{1}\\{2}\\", _logFilesLocation, dateTime, _companyId); }


        var fullLogFile = string.IsNullOrEmpty(logFileName) ? "GeneralLog.txt" : string.Format("{0}.txt", logFileName);


        var msg = string.Format("{0} | {1} | {2}\r\n", DateTime.UtcNow.ToString("dd-MMM-yyyy HH:mm:ss"), messageType, message);

        fullLogFile = GenerateLogFilePath(logFilesLocation, fullLogFile);

        LogToFile(fullLogFile, msg);
    }

    private string GenerateLogFilePath(string objectLogDirectory, string objectLogFileName)
    {
        if (string.IsNullOrEmpty(objectLogDirectory))
            throw new ArgumentNullException(string.Format("{0} location cannot be null or empty", "objectLogDirectory"));
        if (string.IsNullOrEmpty(objectLogFileName))
            throw new ArgumentNullException(string.Format("{0} cannot be null or empty", "objectLogFileName"));

        if (!Directory.Exists(objectLogDirectory))
            Directory.CreateDirectory(objectLogDirectory);
        string logFilePath = string.Format("{0}\\{1}", objectLogDirectory, objectLogFileName);
        return logFilePath;
    }

    private void LogToFile(string logFilePath, string message)
    {
        lock (fileLocker)
        {
            try
            {
                if (!File.Exists(logFilePath))
                {
                    File.WriteAllText(logFilePath, message);
                }
                else
                {
                    File.AppendAllText(logFilePath, message);
                }
            }
            catch (Exception ex)
            {
                var exceptionMessage = ex.InnerException != null
                                ? string.Format("Exception: [{0}], Inner: [{1}], Stack Trace: [{2}]", ex.Message, ex.InnerException.Message, ex.StackTrace)
                                : string.Format("Exception: [{0}], Stack Trace: [{1}]", ex.Message, ex.StackTrace);

                var logFilesLocation = string.Format("{0}{1}\\", _logFilesLocation, DateTime.UtcNow.ToString("dd-MMM-yyyy"));

                var logFile = GenerateLogFilePath(logFilesLocation, "FileAccessExceptions.txt");

                try
                {
                    if (!File.Exists(logFile))
                    {
                        File.WriteAllText(logFile, exceptionMessage);
                    }
                    else
                    {
                        File.AppendAllText(logFile, exceptionMessage);
                    }
                }
                catch (Exception) { }
            }

        }
    }

    public void Dispose()
    {
        //fileLocker = new object();
        //_logFileName = null;
        //_logFilesLocation = null;
        //_companyId = null;
    }
}

Ответ 1

Если вы не хотите использовать существующие решения, разумный подход к обработке многопоточных записей в вашем журнале - это использование очереди. Вот эскиз:

public class LogQueue : IDisposable {
    private static readonly Lazy<LogQueue> _isntance = new Lazy<LogQueue>(CreateInstance, true);
    private Thread _thread;
    private readonly BlockingCollection<LogItem> _queue = new BlockingCollection<LogItem>(new ConcurrentQueue<LogItem>());

    private static LogQueue CreateInstance() {
        var queue = new LogQueue();
        queue.Start();
        return queue;
    }

    public static LogQueue Instance => _isntance.Value;

    public void QueueItem(LogItem item) {
        _queue.Add(item);
    }

    public void Dispose() {
        _queue.CompleteAdding();
        // wait here until all pending messages are written
        _thread.Join();
    }

    private void Start() {
        _thread = new Thread(ConsumeQueue) {
            IsBackground = true
        };
        _thread.Start();
    }

    private void ConsumeQueue() {
        foreach (var item in _queue.GetConsumingEnumerable()) {
            try {
                // append to your item.TargetFile here                    
            }
            catch (Exception ex) {
                // do something or ignore
            }
        }
    }
}

public class LogItem {
    public string TargetFile { get; set; }
    public string Message { get; set; }
    public MessageType MessageType { get; set; }
}

Затем в вашем классе журнала:

private void LogToFile(string logFilePath, string message) {
    LogQueue.Instance.QueueItem(new LogItem() {
        TargetFile = logFilePath,
        Message = message
    });
}

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

Недостаток - вы можете потерять некоторые сообщения в случае сбоя процесса (не думайте, что это действительно проблема, но все еще упоминайте об этом), и вы потребляете отдельный поток для асинхронного журнала. Когда есть один поток, это не проблема, но если вы создаете один поток на устройство, это может быть (хотя нет необходимости - просто используйте одиночную очередь, если вы действительно не пишете много сообщений в секунду).

Ответ 2

Хотя это, вероятно, не самое элегантное решение, вы можете создать встроенную логику повтора. Например:

int retries = 0;
while(retries <= 3){
    try{
        var _logger = new xPTLogger("DeviceId", 12);

        _logger.LogMessage(MessageType.Info, string.Format("Information Message = [{0}]", 1));
        break;
    }
    catch (Exception ex){
        //Console.WriteLine(ex.Message);
        retries++;
    }
}

Кроме того, я написал этот код только сейчас, не тестируя его, поэтому, если какая-то глупая ошибка в нем прощает меня. Но довольно просто он попытается записать в журнал столько раз, сколько вы установили в строке "while". Вы даже можете добавить оператор сна в блок catch, если считаете, что это того стоит.

У меня нет опыта работы с Log4Net или NLog, поэтому комментариев нет. Может быть, есть сладкое решение через один из этих пакетов. Удачи!