Skip to content

Commit

Permalink
Adds support for structured logging properties (IncludeEventPropertie…
Browse files Browse the repository at this point in the history
…s = true)

PR #10 
Issue #9
  • Loading branch information
snakefoot authored and dustinchilson committed May 31, 2018
1 parent 848ff44 commit 45524ff
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 139 deletions.
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net471</TargetFramework>
<Configurations>Debug;Release</Configurations>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>bin\Release\net471\NLogGraylogHttp.Example.NetFx.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>bin\Debug\net471\NLogGraylogHttp.Example.NetFx.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\NLog.Targets.GraylogHttp\NLog.Targets.GraylogHttp.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
<PackageReference Include="NLog" Version="4.5.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Configuration" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net.Http">
<Private>True</Private>
</Reference>
<Reference Include="System.Net.Http.WebRequest" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Data.DataSetExtensions" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net45</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\NLog.Targets.GraylogHttp\NLog.Targets.GraylogHttp.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
<PackageReference Include="NLog" Version="4.5.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Configuration" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Net.Http.WebRequest" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Data.DataSetExtensions" />
</ItemGroup>
<PropertyGroup>
<ProjectConfigFileName>App.config</ProjectConfigFileName>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>bin\Release\netcoreapp2.0\NLogGraylogHttp.Example.NetCore.xml</DocumentationFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>bin\Debug\netcoreapp2.0\NLogGraylogHttp.Example.NetCore.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
Expand Down
12 changes: 6 additions & 6 deletions src/NLog.Targets.GraylogHttp.Tests/MessageBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ public class MessageBuilderTests
public void SimpleMessageTest()
#pragma warning restore CA1822 // Mark members as static
{
var testMessage = new GraylogMessageBuilder(() => 10)
var testMessage = new GraylogMessageBuilder()
.WithCustomProperty("facility", "Test")
.WithProperty("short_message", "short_message")
.WithProperty("host", "magic")
.WithLevel(LogLevel.Debug)
.WithCustomProperty("logger_name", "SimpleMessageTest")
.Render();
.Render(new DateTime(1970, 1, 1, 0, 0, 10, DateTimeKind.Utc));

var expectedMessage = "{\"_facility\":\"Test\",\"short_message\":\"short_message\",\"host\":\"magic\",\"level\":\"7\",\"_logger_name\":\"SimpleMessageTest\",\"timestamp\":\"10\",\"version\":\"1.1\"}";
var expectedMessage = "{\"_facility\":\"Test\",\"short_message\":\"short_message\",\"host\":\"magic\",\"level\":7,\"_logger_name\":\"SimpleMessageTest\",\"timestamp\":10,\"version\":\"1.1\"}";

Assert.Equal(expectedMessage, testMessage);
}
Expand All @@ -28,16 +28,16 @@ public void SimpleMessageTest()
public void MessageWithHugePropertyTest()
#pragma warning restore CA1822 // Mark members as static
{
var testMessage = new GraylogMessageBuilder(() => 10)
var testMessage = new GraylogMessageBuilder()
.WithCustomProperty("facility", "Test")
.WithProperty("short_message", "short_message")
.WithProperty("host", "magic")
.WithLevel(LogLevel.Debug)
.WithCustomProperty("logger_name", "SimpleMessageTest")
.WithProperty("longstring", new string('*', 50000))
.Render();
.Render(new DateTime(1970, 1, 1, 0, 0, 10, DateTimeKind.Utc));

var expectedMessage = $"{{\"_facility\":\"Test\",\"short_message\":\"short_message\",\"host\":\"magic\",\"level\":\"7\",\"_logger_name\":\"SimpleMessageTest\",\"longstring\":\"{new string('*', 16383)}\",\"timestamp\":\"10\",\"version\":\"1.1\"}}";
var expectedMessage = $"{{\"_facility\":\"Test\",\"short_message\":\"short_message\",\"host\":\"magic\",\"level\":7,\"_logger_name\":\"SimpleMessageTest\",\"longstring\":\"{new string('*', 16383)}\",\"timestamp\":10,\"version\":\"1.1\"}}";

Assert.Equal(expectedMessage, testMessage);
}
Expand Down
108 changes: 87 additions & 21 deletions src/NLog.Targets.GraylogHttp/GraylogHttpTarget.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using NLog.Common;
using NLog.Config;

namespace NLog.Targets.GraylogHttp
{
[Target("GraylogHttp")]
public class GraylogHttpTarget : TargetWithLayout
public class GraylogHttpTarget : TargetWithContext
{
public GraylogHttpTarget()
{
Host = Environment.GetEnvironmentVariable("COMPUTERNAME") ?? Environment.GetEnvironmentVariable("HOSTNAME");
Parameters = new List<GraylogParameterInfo>();
ContextProperties = new List<TargetPropertyWithContext>();
}

[RequiredParameter]
Expand All @@ -28,40 +27,107 @@ public GraylogHttpTarget()

public string Host { get; set; }

[ArrayParameter(typeof(GraylogParameterInfo), "parameter")]
public IList<GraylogParameterInfo> Parameters { get; private set; }
[ArrayParameter(typeof(TargetPropertyWithContext), "parameter")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "NLog Behavior")]
public override IList<TargetPropertyWithContext> ContextProperties { get; }

private HttpClient _httpClient;

private Uri _requestAddress;

protected override void InitializeTarget()
{
if (string.IsNullOrEmpty(Host))
Host = GetMachineName();

_httpClient = new HttpClient();
_requestAddress = new Uri(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}:{1}/gelf", GraylogServer, GraylogPort));
_httpClient.DefaultRequestHeaders.ExpectContinue = false; // Expect (100) Continue breaks the graylog server

// Prefix the custom properties with underscore upfront, so we dont have to do it for each logevent
for (int i = 0; i < ContextProperties.Count; ++i)
{
var p = ContextProperties[i];
if (!p.Name.StartsWith("_", StringComparison.Ordinal))
p.Name = string.Concat("_", p.Name);
}

base.InitializeTarget();
}

protected override void Write(LogEventInfo logEvent)
{
GraylogMessageBuilder messageBuilder = new GraylogMessageBuilder()
.WithCustomProperty("facility", Facility)
.WithCustomProperty("_facility", Facility)
.WithProperty("short_message", logEvent.Message)
.WithProperty("host", Host)
.WithLevel(logEvent.Level)
.WithCustomProperty("logger_name", logEvent.LoggerName);
.WithCustomProperty("_logger_name", logEvent.LoggerName);

if (Parameters != null && Parameters.Any())
var properties = GetAllProperties(logEvent);
foreach (var property in properties)
{
Dictionary<string, string> paramsDictionary = Parameters
.Select(p => new KeyValuePair<string, string>(p.Name, p.Layout.Render(logEvent)))
.ToDictionary(pair => pair.Key, pair => pair.Value);

messageBuilder.WithCustomPropertyRange(paramsDictionary);
try
{
if (!string.IsNullOrEmpty(property.Key))
{
if (Convert.GetTypeCode(property.Value) != TypeCode.Object)
messageBuilder.WithCustomProperty(property.Key, property.Value);
else
messageBuilder.WithCustomProperty(property.Key, property.Value?.ToString());
}
}
catch (Exception ex)
{
InternalLogger.Error(ex, "GraylogHttp(Name={0}): Fail to handle LogEvent Properties", Name);
}
}

if (logEvent.Exception != null)
{
if (!string.IsNullOrEmpty(logEvent.Exception.Message))
messageBuilder.WithCustomProperty("exception_message", logEvent.Exception.Message);
messageBuilder.WithCustomProperty("_exception_message", logEvent.Exception.Message);
if (!string.IsNullOrEmpty(logEvent.Exception.StackTrace))
messageBuilder.WithCustomProperty("exception_stack_trace", logEvent.Exception.StackTrace);
messageBuilder.WithCustomProperty("_exception_stack_trace", logEvent.Exception.StackTrace);
}

using (var httpClient = new HttpClient())
try
{
// Ensure to reuse the same HttpClient. See also: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
_httpClient.PostAsync(_requestAddress, new StringContent(messageBuilder.Render(logEvent.TimeStamp), Encoding.UTF8, "application/json")).Result.EnsureSuccessStatusCode();
}
catch (AggregateException ex)
{
foreach (var inner in ex.Flatten().InnerExceptions)
InternalLogger.Error(inner, "GraylogHttp(Name={0}): Fail to post LogEvents", Name);
}
catch (Exception ex)
{
InternalLogger.Error(ex, "GraylogHttp(Name={0}): Fail to post LogEvents", Name);
}
}

/// <summary>
/// Gets the machine name
/// </summary>
private static string GetMachineName()
{
return TryLookupValue(() => Environment.GetEnvironmentVariable("COMPUTERNAME"), "COMPUTERNAME")
?? TryLookupValue(() => Environment.GetEnvironmentVariable("HOSTNAME"), "HOSTNAME")
?? TryLookupValue(() => Dns.GetHostName(), "DnsHostName");
}

private static string TryLookupValue(Func<string> lookupFunc, string lookupType)
{
try
{
string lookupValue = lookupFunc()?.Trim();
return string.IsNullOrEmpty(lookupValue) ? null : lookupValue;
}
catch (Exception ex)
{
httpClient.BaseAddress = new Uri($"{GraylogServer}:{GraylogPort}/gelf");
httpClient.DefaultRequestHeaders.ExpectContinue = false; // Expect (100) Continue breaks the graylog server
HttpResponseMessage httpResponseMessage = httpClient.PostAsync(new Uri(string.Empty), new StringContent(messageBuilder.Render(), Encoding.UTF8, "application/json")).Result;
InternalLogger.Warn(ex, "GraylogHttp: Failed to lookup {0}", lookupType);
return null;
}
}
}
Expand Down
85 changes: 40 additions & 45 deletions src/NLog.Targets.GraylogHttp/GraylogMessageBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,76 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace NLog.Targets.GraylogHttp
{
internal class GraylogMessageBuilder
{
private readonly JsonObject _graylogMessage = new JsonObject();
private readonly Func<int> _timestampGenerator;

internal GraylogMessageBuilder(Func<int> timestampGenerator = null)
{
if (timestampGenerator == null)
timestampGenerator = () => (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;

_timestampGenerator = timestampGenerator;
}
private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

public GraylogMessageBuilder WithLevel(LogLevel level)
{
GelfLevel graylogLevel;
object graylogLevel;

if (level == LogLevel.Debug)
graylogLevel = GelfLevel.Debug;
else if (level == LogLevel.Fatal)
graylogLevel = GelfLevel.Critical;
if (level == LogLevel.Trace)
graylogLevel = GelfLevel_Debug;
else if (level == LogLevel.Debug)
graylogLevel = GelfLevel_Debug;
else if (level == LogLevel.Info)
graylogLevel = GelfLevel.Informational;
else if (level == LogLevel.Trace)
graylogLevel = GelfLevel.Informational;
graylogLevel = GelfLevel_Informational;
else if (level == LogLevel.Warn)
graylogLevel = GelfLevel_Warning;
else if (level == LogLevel.Fatal)
graylogLevel = GelfLevel_Critical;
else
graylogLevel = level == LogLevel.Warn ? GelfLevel.Warning : GelfLevel.Error;
graylogLevel = GelfLevel_Error;

return WithProperty("level", (int)graylogLevel);
return WithProperty("level", graylogLevel);
}

public GraylogMessageBuilder WithProperty(string propertyName, object value)
{
// Trunate due to https://github.com/Graylog2/graylog2-server/issues/873
// 32766b max, C# strings 2b per char
_graylogMessage[propertyName] = value.ToString().Truncate(16383);
return this;
{
if (value is IConvertible primitive && primitive.GetTypeCode() != TypeCode.String)
{
value = primitive;
}
else
{
// Trunate due to https://github.com/Graylog2/graylog2-server/issues/873
// 32766b max, C# strings 2b per char
value = value?.ToString()?.Truncate(16383);
}

_graylogMessage[propertyName] = value;
return this;
}

public GraylogMessageBuilder WithCustomProperty(string propertyName, object value)
{
return WithProperty($"_{propertyName}", value);
}
if (!propertyName.StartsWith("_", StringComparison.Ordinal))
propertyName = string.Concat("_", propertyName);

public GraylogMessageBuilder WithCustomPropertyRange(Dictionary<string, string> properties)
{
return properties.Aggregate(this, (builder, pair) => builder.WithCustomProperty(pair.Key, pair.Value));
return WithProperty(propertyName, value);
}

public string Render()
public string Render(DateTime timestamp)
{
WithProperty("timestamp", _timestampGenerator());
WithProperty("timestamp", (long)(timestamp.ToUniversalTime() - _epochTime).TotalSeconds);
WithProperty("version", "1.1");

return _graylogMessage.ToString();
}

public enum GelfLevel
{
Emergency = 0,
Alert = 1,
Critical = 2,
Error = 3,
Warning = 4,
Notice = 5,
Informational = 6,
Debug = 7
}
//private static readonly object GelfLevel_Emergency = 0;
//private static readonly object GelfLevel_Alert = 1;
private static readonly object GelfLevel_Critical = 2;
private static readonly object GelfLevel_Error = 3;
private static readonly object GelfLevel_Warning = 4;
//private static readonly object GelfLevel_Notice = 5;
private static readonly object GelfLevel_Informational = 6;
private static readonly object GelfLevel_Debug = 7;
}
}
Loading

0 comments on commit 45524ff

Please sign in to comment.