Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 402 additions & 5 deletions .gitignore

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions BetterStack.Logs.NLog.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>BetterStack.Logs.NLog</PackageId>
<Version>1.0.0</Version>
Expand All @@ -15,7 +15,7 @@
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net462;netstandard2.0;netstandard2.1</TargetFrameworks>
<DefaultItemExcludes>$(DefaultItemExcludes);example-project/**;dashboard.png</DefaultItemExcludes>
</PropertyGroup>

Expand All @@ -32,17 +32,20 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.11" />
<PackageReference Include="NLog" Version="5.2.5" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>

<ItemGroup>
<None Include="LICENSE.md" Pack="true" PackagePath="$(PackageLicenseFile)"/>
<None Include="README.md" Pack="true" PackagePath="$(PackageReadmeFile)"/>
<None Include="LICENSE.md" Pack="true" PackagePath="$(PackageLicenseFile)" />
<None Include="README.md" Pack="true" PackagePath="$(PackageReadmeFile)" />
<None Include="icon.png" Pack="true" Visible="false" PackagePath="$(PackageIconFile)" />
</ItemGroup>
</Project>
150 changes: 150 additions & 0 deletions BetterStack.Logs.NLog/BetterStackJsonLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Text;
using NLog.Config;
using NLog.Layouts;

namespace BetterStack.Logs.NLog
{
internal class BetterStackJsonLayout : JsonLayout
{
/// <summary>
/// Gets or sets the layout used to format log messages.
/// </summary>
/// <remarks>
/// The default value of the layout is: <code>${message}</code>
/// </remarks>
public Layout Message
{
get => ResolveJsonAttribute(nameof(Message))?.Layout;
set
{
var attribute = ResolveJsonAttribute(nameof(Message));
if (attribute != null && value != null)
attribute.Layout = value;
}
}
/// <inheritdoc cref="JsonLayout.IncludeEventProperties"/>
public new bool IncludeEventProperties
{
get
{
var attribute = ResolveJsonAttribute(nameof(Context));
var propertiesLayout = ResolveJsonAttribute("properties", attribute.Layout as JsonLayout)?.Layout as JsonLayout;
return propertiesLayout?.IncludeEventProperties ?? false;
}
set
{
var attribute = ResolveJsonAttribute(nameof(Context));
var propertiesLayout = ResolveJsonAttribute("properties", attribute.Layout as JsonLayout)?.Layout as JsonLayout;
if (propertiesLayout != null)
propertiesLayout.IncludeEventProperties = value;
}
}

/// <inheritdoc cref="JsonLayout.IncludeScopeProperties"/>
public new bool IncludeScopeProperties
{
get => (ResolveJsonAttribute(nameof(Context))?.Layout as JsonLayout)?.IncludeScopeProperties ?? false;
set
{
var attribute = ResolveJsonAttribute(nameof(Context));
if (attribute != null && attribute.Layout is JsonLayout jsonLayout)
jsonLayout.IncludeScopeProperties = value;
}
}

/// <inheritdoc cref="JsonLayout.IncludeGdc"/>
public bool IncludeGdcProperties
{
get => (ResolveJsonAttribute(nameof(Context))?.Layout as JsonLayout)?.IncludeGdc ?? false;
set
{
var attribute = ResolveJsonAttribute(nameof(Context));
if (attribute != null && attribute.Layout is JsonLayout jsonLayout)
jsonLayout.IncludeGdc = value;
}
}

/// <summary>
/// Gets the array of attributes for the "properties"-section
/// </summary>
[ArrayParameter(typeof(JsonAttribute), "ContextProperty")]
public IList<JsonAttribute> Context
{
get
{
var attribute = ResolveJsonAttribute(nameof(Context));
return (attribute?.Layout as JsonLayout)?.Attributes;
}
}

/// <summary>
/// Control callsite capture of source-file and source-linenumber.
/// </summary>
/// <remarks>Enabling this will hurt application performance as NLog will capture StackTrace for each log-message</remarks>
public StackTraceUsage StackTraceUsage
{
get
{
var attribute = ResolveJsonAttribute(nameof(Context));
var runtimeLayout = ResolveJsonAttribute("runtime", attribute.Layout as JsonLayout)?.Layout as JsonLayout;
return runtimeLayout?.Attributes?.Count > 0 ? StackTraceUsage.WithCallSite : StackTraceUsage.None;
}
set
{
var attribute = ResolveJsonAttribute(nameof(Context));
var runtimeLayout = ResolveJsonAttribute("runtime", attribute.Layout as JsonLayout)?.Layout as JsonLayout;
if (value == StackTraceUsage.None && runtimeLayout != null)
{
runtimeLayout.Attributes.Clear();
}
else if (value != StackTraceUsage.None && runtimeLayout != null)
{
runtimeLayout.Attributes.Clear();
runtimeLayout.Attributes.Add(new JsonAttribute("class", "${callsite:classname=true:methodName=false}"));
runtimeLayout.Attributes.Add(new JsonAttribute("member", "${callsite:classname=false:methodName=true}"));
if (value.HasFlag(StackTraceUsage.WithFileNameAndLineNumber) || value.HasFlag(StackTraceUsage.WithStackTrace))
{
runtimeLayout.Attributes.Add(new JsonAttribute("file", "${callsite-filename}"));
runtimeLayout.Attributes.Add(new JsonAttribute("line", "${callsite-linenumber}"));
}
}
}
}

public BetterStackJsonLayout()
{
Attributes.Add(new JsonAttribute("dt", "\"${date:universalTime=true:format=o}\"", encode: false));
Attributes.Add(new JsonAttribute("level", "${level}"));
Attributes.Add(new JsonAttribute("message", "${message}"));
Attributes.Add(new JsonAttribute("logger", "${logger}"));
Attributes.Add(new JsonAttribute("exception", "${exception:format=tostring}"));
var contextLayout = new JsonLayout() { SuppressSpaces = true };
contextLayout.Attributes.Add(new JsonAttribute("properties", new JsonLayout() { IncludeEventProperties = true, SuppressSpaces = true }, encode: false));
contextLayout.Attributes.Add(new JsonAttribute("runtime", new JsonLayout() { SuppressSpaces = true }, encode: false));
Attributes.Add(new JsonAttribute("context", contextLayout, encode: false));
SuppressSpaces = true;
}

protected override void InitializeLayout()
{
base.IncludeEventProperties = false;
base.IncludeScopeProperties = false;
base.InitializeLayout();
}

private JsonAttribute ResolveJsonAttribute(string attributeName, JsonLayout jsonLayout = null)
{
var attributes = jsonLayout?.Attributes ?? Attributes;
for (int i = 0; i < attributes.Count; ++i)
{
if (attributeName.Equals(attributes[i].Name, StringComparison.OrdinalIgnoreCase))
{
return attributes[i];
}
}
return null;
}
}
}
136 changes: 68 additions & 68 deletions BetterStack.Logs.NLog/BetterStackLogsTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using NLog.Config;
using NLog.Targets;
using NLog.Layouts;
using NLog.Common;

namespace BetterStack.Logs.NLog
{
Expand All @@ -12,13 +13,26 @@ namespace BetterStack.Logs.NLog
/// to the Better Stack server but it sends them periodically in batches.
/// </summary>
[Target("BetterStack.Logs")]
public sealed class BetterStackLogsTarget : TargetWithContext
public sealed class BetterStackLogsTarget : TargetWithLayout
{
private readonly BetterStackJsonLayout _jsonLayout = new BetterStackJsonLayout();
private Drain betterStackDrain = null;

/// <summary>
/// Gets the JSON layout configuration used for formatting log entries.
/// </summary>
public JsonLayout JsonLayoutt => _jsonLayout;

/// <inheritdoc cref="BetterStackJsonLayout.Message"/>
public override Layout Layout
{
get => _jsonLayout.Message;
set => _jsonLayout.Message = value;
}

/// <summary>
/// Gets or sets the Better Stack Logs source token.
/// </summary>
/// <value>The source token.</value>
[RequiredParameter]
public Layout SourceToken { get; set; }

/// <summary>
Expand All @@ -41,52 +55,55 @@ public sealed class BetterStackLogsTarget : TargetWithContext
/// </summary>
public int Retries { get; set; } = 10;

/// <inheritdoc cref="BetterStackJsonLayout.IncludeEventProperties"/>
public bool IncludeEventProperties
{
get => _jsonLayout.IncludeEventProperties;
set => _jsonLayout.IncludeEventProperties = value;
}

/// <inheritdoc cref="BetterStackJsonLayout.IncludeScopeProperties"/>
public bool IncludeScopeProperties
{
get => _jsonLayout.IncludeScopeProperties;
set => _jsonLayout.IncludeScopeProperties = value;
}

/// <inheritdoc cref="BetterStackJsonLayout.IncludeGdcProperties"/>
public bool IncludeGlobalDiagnosticContext
{
get => _jsonLayout.IncludeGdcProperties;
set => _jsonLayout.IncludeGdcProperties = value;
}

/// <summary>
/// We capture the file and line of every log message by default. You can turn this
/// option off if it has negative impact on the performance of your application.
/// Capture the file and line of every log-message.
/// </summary>
/// <remarks>Enabling this will hurt application performance as NLog will capture StackTrace for each log-message</remarks>
public bool CaptureSourceLocation
{
get => StackTraceUsage == StackTraceUsage.Max;
get => StackTraceUsage != StackTraceUsage.None;
set => StackTraceUsage = value ? StackTraceUsage.Max : StackTraceUsage.None;
}

/// <summary>
/// Include GlobalDiagnosticContext in logs.
/// </summary>
public bool IncludeGlobalDiagnosticContext { get; set; } = true;

/// <summary>
/// Control callsite capture of source-file and source-linenumber.
/// </summary>
/// <inheritdoc cref="BetterStackJsonLayout.StackTraceUsage"/>
public StackTraceUsage StackTraceUsage
{
get => _stackTraceUsage;
set
{
if (value == StackTraceUsage.None)
{
IncludeCallSite = false;
IncludeCallSiteStackTrace = false;
}
else
{
IncludeCallSite = true;
IncludeCallSiteStackTrace = value == StackTraceUsage.Max;
}
_stackTraceUsage = value;
}
get => _jsonLayout.StackTraceUsage;
set => _jsonLayout.StackTraceUsage = value;
}
private StackTraceUsage _stackTraceUsage;

private Drain betterStackDrain = null;
/// <inheritdoc cref="BetterStackJsonLayout.Context"/>
/// <remarks>To replicate <see cref="TargetWithContext.ContextProperties"/></remarks>
[ArrayParameter(typeof(JsonAttribute), "contextproperty")]
public IList<JsonAttribute> ContextProperties => _jsonLayout.Context;

/// <summary>
/// Initializes a new instance of the BetterStack.Logs.NLog.BetterStackLogsTarget class.
/// </summary>
public BetterStackLogsTarget()
{
StackTraceUsage = StackTraceUsage.Max;
base.Layout = _jsonLayout;
}

/// <inheritdoc/>
Expand All @@ -95,7 +112,12 @@ protected override void InitializeTarget()
betterStackDrain?.Stop().Wait();

var sourceToken = RenderLogEvent(SourceToken, LogEventInfo.CreateNullEvent());
if (string.IsNullOrEmpty(sourceToken))
throw new NLogConfigurationException("SourceToken is required for BetterStackLogsTarget.");

var endpoint = RenderLogEvent(Endpoint, LogEventInfo.CreateNullEvent());
if (string.IsNullOrEmpty(endpoint))
throw new NLogConfigurationException("Endpoint is required for BetterStackLogsTarget.");

var client = new Client(
sourceToken,
Expand All @@ -115,48 +137,26 @@ protected override void InitializeTarget()
/// <inheritdoc/>
protected override void CloseTarget()
{
betterStackDrain?.Stop().Wait();
if (betterStackDrain != null && !betterStackDrain.Stop().Wait(TimeSpan.FromSeconds(15)))
global::NLog.Common.InternalLogger.Warn("BetterStackLogsTarget: Failed to Stop. Check for network connectivity issues.");
base.CloseTarget();
}

/// <inheritdoc/>
protected override void Write(LogEventInfo logEvent)
{
var contextDictionary = new Dictionary<string, object> {
["logger"] = logEvent.LoggerName,
["properties"] = logEvent.Properties,
["runtime"] = new Dictionary<string, object> {
["class"] = logEvent.CallerClassName,
["member"] = logEvent.CallerMemberName,
["file"] = string.IsNullOrEmpty(logEvent.CallerFilePath) ? null : logEvent.CallerFilePath,
["line"] = string.IsNullOrEmpty(logEvent.CallerFilePath) ? null : logEvent.CallerLineNumber as int?,
},
};

if (IncludeGlobalDiagnosticContext) {
var gdcKeys = GlobalDiagnosticsContext.GetNames();

if (gdcKeys.Count > 0) {
var gdcDict = new Dictionary<string, object>();

foreach (string key in gdcKeys) {
if (string.IsNullOrEmpty(key)) continue;
gdcDict[key] = GlobalDiagnosticsContext.GetObject(key);
}

contextDictionary["gdc"] = gdcDict;
}
}
string logMessage = RenderLogEvent(this.Layout, logEvent);

var log = new Log {
Timestamp = new DateTimeOffset(logEvent.TimeStamp),
Message = logMessage,
Level = logEvent.Level.Name,
Context = contextDictionary
};

betterStackDrain.Enqueue(log);
var payload = RenderLogEvent(_jsonLayout, logEvent);
if (!string.IsNullOrEmpty(payload))
betterStackDrain.Enqueue(payload);
}

/// <inheritdoc/>
protected override void FlushAsync(AsyncContinuation asyncContinuation)
{
if (betterStackDrain != null)
betterStackDrain.Flush().ContinueWith(t => asyncContinuation(t.Exception));
else
base.FlushAsync(asyncContinuation);
}
}
}
Loading