From e8b5c35e8c2d7cb860d5a3cedd728624bb4bf1f8 Mon Sep 17 00:00:00 2001 From: Rolf Kristensen Date: Mon, 30 Apr 2018 19:20:07 +0200 Subject: [PATCH] NLog ApplicationInsightsTarget - GlobalDiagnosticContext must be explictly added using ContextProperties --- CHANGELOG.md | 3 +- src/NLogTarget/ApplicationInsightsTarget.cs | 42 +++++++++------ src/NLogTarget/TargetPropertyWithContext.cs | 52 +++++++++++++++++++ .../NLogTarget.Net45.Tests/NLogTargetTests.cs | 20 ++++--- 4 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 src/NLogTarget/TargetPropertyWithContext.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 550074df..ba70d57d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -### Version 2.6.0 +### Version 2.6.0-vNext - [NLog Flush should include async delay](https://github.com/Microsoft/ApplicationInsights-dotnet-logging/pull/176) +- [NLog can include additional ContextProperties](https://github.com/Microsoft/ApplicationInsights-dotnet-logging/pull/183) ### Version 2.6.0-beta3 - [NetStandard Support for TraceListener](https://github.com/Microsoft/ApplicationInsights-dotnet-logging/pull/166) diff --git a/src/NLogTarget/ApplicationInsightsTarget.cs b/src/NLogTarget/ApplicationInsightsTarget.cs index 4069864c..02c1d6f1 100644 --- a/src/NLogTarget/ApplicationInsightsTarget.cs +++ b/src/NLogTarget/ApplicationInsightsTarget.cs @@ -18,13 +18,14 @@ namespace Microsoft.ApplicationInsights.NLogTarget using NLog; using NLog.Common; + using NLog.Config; using NLog.Targets; /// /// NLog Target that routes all logging output to the Application Insights logging framework. /// The messages will be uploaded to the Application Insights cloud service. /// - [Target("ApplicationInsightsTarget")] + [Target("ApplicationInsightsTarget")] public sealed class ApplicationInsightsTarget : TargetWithLayout { private TelemetryClient telemetryClient; @@ -43,6 +44,12 @@ public ApplicationInsightsTarget() /// public string InstrumentationKey { get; set; } + /// + /// Gets the array of custom attributes to be passed into the logevent context + /// + [ArrayParameter(typeof(TargetPropertyWithContext), "contextproperty")] + public IList ContextProperties { get; } = new List(); + /// /// Gets the logging controller we will be using. /// @@ -78,8 +85,20 @@ internal void BuildPropertyBag(LogEventInfo logEvent, ITelemetry trace) propertyBag.Add("UserStackFrameNumber", logEvent.UserStackFrameNumber.ToString(CultureInfo.InvariantCulture)); } - this.LoadGlobalDiagnosticsContextProperties(propertyBag); - this.LoadLogEventProperties(logEvent, propertyBag); + for (int i = 0; i < this.ContextProperties.Count; ++i) + { + var contextProperty = this.ContextProperties[i]; + if (!string.IsNullOrEmpty(contextProperty.Name)) + { + string propertyValue = contextProperty.Layout?.Render(logEvent); + this.PopulatePropertyBag(propertyBag, contextProperty.Name, propertyValue); + } + } + + if (logEvent.HasProperties) + { + this.LoadLogEventProperties(logEvent, propertyBag); + } } /// @@ -165,7 +184,7 @@ private void SendException(LogEventInfo logEvent) private void SendTrace(LogEventInfo logEvent) { string logMessage = this.Layout.Render(logEvent); - + var trace = new TraceTelemetry(logMessage) { SeverityLevel = this.GetSeverityLevel(logEvent.Level) @@ -175,20 +194,11 @@ private void SendTrace(LogEventInfo logEvent) this.telemetryClient.Track(trace); } - private void LoadGlobalDiagnosticsContextProperties(IDictionary propertyBag) - { - foreach (string key in GlobalDiagnosticsContext.GetNames()) - { - this.PopulatePropertyBag(propertyBag, key, GlobalDiagnosticsContext.GetObject(key)); - } - } - private void LoadLogEventProperties(LogEventInfo logEvent, IDictionary propertyBag) { - var properties = logEvent.Properties; - if (properties != null) + if (logEvent.Properties?.Count > 0) { - foreach (var keyValuePair in properties) + foreach (var keyValuePair in logEvent.Properties) { string key = keyValuePair.Key.ToString(); object valueObj = keyValuePair.Value; @@ -204,7 +214,7 @@ private void PopulatePropertyBag(IDictionary propertyBag, string return; } - string value = valueObj.ToString(); + string value = Convert.ToString(valueObj, CultureInfo.InvariantCulture); if (propertyBag.ContainsKey(key)) { if (string.Equals(value, propertyBag[key], StringComparison.Ordinal)) diff --git a/src/NLogTarget/TargetPropertyWithContext.cs b/src/NLogTarget/TargetPropertyWithContext.cs new file mode 100644 index 00000000..2616a59f --- /dev/null +++ b/src/NLogTarget/TargetPropertyWithContext.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.ApplicationInsights.NLogTarget +{ + using NLog.Config; + using NLog.Layouts; + + /// + /// NLog Target Context Property that allows capture of context information for all logevents (Ex. Layout=${threadid}) + /// + [NLogConfigurationItem] + [ThreadAgnostic] + public class TargetPropertyWithContext + { + /// + /// Initializes a new instance of the class. + /// + public TargetPropertyWithContext() : this(null, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the attribute. + /// The layout of the attribute's value. + public TargetPropertyWithContext(string name, Layout layout) + { + this.Name = name; + this.Layout = layout; + } + + /// + /// Gets or sets the name of the attribute. + /// + /// + [RequiredParameter] + public string Name { get; set; } + + /// + /// Gets or sets the layout that will be rendered as the attribute's value. + /// + /// + [RequiredParameter] + public Layout Layout { get; set; } + } +} diff --git a/test/NLogTarget.Net45.Tests/NLogTargetTests.cs b/test/NLogTarget.Net45.Tests/NLogTargetTests.cs index 79ecc37b..cdaa884a 100644 --- a/test/NLogTarget.Net45.Tests/NLogTargetTests.cs +++ b/test/NLogTarget.Net45.Tests/NLogTargetTests.cs @@ -62,7 +62,7 @@ public void InitializeTargetNotThrowsWhenInstrumentationKeyIsEmptyString() Assert.Fail("Expected NLogConfigurationException but none was thrown with message:{0}", ex.Message); } } - + [TestMethod] [TestCategory("NLogTarget")] public void ExceptionsDoNotEscapeNLog() @@ -164,7 +164,7 @@ public void TraceMessageCanBeFormedUsingLayout() target.Layout = @"${uppercase:${level}} ${message}"; Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey("test", null, target); - + aiLogger.Debug("Message"); var telemetry = (TraceTelemetry)this.adapterHelper.Channel.SentItems.FirstOrDefault(); @@ -222,7 +222,9 @@ public void TraceHasCustomProperties() [TestCategory("NLogTarget")] public void GlobalDiagnosticContextPropertiesAreAddedToProperties() { - Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(); + ApplicationInsightsTarget target = new ApplicationInsightsTarget(); + target.ContextProperties.Add(new TargetPropertyWithContext("global_prop", "${gdc:item=global_prop}")); + Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(target: target); NLog.GlobalDiagnosticsContext.Set("global_prop", "global_value"); aiLogger.Debug("Message"); @@ -235,7 +237,9 @@ public void GlobalDiagnosticContextPropertiesAreAddedToProperties() [TestCategory("NLogTarget")] public void GlobalDiagnosticContextPropertiesSupplementEventProperties() { - Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(); + ApplicationInsightsTarget target = new ApplicationInsightsTarget(); + target.ContextProperties.Add(new TargetPropertyWithContext("global_prop", "${gdc:item=global_prop}")); + Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(target: target); NLog.GlobalDiagnosticsContext.Set("global_prop", "global_value"); @@ -254,7 +258,9 @@ public void GlobalDiagnosticContextPropertiesSupplementEventProperties() public void EventPropertyKeyNameIsAppendedWith_1_IfSameAsGlobalDiagnosticContextKeyName() #pragma warning restore CA1707 // Identifiers should not contain underscores { - Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(); + ApplicationInsightsTarget target = new ApplicationInsightsTarget(); + target.ContextProperties.Add(new TargetPropertyWithContext("Name", "${gdc:item=Name}")); + Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(target: target); NLog.GlobalDiagnosticsContext.Set("Name", "Global Value"); var eventInfo = new LogEventInfo(LogLevel.Trace, "TestLogger", "Hello!"); @@ -294,7 +300,7 @@ public void TraceAreEnqueuedInChannelAndContainExceptionMessage() public void CustomMessageIsAddedToExceptionTelemetryCustomProperties() { Logger aiLogger = this.CreateTargetWithGivenInstrumentationKey(); - + try { throw new Exception("Test logging exception"); @@ -313,7 +319,7 @@ public void CustomMessageIsAddedToExceptionTelemetryCustomProperties() public void NLogTraceIsSentAsVerboseTraceItem() { var aiLogger = this.CreateTargetWithGivenInstrumentationKey("F8474271-D231-45B6-8DD4-D344C309AE69"); - + aiLogger.Trace("trace"); var telemetry = (TraceTelemetry)this.adapterHelper.Channel.SentItems.FirstOrDefault();