Here is my example of async version of FluentFTP using log4net with TextBoxAppender which I already described here

For this example I have added following NuGet packages:

FluentFTP

FluentFTP.Logging

log4net

Microsoft.Extensions.Configuration.Json - this package I needed because user name and pass for FTP I have saved in JSON configuration file.

In short code looks like this:

Log4NetLogger log4NetLogger = new Log4NetLogger(log);
AsyncFtpClient client = new AsyncFtpClient(host, user, pass);
client.ValidateCertificate += OnValidateCertificate;
client.Logger = new FtpLogAdapter(log4NetLogger);
await client.AutoConnect();
await client.UploadFile(fileName, "/public_html/kmlTestDelete/test.kml");

private void OnValidateCertificate(BaseFtpClient control, FtpSslValidationEventArgs e)
{
	e.Accept = true;
}
The method OnValidateCertificate I am using to accept any certificate, this part of code I took from here

Class Log4NetLogger looks like this:

using log4net;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace FluentFTPasyncExample;

public class Log4NetLogger : ILogger
{
    private readonly ILog _log;

    public Log4NetLogger(ILog log)
    {
        _log = log;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        string message = $"{formatter(state, exception)} {exception}";
        if (!string.IsNullOrEmpty(message))
        {
            switch (logLevel)
            {
                case LogLevel.Critical:
                    _log.Fatal(message);
                    break;
                case LogLevel.Debug:
                case LogLevel.Trace:
                    _log.Debug(message);
                    break;
                case LogLevel.Error:
                    _log.Error(message);
                    break;
                case LogLevel.Information:
                    _log.Info(message);
                    break;
                case LogLevel.Warning:
                    _log.Warn(message);
                    break;
                default:
                    _log.Warn($"Encountered unknown log level {logLevel}, writing out as Info.");
                    _log.Info(message, exception);
                    break;
            }
        }
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        switch (logLevel)
        {
            case LogLevel.Critical:
                return _log.IsFatalEnabled;
            case LogLevel.Debug:
            case LogLevel.Trace:
                return _log.IsDebugEnabled;
            case LogLevel.Error:
                return _log.IsErrorEnabled;
            case LogLevel.Information:
                return _log.IsInfoEnabled;
            case LogLevel.Warning:
                return _log.IsWarnEnabled;
            default:
                throw new ArgumentOutOfRangeException(nameof(logLevel));
        }
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return null!;
    }

}
Notice that I am injecting log4net in constructor.

Example download from here.