Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NLog Target support ConnectionString with isolated TelemetryClient #2897

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ApplicationIn
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ContextProperties.get -> System.Collections.Generic.IList<Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext>
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.InstrumentationKey.get -> string
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.InstrumentationKey.set -> void
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ConnectionString.get -> string
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ConnectionString.set -> void
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext.Layout.get -> NLog.Layouts.Layout
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext.Layout.set -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ApplicationIn
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ContextProperties.get -> System.Collections.Generic.IList<Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext>
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.InstrumentationKey.get -> string
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.InstrumentationKey.set -> void
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ConnectionString.get -> string
Microsoft.ApplicationInsights.NLogTarget.ApplicationInsightsTarget.ConnectionString.set -> void
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext.Layout.get -> NLog.Layouts.Layout
Microsoft.ApplicationInsights.NLogTarget.TargetPropertyWithContext.Layout.set -> void
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## VNext
- [Populate required field Message with "n/a" if it is empty](https://github.com/microsoft/ApplicationInsights-dotnet/issues/1066)
- [NLog Target with support for specifying ConnectionString](https://github.com/microsoft/ApplicationInsights-dotnet/issues/2897)

## Version 2.22.0
- no changes since beta.
Expand Down
28 changes: 27 additions & 1 deletion LOGGING/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,26 @@ Application Insights NLog Target nuget package adds ApplicationInsights target i

If your application does not have web.config then it can also be configured manually.

* **Configure ApplicationInsightsTarget using NLog.config** :
* **Configure ApplicationInsightsTarget with ConnectionString using NLog.config** :
ConnectionString is the preferred approach to write logs into Application Insights. If the NLog Application Insights target has connectionString in the config file then TelemetryClient will use it, otherwise it will use the instrumentationKey.

```xml
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<extensions>
<add assembly="Microsoft.ApplicationInsights.NLogTarget" />
</extensions>
<targets>
<target xsi:type="ApplicationInsightsTarget" name="aiTarget">
<connectionString>Your_ApplicationInsights_ConnectionString</connectionString> <!-- Only required if not using ApplicationInsights.config -->
<contextproperty name="threadid" layout="${threadid}" /> <!-- Can be repeated with more context -->
</target>
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="aiTarget" />
</rules>
</nlog>
```
* **Configure ApplicationInsightsTarget using NLog.config** :

```xml
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
Expand Down Expand Up @@ -70,6 +89,10 @@ For more information see:


```csharp
// You need this only if you did not define ConnectionString or InstrumentationKey in ApplicationInsights.config (Or in the NLog.config)
TelemetryConfiguration.Active.ConnectionString = "Your_ApplicationInsights_ConnectionString";

or
// You need this only if you did not define InstrumentationKey in ApplicationInsights.config (Or in the NLog.config)
TelemetryConfiguration.Active.InstrumentationKey = "Your_Resource_Key";

Expand All @@ -85,6 +108,9 @@ If you configure NLog programmatically with the [NLog Config API](https://github
var config = new LoggingConfiguration();

ApplicationInsightsTarget target = new ApplicationInsightsTarget();
// You need this only if you did not define Application Insights ConnectionString in ApplicationInsights.config or want to use different connectionstring
target.ConnectionString = "Your_ApplicationInsights_ConnectionString";
or
// You need this only if you did not define InstrumentationKey in ApplicationInsights.config or want to use different instrumentation key
target.InstrumentationKey = "Your_Resource_Key";

Expand Down
90 changes: 62 additions & 28 deletions LOGGING/src/NLogTarget/ApplicationInsightsTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Microsoft.ApplicationInsights.NLogTarget

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
using Microsoft.ApplicationInsights.Implementation;

Expand All @@ -29,8 +30,9 @@ namespace Microsoft.ApplicationInsights.NLogTarget
public sealed class ApplicationInsightsTarget : TargetWithLayout
{
private TelemetryClient telemetryClient;
private DateTime lastLogEventTime;
private TelemetryConfiguration telemetryConfiguration;
private NLog.Layouts.Layout instrumentationKeyLayout = string.Empty;
private NLog.Layouts.Layout connectionStringLayout = string.Empty;

/// <summary>
/// Initializers a new instance of ApplicationInsightsTarget type.
Expand All @@ -50,19 +52,25 @@ public string InstrumentationKey
set => this.instrumentationKeyLayout = value ?? string.Empty;
}

/// <summary>
/// Gets or sets the Application Insights connectionstring for your application.
/// </summary>
public string ConnectionString
{
get => (this.connectionStringLayout as NLog.Layouts.SimpleLayout)?.Text ?? null;
set => this.connectionStringLayout = value ?? string.Empty;
}

/// <summary>
/// Gets the array of custom attributes to be passed into the logevent context.
/// </summary>
[ArrayParameter(typeof(TargetPropertyWithContext), "contextproperty")]
public IList<TargetPropertyWithContext> ContextProperties { get; } = new List<TargetPropertyWithContext>();

/// <summary>
/// Gets the logging controller we will be using.
/// Gets or sets the factory for creating TelemetryConfiguration, so unit-tests can override in-memory-channel.
/// </summary>
internal TelemetryClient TelemetryClient
{
get { return this.telemetryClient; }
}
internal Func<TelemetryConfiguration> TelemetryConfigurationFactory { get; set; }

internal void BuildPropertyBag(LogEventInfo logEvent, ITelemetry trace)
{
Expand Down Expand Up @@ -108,25 +116,49 @@ internal void BuildPropertyBag(LogEventInfo logEvent, ITelemetry trace)
}

/// <summary>
/// Initializes the Target and perform instrumentationKey validation.
/// Initializes the Target and configures TelemetryClient.
/// </summary>
/// <exception cref="NLogConfigurationException">Will throw when <see cref="InstrumentationKey"/> is not set.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "ApplicationInsightsTarget class handles ownership of TelemetryConfiguration with Dispose.")]
protected override void InitializeTarget()
{
base.InitializeTarget();
#pragma warning disable CS0618 // Type or member is obsolete: TelemtryConfiguration.Active is used in TelemetryClient constructor.
this.telemetryClient = new TelemetryClient();
#pragma warning restore CS0618 // Type or member is obsolete

string instrumentationKey = this.instrumentationKeyLayout.Render(LogEventInfo.CreateNullEvent());
if (!string.IsNullOrWhiteSpace(instrumentationKey))
string connectionString = this.connectionStringLayout.Render(LogEventInfo.CreateNullEvent());

// Check if nlog application insights target has connectionstring in config file then
// configure new telemetryclient with the connectionstring otherwise using legacy instrumentationkey.
if (!string.IsNullOrWhiteSpace(connectionString))
{
this.telemetryClient.Context.InstrumentationKey = instrumentationKey;
this.telemetryConfiguration = this.TelemetryConfigurationFactory?.Invoke() ?? TelemetryConfiguration.CreateDefault();
this.telemetryConfiguration.ConnectionString = connectionString;
this.telemetryClient = new TelemetryClient(this.telemetryConfiguration);
}
else
{
#pragma warning disable CS0618 // Type or member is obsolete: TelemtryConfiguration.Active is used in TelemetryClient constructor.
this.telemetryClient = new TelemetryClient();
#pragma warning restore CS0618 // Type or member is obsolete
string instrumentationKey = this.instrumentationKeyLayout.Render(LogEventInfo.CreateNullEvent());
if (!string.IsNullOrWhiteSpace(instrumentationKey))
{
this.telemetryClient.Context.InstrumentationKey = instrumentationKey;
}
}

this.telemetryClient.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("nlog:");
}

/// <summary>
/// Closes the target and releases resources used by the current instance of the <see cref="ApplicationInsightsTarget"/> class.
/// </summary>
protected override void CloseTarget()
{
this.telemetryConfiguration?.Dispose();
this.telemetryConfiguration = null;

base.CloseTarget();
}

/// <summary>
/// Send the log message to Application Insights.
/// </summary>
Expand All @@ -138,8 +170,6 @@ protected override void Write(LogEventInfo logEvent)
throw new ArgumentNullException(nameof(logEvent));
}

this.lastLogEventTime = DateTime.UtcNow;

if (logEvent.Exception != null)
{
this.SendException(logEvent);
Expand All @@ -163,25 +193,29 @@ protected override void FlushAsync(AsyncContinuation asyncContinuation)

try
{
this.TelemetryClient.Flush();
if (DateTime.UtcNow.AddSeconds(-30) > this.lastLogEventTime)
{
// Nothing has been written, so nothing to wait for
asyncContinuation(null);
}
else
{
// Documentation says it is important to wait after flush, else nothing will happen
// https://docs.microsoft.com/azure/application-insights/app-insights-api-custom-events-metrics#flushing-data
System.Threading.Tasks.Task.Delay(TimeSpan.FromMilliseconds(500)).ContinueWith((task) => asyncContinuation(null));
}
this.telemetryClient.FlushAsync(System.Threading.CancellationToken.None).ContinueWith(t => asyncContinuation(t.Exception));
}
catch (Exception ex)
{
asyncContinuation(ex);
}
}

/// <summary>
/// Releases resources used by the current instance of the <see cref="ApplicationInsightsTarget"/> class.
/// </summary>
/// <param name="disposing">Dispose managed state (managed objects).</param>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
this.telemetryConfiguration?.Dispose();
this.telemetryConfiguration = null;
}
}

private static void LoadLogEventProperties(LogEventInfo logEvent, IDictionary<string, string> propertyBag)
{
if (logEvent.Properties?.Count > 0)
Expand Down
41 changes: 38 additions & 3 deletions LOGGING/test/NLogTarget.Tests/NLogTargetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public void TraceHasCustomProperties()
}


[TestMethod]
[TestMethod]
[TestCategory("NLogTarget")]
public void GlobalDiagnosticContextPropertiesAreAddedToProperties()
{
Expand Down Expand Up @@ -455,8 +455,14 @@ public void NLogTargetFlushesTelemetryClient()
NLog.Common.AsyncContinuation asyncContinuation = (ex) => { flushException = ex; flushEvent.Set(); };
aiLogger.Factory.Flush(asyncContinuation, 5000);
Assert.IsTrue(flushEvent.WaitOne(5000));
Assert.IsNotNull(flushException);
Assert.AreEqual("Flush called", flushException.Message);
}

[TestMethod]
[TestCategory("NLogTarget")]
public void NLogTargetWithConnectionString()
{
var aiLogger = this.CreateTargetWithGivenConnectionString("TestAI");
VerifyMessagesInMockChannel(aiLogger, "TestAI");
}

private void VerifyMessagesInMockChannel(Logger aiLogger, string instrumentationKey)
Expand Down Expand Up @@ -504,5 +510,34 @@ private Logger CreateTargetWithGivenInstrumentationKey(

return aiLogger;
}

private Logger CreateTargetWithGivenConnectionString(
string instrumentationKey = "TEST",
Action<Logger> loggerAction = null)
{
ApplicationInsightsTarget target = new ApplicationInsightsTarget
{
ConnectionString = $"InstrumentationKey={instrumentationKey};IngestionEndpoint=https://localhost/;LiveEndpoint=https://localhost/"
};

target.TelemetryConfigurationFactory = () => new TelemetryConfiguration() { TelemetryChannel = this.adapterHelper.Channel };

LoggingRule rule = new LoggingRule("*", LogLevel.Trace, target);
LoggingConfiguration config = new LoggingConfiguration();
config.AddTarget("AITarget", target);
config.LoggingRules.Add(rule);

LogManager.Configuration = config;
Logger aiLogger = LogManager.GetLogger("AITarget");

if (loggerAction != null)
{
loggerAction(aiLogger);
target.Dispose();
return null;
}

return aiLogger;
}
}
}
Loading