diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..237d960
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,183 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# If using the old MSBuild-Integrated Package Restore, uncomment this:
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
diff --git a/README.md b/README.md
index 5f5aa71..2b73cd0 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,25 @@
A Serilog sink that writes events to the [NewRelic](https://newrelic.com) apm application.
-[![Package Logo](http://serilog.net/images/serilog-sink-seq-nuget.png)](http://nuget.org/packages/serilog.sinks.seq)
-
## Getting started
-To get started install the _Serilog.Sinks.Seq_ package from Visual Studio's _NuGet_ console:
+Install NewRelic NuGetPackage
+
+Configure NewRelic Settings:
-```powershell
-PM> Install-Package Serilog.Sinks.Seq
+```xml
+
+
+
+
+
```
Point the logger to NewRelic:
```csharp
Log.Logger = new LoggerConfiguration()
- .WriteTo.NewRelic()
+ .WriteTo.NewRelic(applicationName: "Serilog.Sinks.NewRelic.Sample")
.CreateLogger();
```
@@ -24,3 +28,19 @@ And use the Serilog logging methods to associate named properties with log event
```csharp
Log.Error("Failed to log on user {ContactId}", contactId);
```
+
+The sink also supports sending Serilog.Metrics to NewRelic although this requires a custom transaction in NewRelic See [here](https://docs.newrelic.com/docs/agents/net-agent/instrumentation/net-custom-instrumentation) and may turn out to be largely redundant!
+
+```csharp
+// Adding a custom transaction
+
+using (logger.BeginTimedOperation("Time a thread sleep for 2 seconds."))
+{
+ Thread.Sleep(1000);
+ using (logger.BeginTimedOperation("And inside we try a Task.Delay for 2 seconds."))
+ {
+ Task.Delay(2000).Wait();
+ }
+ Thread.Sleep(1000);
+}
+```
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic.Sample/App.config b/Serilog.Sinks.NewRelic.Sample/App.config
new file mode 100644
index 0000000..5351b5d
--- /dev/null
+++ b/Serilog.Sinks.NewRelic.Sample/App.config
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Serilog.Sinks.NewRelic.Sample/Program.cs b/Serilog.Sinks.NewRelic.Sample/Program.cs
new file mode 100644
index 0000000..ea81737
--- /dev/null
+++ b/Serilog.Sinks.NewRelic.Sample/Program.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Serilog.Enrichers;
+using Serilog.Events;
+
+namespace Serilog.Sinks.NewRelic.Sample
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ var logger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .WriteTo.ColoredConsole(
+ outputTemplate: "{Timestamp:HH:mm:ss} ({ThreadId}) [{Level}] {Message}{NewLine}{Exception}")
+ .WriteTo.Trace()
+ .WriteTo.NewRelic(applicationName: "Serilog.Sinks.NewRelic.Sample")
+ .Enrich.With(new ThreadIdEnricher(), new MachineNameEnricher())
+ .CreateLogger();
+
+ logger.Information("This is a simple information message {Property1}",100);
+
+ // Adding a custom transaction
+
+ using (logger.BeginTimedOperation("Time a thread sleep for 2 seconds."))
+ {
+ Thread.Sleep(1000);
+ using (logger.BeginTimedOperation("And inside we try a Task.Delay for 2 seconds."))
+ {
+ Task.Delay(2000).Wait();
+ }
+ Thread.Sleep(1000);
+ }
+
+ using (logger.BeginTimedOperation("Using a passed in identifier", "test-loop"))
+ {
+ // ReSharper disable once NotAccessedVariable
+ var a = "";
+ for (var i = 0; i < 1000; i++)
+ {
+ a += "b";
+ }
+ }
+
+ // Exceed a limit
+ using (logger.BeginTimedOperation("This should execute within 1 second.", null, LogEventLevel.Debug, TimeSpan.FromSeconds(1)))
+ {
+ Thread.Sleep(1100);
+ }
+
+ // Gauge
+
+ var queue = new Queue();
+ var gauge = logger.GaugeOperation("queue", "item(s)", () => queue.Count());
+
+ gauge.Write();
+
+ queue.Enqueue(20);
+
+ gauge.Write();
+
+ queue.Dequeue();
+
+ gauge.Write();
+
+ // Counter
+ var counter = logger.CountOperation("counter", "operation(s)", true, LogEventLevel.Debug, resolution: 2);
+ counter.Increment();
+ counter.Increment();
+ counter.Increment();
+ counter.Decrement();
+
+ // Throw Exception
+ try
+ {
+ throw new ApplicationException("This is an exception raised to test the New Relic API");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error whilst testing the Serilog.Sinks.NewRelic.Sample");
+ }
+
+
+
+ System.Console.WriteLine("Press a key to exit.");
+ System.Console.ReadKey(true);
+
+ }
+ }
+}
diff --git a/Serilog.Sinks.NewRelic.Sample/Properties/AssemblyInfo.cs b/Serilog.Sinks.NewRelic.Sample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..fb711c8
--- /dev/null
+++ b/Serilog.Sinks.NewRelic.Sample/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Serilog.Sinks.NewRelic.Sample")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Serilog.Sinks.NewRelic.Sample")]
+[assembly: AssemblyCopyright("Copyright © 2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("569471cf-2fb0-434b-9f8e-58784efb6f78")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Serilog.Sinks.NewRelic.Sample/Serilog.Sinks.NewRelic.Sample.csproj b/Serilog.Sinks.NewRelic.Sample/Serilog.Sinks.NewRelic.Sample.csproj
new file mode 100644
index 0000000..268019c
--- /dev/null
+++ b/Serilog.Sinks.NewRelic.Sample/Serilog.Sinks.NewRelic.Sample.csproj
@@ -0,0 +1,94 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {569471CF-2FB0-434B-9F8E-58784EFB6F78}
+ Exe
+ Properties
+ Serilog.Sinks.NewRelic.Sample
+ Serilog.Sinks.NewRelic.Sample
+ v4.6
+ 512
+ true
+
+
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\NewRelic.Agent.Api.5.8.28.0\lib\NewRelic.Api.Agent.dll
+ True
+
+
+ ..\packages\NRConfig.Tool.1.5.0.0\lib\NRConfig.dll
+ True
+
+
+ ..\packages\Serilog.1.5.12\lib\net45\Serilog.dll
+ True
+
+
+ ..\packages\Serilog.1.5.12\lib\net45\Serilog.FullNetFx.dll
+ True
+
+
+ ..\packages\SerilogMetrics.1.0.29\lib\net45\SerilogMetrics.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {f34ae60b-cfd5-464c-bf49-a9d8fa7ee424}
+ Serilog.Sinks.NewRelic
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic.Sample/packages.config b/Serilog.Sinks.NewRelic.Sample/packages.config
new file mode 100644
index 0000000..be5f075
--- /dev/null
+++ b/Serilog.Sinks.NewRelic.Sample/packages.config
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/NewRelicLoggerConfigurationExtensions.cs b/Serilog.Sinks.NewRelic/NewRelicLoggerConfigurationExtensions.cs
new file mode 100644
index 0000000..370df92
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/NewRelicLoggerConfigurationExtensions.cs
@@ -0,0 +1,45 @@
+using System;
+using Serilog.Configuration;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Sinks.NewRelic.Sinks.NewRelic;
+
+namespace Serilog.Sinks.NewRelic
+{
+ public static class NewRelicLoggerConfigurationExtensions
+ {
+ public static LoggerConfiguration NewRelic(
+ this LoggerSinkConfiguration loggerSinkConfiguration,
+ LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
+ int batchPostingLimit = NewRelicSink.DefaultBatchPostingLimit,
+ TimeSpan? period = null,
+ string applicationName = null,
+ string bufferBaseFilename = null,
+ long? bufferFileSizeLimitBytes = null)
+ {
+ if (loggerSinkConfiguration == null) throw new ArgumentNullException("loggerSinkConfiguration");
+
+ if (bufferFileSizeLimitBytes.HasValue && bufferFileSizeLimitBytes < 0)
+ throw new ArgumentException("Negative value provided; file size limit must be non-negative");
+
+ if (string.IsNullOrEmpty(applicationName))
+ throw new ArgumentException("Must supply an application name");
+
+ var defaultedPeriod = period ?? NewRelicSink.DefaultPeriod;
+
+ ILogEventSink sink;
+
+ if (bufferBaseFilename == null)
+ sink = new NewRelicSink(applicationName, batchPostingLimit, defaultedPeriod);
+ else
+ {
+ //sink = new DurableNewRelicSink(bufferBaseFilename, applicationName, batchPostingLimit, defaultedPeriod,
+ // bufferFileSizeLimitBytes);
+
+ throw new NotImplementedException("DurableNewRelicSink is not implemented yet.");
+ }
+
+ return loggerSinkConfiguration.Sink(sink, restrictedToMinimumLevel);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/Properties/AssemblyInfo.cs b/Serilog.Sinks.NewRelic/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..a350cd1
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Serilog.Sinks.NewRelic")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Serilog.Sinks.NewRelic")]
+[assembly: AssemblyCopyright("Copyright © 2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f34ae60b-cfd5-464c-bf49-a9d8fa7ee424")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.csproj b/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.csproj
new file mode 100644
index 0000000..44cce13
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.csproj
@@ -0,0 +1,79 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}
+ Library
+ Properties
+ Serilog.Sinks.NewRelic
+ Serilog.Sinks.NewRelic
+ v4.6
+ 512
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\NewRelic.Agent.Api.5.8.28.0\lib\NewRelic.Api.Agent.dll
+ True
+
+
+ ..\packages\NRConfig.Tool.1.5.0.0\lib\NRConfig.dll
+ True
+
+
+ ..\packages\Serilog.1.5.12\lib\net45\Serilog.dll
+ True
+
+
+ ..\packages\Serilog.1.5.12\lib\net45\Serilog.FullNetFx.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.sln b/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.sln
new file mode 100644
index 0000000..003191b
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Serilog.Sinks.NewRelic.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.23107.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.NewRelic", "Serilog.Sinks.NewRelic.csproj", "{F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.NewRelic.Sample", "..\Serilog.Sinks.NewRelic.Sample\Serilog.Sinks.NewRelic.Sample.csproj", "{569471CF-2FB0-434B-9F8E-58784EFB6F78}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{30EC5940-9B2D-485B-B854-F978C4B8BD5A}"
+ ProjectSection(SolutionItems) = preProject
+ ..\README.md = ..\README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F34AE60B-CFD5-464C-BF49-A9D8FA7EE424}.Release|Any CPU.Build.0 = Release|Any CPU
+ {569471CF-2FB0-434B-9F8E-58784EFB6F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {569471CF-2FB0-434B-9F8E-58784EFB6F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {569471CF-2FB0-434B-9F8E-58784EFB6F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {569471CF-2FB0-434B-9F8E-58784EFB6F78}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Serilog.Sinks.NewRelic/Sinks/NewRelic/DurableNewRelicSink.cs b/Serilog.Sinks.NewRelic/Sinks/NewRelic/DurableNewRelicSink.cs
new file mode 100644
index 0000000..69f29db
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Sinks/NewRelic/DurableNewRelicSink.cs
@@ -0,0 +1,45 @@
+using System;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Sinks.RollingFile;
+
+namespace Serilog.Sinks.NewRelic.Sinks.NewRelic
+{
+ internal class DurableNewRelicSink : ILogEventSink, IDisposable
+ {
+ private readonly NewRelicLogShipper _shipper;
+ private readonly RollingFileSink _sink;
+ private string bufferBaseFilename;
+ private string applicationName;
+ private int batchPostingLimit;
+ private TimeSpan defaultedPeriod;
+ private long? bufferFileSizeLimitBytes;
+
+ public DurableNewRelicSink(string bufferBaseFilename, string applicationName, int batchPostingLimit, TimeSpan defaultedPeriod, long? bufferFileSizeLimitBytes)
+ {
+ this.bufferBaseFilename = bufferBaseFilename;
+ this.applicationName = applicationName;
+ this.batchPostingLimit = batchPostingLimit;
+ this.defaultedPeriod = defaultedPeriod;
+ this.bufferFileSizeLimitBytes = bufferFileSizeLimitBytes;
+ }
+
+ public void Emit(LogEvent logEvent)
+ {
+ // This is a lagging indicator, but the network bandwidth usage benefits
+ // are worth the ambiguity.
+ var minimumAcceptedLevel = _shipper.MinimumAcceptedLevel;
+ if (minimumAcceptedLevel == null ||
+ (int)minimumAcceptedLevel <= (int)logEvent.Level)
+ {
+ _sink.Emit(logEvent);
+ }
+ }
+
+ public void Dispose()
+ {
+ _sink.Dispose();
+ _shipper.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/Sinks/NewRelic/LogEventExtensions.cs b/Serilog.Sinks.NewRelic/Sinks/NewRelic/LogEventExtensions.cs
new file mode 100644
index 0000000..ca4ba57
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Sinks/NewRelic/LogEventExtensions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Serilog.Events;
+
+namespace Serilog.Sinks.NewRelic.Sinks.NewRelic
+{
+ public static class LogEventExtensions
+ {
+ public static bool IsTimerEvent(this LogEvent logEvent)
+ {
+ return logEvent.Properties.Any(p => p.Key == "TimedOperationId");
+ }
+
+ public static bool IsCounterEvent(this LogEvent logEvent)
+ {
+ return logEvent.Properties.Any(p => p.Key == "CounterName");
+ }
+
+ public static bool IsGaugeEvent(this LogEvent logEvent)
+ {
+ return logEvent.Properties.Any(p => p.Key == "GaugeName");
+ }
+
+ public static bool IsTransactionEvent(this LogEvent logEvent)
+ {
+ return logEvent.Properties.Any(p => p.Key == "TransactionName");
+ }
+
+ public static string ToNewRelicSafeString(this string str, IDictionary reservedWords)
+ {
+ return reservedWords.Aggregate(str,
+ (current, reservedWord) => current.ReplaceCaseInsensitiveFind(reservedWord.Key, reservedWord.Value));
+ }
+
+ public static string ReplaceCaseInsensitiveFind(this string str, string currValue,string newValue)
+ {
+ var protectedWords = Regex.Replace(str,
+ "\\b" + Regex.Escape(currValue) + "\\b",
+ newValue,
+ RegexOptions.IgnoreCase);
+
+ var safeCharacters = Regex.Replace(protectedWords, @"[^a-zA-Z0-9:_ ]", "");
+
+ return safeCharacters;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicLogShipper.cs b/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicLogShipper.cs
new file mode 100644
index 0000000..2deb8f1
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicLogShipper.cs
@@ -0,0 +1,317 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using Serilog.Debugging;
+using Serilog.Events;
+
+namespace Serilog.Sinks.NewRelic.Sinks.NewRelic
+{
+ internal class NewRelicLogShipper : IDisposable
+ {
+ private readonly string _apiKey;
+ private readonly int _batchPostingLimit;
+ private readonly Timer _timer;
+ private readonly TimeSpan _period;
+ private readonly object _stateLock = new object();
+
+ private LogEventLevel? _minimumAcceptedLevel;
+ private static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2);
+ private DateTime _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);
+
+ private volatile bool _unloading;
+ private readonly string _bookmarkFilename;
+ private readonly string _logFolder;
+ private readonly string _candidateSearchPath;
+
+ public NewRelicLogShipper(string bufferBaseFilename, string apiKey, int batchPostingLimit, TimeSpan period)
+ {
+ _bookmarkFilename = Path.GetFullPath(bufferBaseFilename + ".bookmark");
+ _logFolder = Path.GetDirectoryName(_bookmarkFilename);
+ _candidateSearchPath = Path.GetFileName(bufferBaseFilename) + "*.json";
+ _timer = new Timer(s => OnTick());
+ _period = period;
+
+ AppDomain.CurrentDomain.DomainUnload += OnAppDomainUnloading;
+ AppDomain.CurrentDomain.ProcessExit += OnAppDomainUnloading;
+
+ SetTimer();
+ }
+
+ private void OnAppDomainUnloading(object sender, EventArgs args)
+ {
+ CloseAndFlush();
+ }
+
+ private void CloseAndFlush()
+ {
+ lock (_stateLock)
+ {
+ if (_unloading)
+ return;
+
+ _unloading = true;
+ }
+
+ AppDomain.CurrentDomain.DomainUnload -= OnAppDomainUnloading;
+ AppDomain.CurrentDomain.ProcessExit -= OnAppDomainUnloading;
+
+ var wh = new ManualResetEvent(false);
+ if (_timer.Dispose(wh))
+ wh.WaitOne();
+
+ OnTick();
+ }
+
+ ///
+ /// Get the last "minimum level" indicated by the Seq server, if any.
+ ///
+ public LogEventLevel? MinimumAcceptedLevel
+ {
+ get
+ {
+ lock (_stateLock)
+ return _minimumAcceptedLevel;
+ }
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// 2
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ ///
+ /// Free resources held by the sink.
+ ///
+ /// If true, called because the object is being disposed; if false,
+ /// the object is being disposed from the finalizer.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing) return;
+ CloseAndFlush();
+ }
+
+ private void SetTimer()
+ {
+ // Note, called under _stateLock
+
+ _timer.Change(_period, Timeout.InfiniteTimeSpan);
+ }
+
+ private void OnTick()
+ {
+ LogEventLevel? minimumAcceptedLevel = null;
+
+ try
+ {
+ int count;
+ do
+ {
+ count = 0;
+
+ // Locking the bookmark ensures that though there may be multiple instances of this
+ // class running, only one will ship logs at a time.
+
+ using (
+ var bookmark = File.Open(_bookmarkFilename, FileMode.OpenOrCreate, FileAccess.ReadWrite,
+ FileShare.Read))
+ {
+ long nextLineBeginsAtOffset;
+ string currentFile;
+
+ TryReadBookmark(bookmark, out nextLineBeginsAtOffset, out currentFile);
+
+ var fileSet = GetFileSet();
+
+ if (currentFile == null || !File.Exists(currentFile))
+ {
+ nextLineBeginsAtOffset = 0;
+ currentFile = fileSet.FirstOrDefault();
+ }
+
+ if (currentFile == null)
+ continue;
+
+
+
+ var payload = new StringWriter();
+ payload.Write("{\"events\":[");
+ var delimStart = "";
+
+ using (var current = File.Open(currentFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
+ )
+ {
+ current.Position = nextLineBeginsAtOffset;
+
+ string nextLine;
+ while (count < _batchPostingLimit &&
+ TryReadLine(current, ref nextLineBeginsAtOffset, out nextLine))
+ {
+ ++count;
+ payload.Write(delimStart);
+ payload.Write(nextLine);
+ delimStart = ",";
+ }
+
+ payload.Write("]}");
+ }
+
+ if (count > 0 || _minimumAcceptedLevel != null && _nextRequiredLevelCheckUtc < DateTime.UtcNow)
+ {
+ lock (_stateLock)
+ {
+ _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);
+ }
+
+ if (true)
+ {
+ WriteBookmark(bookmark, nextLineBeginsAtOffset, currentFile);
+ // var returned = result.Content.ReadAsStringAsync().Result;
+ // minimumAcceptedLevel = SeqApi.ReadEventInputResult(returned);
+ }
+ else
+ {
+ //SelfLog.WriteLine("Received failed HTTP shipping result {0}: {1}", result.StatusCode,
+ // result.Content.ReadAsStringAsync().Result);
+ }
+ }
+ else
+ {
+ // Only advance the bookmark if no other process has the
+ // current file locked, and its length is as we found it.
+
+ if (fileSet.Length == 2 && fileSet.First() == currentFile &&
+ IsUnlockedAtLength(currentFile, nextLineBeginsAtOffset))
+ {
+ WriteBookmark(bookmark, 0, fileSet[1]);
+ }
+
+ if (fileSet.Length > 2)
+ {
+ // Once there's a third file waiting to ship, we do our
+ // best to move on, though a lock on the current file
+ // will delay this.
+
+ File.Delete(fileSet[0]);
+ }
+ }
+ }
+ } while (count == _batchPostingLimit);
+ }
+ catch (Exception ex)
+ {
+ SelfLog.WriteLine("Exception while emitting periodic batch from {0}: {1}", this, ex);
+ }
+ finally
+ {
+ lock (_stateLock)
+ {
+ _minimumAcceptedLevel = minimumAcceptedLevel;
+ if (!_unloading)
+ SetTimer();
+ }
+ }
+ }
+
+ private bool IsUnlockedAtLength(string file, long maxLen)
+ {
+ try
+ {
+ using (var fileStream = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read))
+ {
+ return fileStream.Length <= maxLen;
+ }
+ }
+ catch (IOException ex)
+ {
+ var errorCode = Marshal.GetHRForException(ex) & ((1 << 16) - 1);
+ if (errorCode != 32 && errorCode != 33)
+ {
+ SelfLog.WriteLine("Unexpected I/O exception while testing locked status of {0}: {1}", file, ex);
+ }
+ }
+ catch (Exception ex)
+ {
+ SelfLog.WriteLine("Unexpected exception while testing locked status of {0}: {1}", file, ex);
+ }
+
+ return false;
+ }
+
+ private static void WriteBookmark(FileStream bookmark, long nextLineBeginsAtOffset, string currentFile)
+ {
+ using (var writer = new StreamWriter(bookmark))
+ {
+ writer.WriteLine("{0}:::{1}", nextLineBeginsAtOffset, currentFile);
+ }
+ }
+
+ // It would be ideal to chomp whitespace here, but not required.
+ private static bool TryReadLine(Stream current, ref long nextStart, out string nextLine)
+ {
+ var includesBom = nextStart == 0;
+
+ if (current.Length <= nextStart)
+ {
+ nextLine = null;
+ return false;
+ }
+
+ current.Position = nextStart;
+
+ using (var reader = new StreamReader(current, Encoding.UTF8, false, 128, true))
+ {
+ nextLine = reader.ReadLine();
+ }
+
+ if (nextLine == null)
+ return false;
+
+ nextStart += Encoding.UTF8.GetByteCount(nextLine) + Encoding.UTF8.GetByteCount(Environment.NewLine);
+ if (includesBom)
+ nextStart += 3;
+
+ return true;
+ }
+
+ private static void TryReadBookmark(Stream bookmark, out long nextLineBeginsAtOffset, out string currentFile)
+ {
+ nextLineBeginsAtOffset = 0;
+ currentFile = null;
+
+ if (bookmark.Length != 0)
+ {
+ string current;
+ using (var reader = new StreamReader(bookmark, Encoding.UTF8, false, 128, true))
+ {
+ current = reader.ReadLine();
+ }
+
+ if (current != null)
+ {
+ bookmark.Position = 0;
+ var parts = current.Split(new[] { ":::" }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length == 2)
+ {
+ nextLineBeginsAtOffset = long.Parse(parts[0]);
+ currentFile = parts[1];
+ }
+ }
+
+ }
+ }
+
+ private string[] GetFileSet()
+ {
+ return Directory.GetFiles(_logFolder, _candidateSearchPath)
+ .OrderBy(n => n)
+ .ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicSink.cs b/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicSink.cs
new file mode 100644
index 0000000..5ee2113
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/Sinks/NewRelic/NewRelicSink.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Serilog.Events;
+using Serilog.Sinks.PeriodicBatching;
+
+namespace Serilog.Sinks.NewRelic.Sinks.NewRelic
+{
+ internal class NewRelicSink : PeriodicBatchingSink
+ {
+ LogEventLevel? _minimumAcceptedLevel;
+
+ static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2);
+ DateTime _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);
+
+ public const int DefaultBatchPostingLimit = 1000;
+ public static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(2);
+ private readonly int _batchPostingLimit;
+ private readonly TimeSpan _defaultedPeriod;
+ private readonly IFormatProvider _formatProvider;
+ private readonly IDictionary _reservedWords;
+
+ public NewRelicSink(string applicationName, int batchSizeLimit, TimeSpan period, IFormatProvider formatProvider = null) : base(batchSizeLimit, period)
+ {
+ this._batchPostingLimit = batchSizeLimit;
+ this._defaultedPeriod = period;
+ this._formatProvider = formatProvider;
+
+ global::NewRelic.Api.Agent.NewRelic.SetApplicationName(applicationName);
+
+ _reservedWords = PopulateReservedWords();
+ }
+
+ private IDictionary PopulateReservedWords()
+ {
+ var reservedWords = new Dictionary();
+
+ reservedWords.Add("add","`add`");
+ reservedWords.Add("ago","`ago`");
+ reservedWords.Add("and", "`and`");
+ reservedWords.Add("as", "`as`");
+ reservedWords.Add("auto", "`auto`");
+ reservedWords.Add("begin", "`begin`");
+ reservedWords.Add("begintime", "`begintime`");
+ reservedWords.Add("compare", "`compare`");
+ reservedWords.Add("day", "`day`");
+ reservedWords.Add("days", "`days`");
+ reservedWords.Add("end", "`end`");
+ reservedWords.Add("endtime", "`endtime`");
+ reservedWords.Add("explain", "`explain`");
+ reservedWords.Add("facet", "`facet`");
+ reservedWords.Add("from", "`from`");
+ reservedWords.Add("hour", "`hour`");
+ reservedWords.Add("hours", "`hours`");
+ reservedWords.Add("in", "`in`");
+ reservedWords.Add("is", "`is`");
+ reservedWords.Add("like", "`like`");
+ reservedWords.Add("limit", "`limit`");
+ reservedWords.Add("minute", "`minute`");
+ reservedWords.Add("minutes", "`minutes`");
+ reservedWords.Add("month", "`month`");
+ reservedWords.Add("months", "`months`");
+ reservedWords.Add("not", "`not`");
+ reservedWords.Add("null", "`null`");
+ reservedWords.Add("offset", "`offset`");
+ reservedWords.Add("or", "`or`");
+ reservedWords.Add("second", "`second`");
+ reservedWords.Add("seconds", "`seconds`");
+ reservedWords.Add("select", "`select`");
+ reservedWords.Add("since", "`since`");
+ reservedWords.Add("timeseries", "`timeseries`");
+ reservedWords.Add("until", "`until`");
+ reservedWords.Add("week", "`week`");
+ reservedWords.Add("weeks", "`weeks`");
+ reservedWords.Add("where", "`where`");
+ reservedWords.Add("with", "`with`");
+
+
+ return reservedWords;
+ }
+
+ protected override Task EmitBatchAsync(IEnumerable events)
+ {
+ _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval);
+
+ //TODO: See if there's a way to determine the level of events accepted into NewRelic for now assume all are.
+
+ _minimumAcceptedLevel = LogEventLevel.Verbose;
+
+ return Task.Run(() =>
+ {
+ foreach (var logEvent in events)
+ {
+ var renderedMessage = logEvent.RenderMessage(_formatProvider).ToNewRelicSafeString(_reservedWords);
+
+ // Made up standard for transactions Property = TransactionName, Value = category::name
+
+ if (logEvent.IsTransactionEvent())
+ {
+
+ var transaction = logEvent.Properties.First(x => x.Key == "TransactionName");
+ var transactionValue = transaction.Value.ToString().Replace("\"","");
+ var transactionValues = transactionValue.Split(new[] { "::" }, StringSplitOptions.None);
+
+ if (transactionValues.Length < 2)
+ {
+ continue;
+ }
+
+ var category = transactionValues[0].ToNewRelicSafeString(_reservedWords);
+ var name = transactionValues[1].ToNewRelicSafeString(_reservedWords);
+
+ global::NewRelic.Api.Agent.NewRelic.SetTransactionName(category, name);
+ }
+
+ if (logEvent.IsTimerEvent())
+ {
+ // Ignore the Beginning Operation
+
+ if (logEvent.Properties.All(x => x.Key != "TimedOperationElapsedInMs"))
+ {
+ continue;
+ }
+
+ var elapsedTime = logEvent.Properties.First(x => x.Key == "TimedOperationElapsedInMs");
+ var operation = logEvent.Properties.First(x => x.Key == "TimedOperationDescription");
+
+ int numeric;
+ var isNumber = int.TryParse(elapsedTime.Value.ToString(), out numeric);
+
+ if (isNumber)
+ {
+ var safeOperationString = operation.ToString().ToNewRelicSafeString(_reservedWords);
+
+ global::NewRelic.Api.Agent.NewRelic.RecordResponseTimeMetric(safeOperationString, numeric);
+ }
+
+ continue;
+ }
+
+ if (logEvent.IsCounterEvent())
+ {
+ var operation = logEvent.Properties.First(x => x.Key == "CounterName");
+
+ var safeOperationString = operation.ToString().ToNewRelicSafeString(_reservedWords);
+
+ global::NewRelic.Api.Agent.NewRelic.IncrementCounter(safeOperationString);
+
+ continue;
+ }
+
+ if (logEvent.IsGaugeEvent())
+ {
+ var elapsedTime = logEvent.Properties.First(x => x.Key == "GaugeValue");
+ var operation = logEvent.Properties.First(x => x.Key == "GaugeName");
+
+ float numeric;
+ var isNumber = float.TryParse(elapsedTime.Value.ToString(), out numeric);
+
+ if (isNumber)
+ {
+ var safeOperationString = operation.ToString().ToNewRelicSafeString(_reservedWords);
+
+ global::NewRelic.Api.Agent.NewRelic.RecordMetric(safeOperationString, numeric);
+ }
+
+ continue;
+ }
+
+ if (logEvent.Level == LogEventLevel.Error)
+ {
+ var properties = LogEventPropertiesToNewRelicExceptionProperties(logEvent);
+
+ if (logEvent.Exception != null)
+ {
+ global::NewRelic.Api.Agent.NewRelic.NoticeError(logEvent.Exception, properties);
+ }
+ else
+ {
+ global::NewRelic.Api.Agent.NewRelic.NoticeError(renderedMessage, properties);
+ }
+ }
+ else
+ {
+ var properties = LogEventPropertiesToNewRelicCustomEventProperties(logEvent);
+
+ global::NewRelic.Api.Agent.NewRelic.RecordCustomEvent(renderedMessage, properties);
+ }
+ }
+ });
+ }
+
+
+
+ // The sink must emit at least one event on startup, and the server be
+ // configured to set a specific level, before background level checks will be performed.
+ protected override void OnEmptyBatch()
+ {
+ if (_minimumAcceptedLevel != null &&
+ _nextRequiredLevelCheckUtc < DateTime.UtcNow)
+ {
+ EmitBatch(Enumerable.Empty());
+ }
+ }
+ protected override bool CanInclude(LogEvent evt)
+ {
+ return _minimumAcceptedLevel == null ||
+ (int)_minimumAcceptedLevel <= (int)evt.Level;
+ }
+ private IDictionary LogEventPropertiesToNewRelicExceptionProperties(LogEvent logEvent)
+ {
+ var properties = new Dictionary();
+
+ foreach (var source in logEvent.Properties.Where(p => p.Value != null))
+ {
+ var renderedProperty = source.Value.ToString().ToNewRelicSafeString(_reservedWords);
+
+ properties.Add(source.Key.ToNewRelicSafeString(_reservedWords), renderedProperty);
+ }
+
+ return properties;
+ }
+ private IDictionary LogEventPropertiesToNewRelicCustomEventProperties(LogEvent logEvent)
+ {
+ var properties = new Dictionary();
+
+ foreach (var source in logEvent.Properties.Where(p => p.Value != null))
+ {
+ double numeric;
+ var isNumber = double.TryParse(source.Value.ToString(), out numeric);
+
+ var safeKey = source.Key.ToNewRelicSafeString(_reservedWords);
+
+ if (!isNumber)
+ {
+ var renderedProperty = source.Value.ToString().ToNewRelicSafeString(_reservedWords);
+
+ properties.Add(safeKey, renderedProperty);
+ }
+ else
+ {
+ properties.Add(safeKey, (float)numeric);
+ }
+ }
+
+ return properties;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Serilog.Sinks.NewRelic/app.config b/Serilog.Sinks.NewRelic/app.config
new file mode 100644
index 0000000..920431d
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/app.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Serilog.Sinks.NewRelic/packages.config b/Serilog.Sinks.NewRelic/packages.config
new file mode 100644
index 0000000..760c2ef
--- /dev/null
+++ b/Serilog.Sinks.NewRelic/packages.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file