Collecting Information and Logging in the .NET SDK with Couchbase Server

    +
    Couchbase .NET SDK3 relies on the Microsoft.Extensions.Logging API and specifically on the Microsoft.Extensions.Logging.ILoggerFactory interface to support a wide variety of compatible 3rd party logging implementations such as Serilog, NLog, and others. Further details can be found in the Microsoft documentation for Logging and Logging Providers.

    Couchbase .NET SDK Logging Basics

    The Couchbase .NET SDK allows for any logging provider to be plugged in as long as it implements the Microsoft.Extensions.Logging.ILoggerFactory interface. It does this via Method injection in the ClusterOptions class which is passed to the Cluster object when Cluster.ConnectAsync is called.

    Note: In most of the examples below, the default .NET logging providers are used,  — otherwise, we are using the Serilog one-liner logging package:

    Install-Package Serilog.Extensions.Logging.File -Version 2.0.0
    <PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" />

    Logging in a .NET Core Non-Host Application

    If you are not using a Host, and not using Dependency Injection, you can still configure a ILoggerFactory using the SDK. In this example we have a dependency on Serilog single file logging:

    Install-Package Serilog.Extensions.Logging.File -Version 2.0.0

    We are using a .NET Core Console Application, and simply instantiate the ILoggingProvider:

    IServiceCollection serviceCollection = new ServiceCollection();
    serviceCollection.AddLogging(builder => builder
        .AddFilter(level => level >= LogLevel.Debug)
    );
    var loggerFactory = serviceCollection.BuildServiceProvider().GetService<ILoggerFactory>();
    loggerFactory.AddFile("Logs/myapp-{Date}.txt", LogLevel.Debug);
    
    var clusterOptions = new ClusterOptions().
        WithCredentials("Administrator", "password").
        WithLogging(loggerFactory);
    
    var cluster = Cluster.ConnectAsync("couchbase://10.112.211.101", clusterOptions).
        GetAwaiter().
        GetResult();

    We then call ClusterOptions.WithLogging and pass in the ILoggerProvider that we instatiated, and connect to the Cluster as usual.

    Logging in applications targeting the .NET Core Generic Host

    For .NET Core 3.0 and greater applications, the Generic Host is used to configure logging.

    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureLogging(logging =>
            {
                logging.ClearProviders();
                logging.AddFile("Logs/myapp-{Date}.txt", LogLevel.Debug);
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
                services.AddCouchbase(hostContext.Configuration.GetSection("couchbase"));
                services.AddCouchbaseBucket<INamedBucketProvider>("default");
            });

    In the example above, Host.ConfigureLogging is called in the CreateHostBuilder method where we clear any previously registered logging providers (.NET registers a couple by default). Then AddConsole is called to configure logging to the console which will be visible when you run the application.

    Note that because we are using Couchbase Dependency Injection, we do not have to explicitly call ClusterOptions.WithLogging(ILoggerFactory factory) and pass in the ILoggerFactory; this will be handled by Couchbase Dependency Injection internally.

    Logging in applications targeting the .NET Full Framework

    Targeting logging in .NET Full applications is a bit more challenging in that there is no DI container, thus the standard way of injecting the logging dependencies won’t work. Furthermore, since there is no support for the Microsoft.Extensions.Logging interfaces, we’ll need to create our own wrappers. One for a Log4Net ILogger implementation:

    public class Log4NetLogger : ILogger
    {
        private readonly ILog _log;
    
        public Log4NetLogger(string name, XmlElement xmlElement)
        {
            var loggerRepository = log4net.LogManager.CreateRepository(
                Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
            _log = LogManager.GetLogger(loggerRepository.Name, name);
            log4net.Config.XmlConfigurator.Configure(loggerRepository, xmlElement);
        }
        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }
    
        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 void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }
    
            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }
            string message = null;
            message = formatter(state, exception);
            if (!string.IsNullOrEmpty(message) || exception != null)
            {
                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;
                }
            }
        }
    }

    Then a Microsoft.Extensions.Logging.ILoggingProvider implementation:

    public class Log4NetProvider : ILoggerProvider
    {
        private readonly string _log4NetConfigFile;
        private readonly ConcurrentDictionary<string, Log4NetLogger> _loggers =
            new ConcurrentDictionary<string, Log4NetLogger>();
        public Log4NetProvider(string log4NetConfigFile)
        {
            _log4NetConfigFile = log4NetConfigFile;
        }
    
        public ILogger CreateLogger(string categoryName)
        {
            return _loggers.GetOrAdd(categoryName, CreateLoggerImplementation);
        }
    
        public void Dispose()
        {
            _loggers.Clear();
        }
        private Log4NetLogger CreateLoggerImplementation(string name)
        {
            return new Log4NetLogger(name, ParseLog4NetConfigFile(_log4NetConfigFile));
        }
    
        private static XmlElement ParseLog4NetConfigFile(string filename)
        {
            var log4NetConfig = new XmlDocument();
            log4NetConfig.Load(File.OpenRead(filename));
            return log4NetConfig["log4net"];
        }
    }

    And some extensions to make it easier to use:

    public static class Log4netExtensions
    {
        public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile)
        {
            factory.AddProvider(new Log4NetProvider(log4NetConfigFile));
            return factory;
        }
    
        public static ILoggerFactory AddLog4Net(this ILoggerFactory factory)
        {
            factory.AddProvider(new Log4NetProvider("log4net.config"));
            return factory;
        }
    }

    Finally, we put it all together:

    ILoggerFactory factory = new LoggerFactory();
    factory.AddLog4Net("log4net.config");
    
    var ipAddressList = new List<string> { "10.112.205.101" };
    var config = new ClusterOptions()
        .WithConnectionString(string.Concat("http://", string.Join(", ", ipAddressList)))
        .WithCredentials("Administrator", "password")
        .WithBuckets("default")
        .WithLogging(factory); //<= Need to add the ILoggerFactory via DI
    
    config.KvConnectTimeout = TimeSpan.FromMilliseconds(12000);
    
    var cluster = await Cluster.ConnectAsync(config);
    var bucket = await cluster.BucketAsync("default");
    Console.ReadKey();

    Note that we suggest .NET Core or .NET 5.0 over using SDK3 in .NET Full Framework apps, as the integration is much more difficult.