Организация записи в файл

284
22 сентября 2017, 17:08

Мой статический класс Logger пишет лог в текстовый файл. Файл лога новый каждый день и на каждого пользователя.

Возможная проблема: файл будет занят, запись не пройдет.

Вопрос: Как организовать запись. Чтобы было быстро и записи не пропадали если файл был занят.

Answer 1

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

Файловый поток при этом не должен постоянно открываться и закрываться, т. к. это медленные операции. Отрываем его один раз и дальше просто используем.

Чтобы запись в лог отрабатывала максимально быстро, можно поступить так: пишем не сразу в файл, а в промежуточную потокобезопасную очередь. Отдельная задача выгребает сообщения из очереди и пишет в файл (БД, посылает по сети).

internal static class Logger
{
    private static BlockingCollection<string> _blockingCollection;
    private static string _filename = "log.txt";
    private static Task _task;
    static Logger()
    {
        _blockingCollection = new BlockingCollection<string>();
        _task = Task.Factory.StartNew(() =>
        {
            using (var streamWriter = new StreamWriter(_filename, true, Encoding.UTF8))
            {
                streamWriter.AutoFlush = true;
                foreach (var s in _blockingCollection.GetConsumingEnumerable())
                    streamWriter.WriteLine(s);
            }
        },
        TaskCreationOptions.LongRunning);
    }
    public static void WriteLog(string action, int errorCode, string errorDiscription)
    {
        _blockingCollection.Add($"{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff")} действие: {action}, код: {errorCode.ToString()}, описание: { errorDiscription} ");
    }
    public static void Flush()
    {
        _blockingCollection.CompleteAdding();
        _task.Wait();
    }
}

Здесь в статическом конструкторе логгера создаётся потокобезопасная коллекция, в основе которой по умолчанию лежит очередь (что нам и нужно). Здесь же запускаем Task. Открываем файл на запись.

Метод WriteLog отрабатывает максимально быстро - он всего лишь кладёт сообщение в очередь.

Задача будет ожидать в цикле foreach на методе GetConsumingEnumerable: как только появляются новые данные - он будет выполняться. Выход из цикла произойдёт только после вызова метода CompleteAdding. Для этого предусмотрен метод Flush, который желательно вызвать при завершении программы. При этом блокирующая коллекция получит сигнал о завершении поступления данных. После чего закроется файл и завершится задача.

Но это не обязательно: обратите внимание на AutoFlush = true - это не даст потерять данные при неожиданном закрытии программы (краше кода).

Answer 2

Можно использовать примитивы синхронизации. Например lock - она позволяет ограничить доступ к разделяемому ресурсу только одному потоку.

Пример:

object obj;
string filePath = "c:/text.txt";
// Здесь секция разделяемого ресурса блокируется в случае если в этот блок 
// попадает поток. Остальные потоки остаются в очереди
lock(obj)
{
    using(FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write) 
    { 
        // Ну а здесь код записи данных в файл
    }
}
Answer 3

Запись производится в файл с нескольких приложений при одновременной попытки записи. Если нет доступа записи в файл, ожидает 100 мс, и так по циклу. Быстроты как хотелось не будет, зато есть гарантия записи.

// Упрощеный вариант с отображением в консоли
internal static class LoggerAddin
{
    public static string AppPath { get; set; } = @"C:\Windows\";// Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RBSOFT", "AddInRBSOFTDeviceServiceKKT");
    public static string CurrentAppPath
    {
        get
        {
            return Path.Combine(
                AppPath,
                "Logs",
                DateTime.Now.Year.ToString(),
                DateTime.Now.Month.ToString(),
                DateTime.Now.Day.ToString()
            );
        }
    }
    public static void WriteLog(string action, int errorCod, string errorDiscription)
    {
        string fileName = Path.Combine(CurrentAppPath, "Log-" + DateTime.Today.ToString("yy-MM-dd") + ".txt");
        if (!Directory.Exists(CurrentAppPath))
            Directory.CreateDirectory(CurrentAppPath);
        bool repit = true;
       // Console.WriteLine(DateTime.Now.ToString());
        while (repit)
        {
            try
            {
                using (var stream = new StreamWriter(fileName, true, Encoding.UTF8))
                {
                    stream.WriteLine($"{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff")} действие: {action}, код: {errorCod.ToString()}, описание: { errorDiscription} ");                     
                }
                repit = false;
               // Console.WriteLine("Ready");
            }
            catch (IOException)
            {
                Console.WriteLine("IOException");
            }
            catch (UnauthorizedAccessException )
            {
                //Console.WriteLine("UnauthorizedAccessException");
                repit = false;
            }
            catch (Exception ex)
            {
                //Console.WriteLine(ex.Message);
                repit = false;
            }
            if (repit)
            {
                Thread.Sleep(100);
            }
        }
        //Console.WriteLine(DateTime.Now.ToString());
    }
}
READ ALSO
RdKafka Avro Consumer

RdKafka Avro Consumer

В клиенте RdKafka мне необходимо прочитать Avro сообщенияВ консоли я дописывал свойство:

207
asp.net mvc 5 C#

asp.net mvc 5 C#

в этом коде пишут ошибку

283
MSSQL Server permissions + Entity Framework [Необходимые разрешения]

MSSQL Server permissions + Entity Framework [Необходимые разрешения]

Ранее имел дело с MySQL сервером и понятной установкой разрешений для аккаунтов и объектовСейчас разбираюсь с MSSQL сервером и Entity Framework

193