C# log4net AsyncRollingFileAppender

Date: 2026-02-05
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using log4net.Appender;
using log4net.Core;
using log4net.Layout;


namespace Logging
{
    public sealed class AsyncRollingFileAppender : AppenderSkeleton, IFlushable
    {
        private BlockingCollection<string> _queue;
        private Thread _worker;
        private CancellationTokenSource _cts;

        private FileStream _stream;
        private StreamWriter _writer;

        private long _currentSize;

        private string _fullPath = null;
        private string _directory = null;

        public string FileName { get; set; }
        public int MaxSizeRollBackups { get; set; } = 5;
        public long MaximumFileSizeBytes { get; set; } = 20 * 1024 * 1024;
        public TimeSpan IdleFlushInterval { get; set; } = TimeSpan.FromSeconds(5);
        public int BufferSize { get; set; } = 10000;

        public override void ActivateOptions()
        {
            base.ActivateOptions();

            if (Layout == null)
                Layout = new PatternLayout("%date %-5level %message%newline");

            if (_stream != null)
            {
                OnClose();
            }

            _queue = new BlockingCollection<string>(BufferSize);
            _cts = new CancellationTokenSource();

            _fullPath = ResolveFilePath(FileName);
            _directory = Path.GetDirectoryName(_fullPath);
            Directory.CreateDirectory(_directory);

            OpenFile();

            _worker = new Thread(WorkerLoop)
            {
                IsBackground = true,
                Name = "AsyncRollingFileAppender"
            };
            _worker.Start();
        }

        protected override void Append(LoggingEvent loggingEvent)
        {
            loggingEvent.Fix = FixFlags.All;

            var rendered = RenderLoggingEvent(loggingEvent);
            _queue.TryAdd(rendered);
        }

        private void WorkerLoop()
        {
            try
            {
                while (!_cts.IsCancellationRequested)
                {
                    if (_queue.TryTake(out var line, IdleFlushInterval))
                    {
                        WriteInternal(line);
                    }
                    else
                    {
                        FlushInternal();
                    }
                }
            }
            catch
            {
                // logging mag nooit exceptions naar buiten gooien
            }
        }

        private void WriteInternal(string text)
        {
            var bytes = Encoding.UTF8.GetByteCount(text);

            if (_currentSize + bytes > MaximumFileSizeBytes)
                RollFile();

            _writer.Write(text);
            _currentSize += bytes;
        }

        private void FlushInternal()
        {
            _writer?.Flush();
            _stream?.Flush(true);
        }

        private static string ResolveFilePath(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName))
                throw new ArgumentException("File name must be set");

            // 1. Environment variables (%TEMP%, %APPDATA%, etc.)
            var expanded = Environment.ExpandEnvironmentVariables(fileName);

            // 2. Absolute path maken (relatief → base directory)
            var fullPath = Path.GetFullPath(expanded);

            // 3. Directory bepalen
            var directory = Path.GetDirectoryName(fullPath);

            // 4. Alleen directory aanmaken als die bestaat
            if (!string.IsNullOrEmpty(directory))
            {
                Directory.CreateDirectory(directory);
            }

            return fullPath;
        }

        private void OpenFile()
        {
            var fullPath = ResolveFilePath(FileName);
            Directory.CreateDirectory(Path.GetDirectoryName(fullPath));

            _stream = new FileStream(
                fullPath,
                FileMode.Append,
                FileAccess.Write,
                FileShare.Read,
                4096,
                FileOptions.SequentialScan);

            _writer = new StreamWriter(_stream, Encoding.UTF8)
            {
                AutoFlush = false
            };

            _currentSize = _stream.Length;
        }

        private void RollFile()
        {
            FlushInternal();
            _writer.Dispose();
            _stream.Dispose();

            var baseFileName = Path.GetFileName(_fullPath);

            for (int i = MaxSizeRollBackups - 1; i >= 1; i--)
            {
                var src = Path.Combine(_directory, $"{baseFileName}.{i}");
                var dst = Path.Combine(_directory, $"{baseFileName}.{i + 1}");
                if (File.Exists(src))
                {
                    if (File.Exists(dst)) File.Delete(dst);
                    File.Move(src, dst);
                }
            }

            if (MaxSizeRollBackups > 0)
            {
                var first = Path.Combine(_directory, $"{baseFileName}.1");
                if (File.Exists(first)) File.Delete(first);
                File.Move(_fullPath, first);
            }

            OpenFile();
        }

        protected override void OnClose()
        {
            Flush(5000);

            _cts.Cancel();
            _queue.CompleteAdding();
            _worker.Join(5000);

            _writer?.Dispose();
            _stream?.Dispose();

            base.OnClose();
        }

        bool IFlushable.Flush(int millisecondsTimeout)
        {
            var sw = Stopwatch.StartNew();
            while (_queue.Count > 0 && sw.ElapsedMilliseconds < millisecondsTimeout)
            {
                Thread.Sleep(10);
            }

            FlushInternal();
            return _queue.Count == 0;
        }
    }
}
100570cookie-checkC# log4net AsyncRollingFileAppender