Logentries.com and F#

I recently was working on a project that has a fair bit of legacy code.  One of the pieces of the project is an logging service whose interface is this:

1 public interface ILoggingRepository 2 { 3 void LogMessage(String message); 4 void LogException(String message, Exception exception); 5 }

 

There are 2 or 3 different implementations of the logging repository – one that covers the windows logs, one that writes to azure service bus, one that writes to nothing (and in-memory one used for testing).  I thought about using Logentries as place to write the messages to.  I created an account and set up my first log

image image

Note that the log also gets a token (a guid) that I will use to send messages to the log at the bottom of the page.

I then fired up visual studio and created a new FSharp project and added a reference from the CSharp project to the FSharp project.  I then added an associated unit test class to the existing unit test project:

image

I then went back to Logentries and read the api documentation about posting to the log here.   They suggested either log4net or NLog.  For no particular reason, I picked NLog.  I fired up Nuget and installed the Logentries.NLog package

image

I then read further down the documentation and yuck, there is tons of places where you have to add to the configuration file.  I am trying to maintain a clean separation of concerns in the app and this intertwines the working code with the .config file.  Also, the other implementations don’t use the .config so I would like to keep consistant there.  After bouncing around in the api for a bit, I went to stack overflow and asked if there was a way I could implement without the .config file.  Sure enough, the dev team was kind enough to answer.  I went ahead and implemented their code (after porting it from C#)  in my project like so:

1 namespace ChickenSoftware.LoggingExample.FS 2 3 open NLog 4 open System 5 open NLog.Targets 6 open NLog.Config; 7 open ChickenSoftware.LoggingExample 8 9 type LogEntriesLoggingRepository(logEntriesToken:string) = 10 let target = new LogentriesTarget() 11 let config = new LoggingConfiguration() 12 do target.Token <- logEntriesToken 13 do target.Ssl <- true 14 do target.Debug <- true 15 do target.Name <- "Logentries" 16 let layout = Layouts.Layout.FromString("${date:format=ddd MMM dd} ${time:format=HH:mm:ss} ${date:format=zzz yyyy} ${logger} : ${LEVEL}, ${message}") 17 do target.Layout <- layout 18 do target.HttpPut <- false 19 do config.AddTarget("Logentries2",target) 20 let loggingRule = new LoggingRule("*", LogLevel.Debug, target) 21 do LogManager.Configuration.AddTarget("targetName", target) 22 do LogManager.Configuration.LoggingRules.Add(loggingRule) 23 do LogManager.Configuration.Reload() |> ignore 24 let logger = LogManager.GetCurrentClassLogger() 25 26 interface ILoggingRepository with 27 member this.LogMessage(message) = 28 logger.Log(LogLevel.Warn, message) 29 member this.LogException(message, exn) = 30 logger.LogException(LogLevel.Error,message,exn)

I then went into the unit test and attempted to generate a log message:

1 public class LogEntriesLoggingRepositoryTests 2 { 3 ILoggingRepository _repository = null; 4 public LogEntriesLoggingRepositoryTests() 5 { 6 string logEntriesToken = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; 7 _repository = new LogEntriesLoggingRepository(logEntriesToken); 8 } 9 10 [TestMethod] 11 public void LogMessage_ReturnsExpected() 12 { 13 _repository.LogMessage("This is a test"); 14 15 } 16 }

Unfortunately, when I ran it, I got the following exception, even though I marked the .dlls to be copied

image image

So back to Nuget, where I added in the Logentries.NLog to the Tests project.  I feel really dirty by doing it:

image

I then ran the test again but I got this exception:

image

When I added a break to the code and stepped through, I found it was on the LogManager.Configuration.

image

Apparently, the only way out of this pickle is to add some basic entries to the .config file <sigh>:

1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 <configSections> 4 <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" /> 5 </configSections> 6 <runtime> 7 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> 8 <dependentAssembly> 9 <assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" /> 10 <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" /> 11 </dependentAssembly> 12 </assemblyBinding> 13 </runtime> 14 <nlog> 15 <extensions> 16 <add assembly="LogentriesNLog" /> 17 </extensions> 18 <targets> 19 <target name="logentries" type="Logentries" debug="true" httpPut="false" ssl="false" layout="${date:format=ddd MMM dd} ${time:format=HH:mm:ss} ${date:format=zzz yyyy} ${logger} : ${LEVEL}, ${message}" /> 20 </targets> 21 <rules> 22 <logger name="*" minLevel="Debug" appendTo="logentries" /> 23 </rules> 24 </nlog> 25 </configuration>

After I added it, the test ran green.

image

Alas, nothing was showing up in the log!

image

After some back and forth with the Logentries team, it became clear that the thread was terminating before the Logentries library had a chance to post it to the service.  This was proven by adding a Thread.Sleep to the test:

1 public void LogMessage_ReturnsExpected() 2 { 3 _repository.LogMessage("This is a test"); 4 Thread.Sleep(500); 5 6 }

image

So what to do?  The api does not have an async implementation so I can’t await it and if I leave that Thread.Sleep as is, the main thread will be blocked.  I decided to add an async implementation to the interface

1 public interface ILoggingRepository 2 { 3 void LogMessage(String message); 4 Task LogMessageAsync(String message); 5 void LogException(String message, Exception exception); 6 Task LogExceptionAsync(String message, Exception exception); 7 }

I then updated the repository like so:

1 interface ILoggingRepository with 2 member this.LogMessage(message) = 3 logger.Log(LogLevel.Warn, message) 4 member this.LogMessageAsync(message) = 5 Tasks.Task.Run(fun _ -> logger.Log(LogLevel.Warn, message) 6 Thread.Sleep(500)) 7 member this.LogException(message, exn) = 8 logger.LogException(LogLevel.Error,message,exn) 9 member this.LogExceptionAsync(message, exn) = 10 Tasks.Task.Run(fun _ -> logger.LogException(LogLevel.Error,message,exn) 11 Thread.Sleep(500))

And then I added an async unit test like so:

1 [TestMethod] 2 public void LogMessageAsync_ReturnsExpected() 3 { 4 var task = _repository.LogMessageAsync("This is an async test"); 5 task.Wait(); 6 }

And sure enough, green (note that the async test takes longer than 500MS) and the expected side-effect:

image 

image

So now another CSharp shop has some FSharp sprinkled into their code base.  Note the code actually used is slightly different b/c  the code as written will keep adding more and more targets, which is not what we want.

One Response to Logentries.com and F#

  1. Pingback: F# Weekly #5, 2015 | Sergey Tihon's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: