Please note: This blog is no longer active. My new blog is located at http://blog.timwheeler.io

Sunday, May 17, 2015

Using ELMAH with Couchbase NetClient 2.1

Originally I tried the nuget package for Elmah integration with Couchbase. However it was using an earlier version of the NetClient and I was having some trouble getting it going. After grabbing the code from git hub (https://github.com/couchbaselabs/elmah-couchbase)
 I managed to update it for the new SDK syntax.
I also needed a custom json converter for the NameValueCollection type due to a deserialization error I received.

Check out the converter here: NameValueCollection Json Converter


My web.config
   <sectionGroup name="couchbaseClients">
      <section name="couchbase"
               type="Couchbase.Configuration.Client.Providers.CouchbaseClientSection, Couchbase.NetClient"/>
    </sectionGroup>
  </configSections>
  <couchbaseClients>
    <couchbase useSsl="false">
      <servers>
        <add uri="http://localhost:8091/pools"></add>
      </servers>
      <buckets>
        <add name="default" useSsl="false">
          <connectionPool name="custom" maxSize="10" minSize="5"></connectionPool>
        </add>
      </buckets>
    </couchbase>
  </couchbaseClients>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" />
  </startup>

...
 <elmah>
    <errorLog type="MyNameSpace.CouchbaseErrorLog, MyAssembly" couchbaseConfigSection="couchbaseClients/couchbase"  />
  </elmah>

CouchBaseErrorLog.cs
public class CouchbaseErrorLog : ErrorLog
    {
        private static ClientConfiguration _client;
        private static Cluster _cluster;
        private readonly string _elmahBucketName = "default";

        /// <summary>
        ///     Initialize new instance of CouchbaseClient using either default config
        ///     or specified configuration name
        /// </summary>
        public CouchbaseErrorLog(IDictionary config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            if (_client == null)
            {
                if (config.Contains("couchbaseConfigSection"))
                {
                    _client = CreateCouchbaseClient(config["couchbaseConfigSection"] as string);
                }
                else
                {
                    _client = CreateCouchbaseClient();
                }

                if (config.Contains("applicationName"))
                {
                    ApplicationName = config["applicationName"] as string;
                }
                _cluster = new Cluster(_client);
                SetNameValueConverter();
                    //This fixes issue with NameValueCollection deserialization and the serverVaribles property.
            }
        }

        /// <summary>
        ///     Name displayed in ELMAH viewer
        /// </summary>
        public override string Name
        {
            get { return "Couchbase Server Error Log"; }
        }

        public static ClientConfiguration CreateCouchbaseClient(string couchbaseConfigSection = null)
        {
            if (couchbaseConfigSection != null)
            {
                var config = ConfigurationManager.GetSection(couchbaseConfigSection) as CouchbaseClientSection;
                if (config == null)
                    throw new ArgumentException("Couchbase config section " + couchbaseConfigSection + " not found.");

                return new ClientConfiguration(config);
            }

            return new ClientConfiguration();
        }

        private static void SetNameValueConverter()
        {
            var settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                TypeNameHandling = TypeNameHandling.All,
                Formatting = Formatting.Indented
            };
            settings.Converters.Add(new NameValueCollectionConverter());
            JsonConvert.DefaultSettings = () => settings;
        }

        /// <summary>
        ///     Get error log entry by id
        /// </summary>
        public override ErrorLogEntry GetError(string id)
        {
            using (var bucket = _cluster.OpenBucket(_client.BucketConfigs[_elmahBucketName].BucketName))
            {
                return GetError(bucket, id);
            }
        }

        private ErrorLogEntry GetError(IBucket bucket, string id)
        {
            if (string.IsNullOrEmpty(id))
            {
                throw new ArgumentNullException("id");
            }
            try
            {
                Guid.Parse(id);
            }
            catch (FormatException e)
            {
                throw new ArgumentException(e.Message, "id", e);
            }
            var document = bucket.Get<string>(id);
            if (document.Success)
            {
                var error = JsonConvert.DeserializeObject<Error>(document.Value);
                return new ErrorLogEntry(this, id, error);
            }
            return null;
        }

        /// <summary>
        ///     Get list of errors for view in Elmah web viewer
        /// </summary>
        public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
        {
            if (pageIndex < 0)
                throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null);

            if (pageSize < 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, null);

            var skip = pageSize*pageIndex;

            //this is NOT the most efficient way to page in Couchbase/CouchDB, but is necessary because
            //there is no way to keep state of the startkey between requests
            //see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-querying-pagination.html
            //for more information
            using (var bucket = _cluster.OpenBucket(_client.BucketConfigs[_elmahBucketName].BucketName))
            {
                var query = bucket.CreateQuery("errors", "by_date", false).Desc().Skip(skip).Limit(pageSize);

                var view = bucket.Query<dynamic>(query);

                foreach (var item in view.Rows)
                {
                    var errorLogEntry = GetError(bucket, item.Id);
                    if (errorLogEntry != null)
                    {
                        errorEntryList.Add(errorLogEntry);
                    }
                }

                return view.Rows.Count();
            }
        }

        /// <summary>
        ///     Log an error
        /// </summary>
        public override string Log(Error error)
        {
            if (error == null)
                throw new ArgumentNullException("error");

            var id = Guid.NewGuid().ToString();
            using (var bucket = _cluster.OpenBucket(_client.BucketConfigs[_elmahBucketName].BucketName))
            {
                // var errorJson = JsonConvert.SerializeObject(error);
                var errorJson = JsonConvert.SerializeObject(error);
                var result = bucket.Insert(id, errorJson);
#if DEBUG
                if (!result.Success && Debugger.IsAttached)
                {
                    Debugger.Break();
                }
#endif
            }
            return id;
        }
    }
View
One last thing, I need to modify the couchbase view.
//view code for Elmah viewer. Must be named "by_date" and located in design document named "errors" without the quotes.
function (doc) {
if (doc.exception) {
emit(doc.time, null);
}
}