Skip to content
Merged
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup Label="Runtime">
<PackageVersion Include="Microsoft.ApplicationInsights" Version="2.23.0" />
<PackageVersion Include="NLog" Version="5.5.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup Label="Test">
<PackageVersion Include="WireMock.Net" Version="1.6.9" />
Expand Down
4 changes: 3 additions & 1 deletion src/NLogTarget/ApplicationInsightsTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@
propertyBag.Add("LoggerName", logEvent.LoggerName);
}

if (logEvent.UserStackFrame != null)

Check warning on line 106 in src/NLogTarget/ApplicationInsightsTarget.cs

View workflow job for this annotation

GitHub Actions / build

'LogEventInfo.UserStackFrame' is obsolete: 'Instead use ${callsite} or CallerMemberName. Marked obsolete with NLog 5.3'
{
propertyBag.Add("UserStackFrame", logEvent.UserStackFrame.ToString());

Check warning on line 108 in src/NLogTarget/ApplicationInsightsTarget.cs

View workflow job for this annotation

GitHub Actions / build

'LogEventInfo.UserStackFrame' is obsolete: 'Instead use ${callsite} or CallerMemberName. Marked obsolete with NLog 5.3'
propertyBag.Add("UserStackFrameNumber", logEvent.UserStackFrameNumber.ToString(CultureInfo.InvariantCulture));

Check warning on line 109 in src/NLogTarget/ApplicationInsightsTarget.cs

View workflow job for this annotation

GitHub Actions / build

'LogEventInfo.UserStackFrameNumber' is obsolete: 'Instead use ${callsite} or CallerMemberName. Marked obsolete with NLog 5.4'
}
else
{
Expand Down Expand Up @@ -138,7 +138,7 @@
if (this.ExcludeProperties?.Contains(propertyKey) == true)
continue;

TryAddPropertyToPropertyBag(propertyBag, propertyKey, property.Value);

Check warning on line 141 in src/NLogTarget/ApplicationInsightsTarget.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'propertyName' in 'void ApplicationInsightsTarget.TryAddPropertyToPropertyBag(IDictionary<string, string> propertyBag, string propertyName, object propertyValue)'.
}
}

Expand Down Expand Up @@ -172,7 +172,9 @@
while (propertyBag.ContainsKey(propertyName));
}

propertyBag[propertyName] = Convert.ToString(propertyValue, CultureInfo.InvariantCulture);
// Use StringDictionaryConverter to properly serialize complex objects to JSON
var converter = new StringDictionaryConverter(propertyBag);
converter[propertyName] = propertyValue;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/NLogTarget/NLogTarget.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="NLog" />
<PackageReference Include="Microsoft.ApplicationInsights" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>

<Import Project="..\CommonShared\CommonShared.projitems" Label="Shared" />
Expand Down
27 changes: 26 additions & 1 deletion src/NLogTarget/StringDictionaryConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.ApplicationInsights.NLogTarget
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json;

/// <summary>
/// Converts from NLog Object-properties to ApplicationInsight String-properties
Expand Down Expand Up @@ -101,7 +102,31 @@ private static string SafeValueConverter(object value)
{
try
{
return Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture);
if (value == null)
{
return string.Empty;
}

// Handle primitive types and strings directly
if (value is string str)
{
return str;
}

// Check if the value is a primitive type or a simple type that Convert.ToString handles well
var type = value.GetType();
if (type.IsPrimitive || type.IsEnum || value is decimal || value is DateTime || value is DateTimeOffset || value is Guid || value is TimeSpan)
{
return Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture);
}

// For complex objects, serialize to JSON
return JsonSerializer.Serialize(value, new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = null,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never
});
}
catch
{
Expand Down
73 changes: 62 additions & 11 deletions src/NLogTarget/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,38 @@
"resolved": "5.5.0",
"contentHash": "FCH8s7GWlonH5JXV9/EpeNJ8pRZQMVZOSWX3JrHPU8rzdHJhS5+lUGGvJIUOtzkGV1clYBFR0WXOI5FnUwVCMA=="
},
"System.Text.Json": {
"type": "Direct",
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "1Dpjwq9peG/Wt5BNbrzIhTpclfOSqBWZsUO28vVr59yQlkvL5jLBWfpfzRmJ1OY+6DciaY0DUcltyzs4fuZHjw==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "10.0.0",
"System.Buffers": "4.6.1",
"System.IO.Pipelines": "10.0.0",
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2",
"System.Text.Encodings.Web": "10.0.0",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "10.0.0",
"contentHash": "vFuwSLj9QJBbNR0NeNO4YVASUbokxs+i/xbuu8B+Fs4FAZg5QaFa6eGrMaRqTzzNI5tAb97T7BhSxtLckFyiRA==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
Expand All @@ -45,25 +68,53 @@
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "10.0.0",
"contentHash": "M1eb3nfXntaRJPrrMVM9EFS8I1bDTnt0uvUS6QP/SicZf/ZZjydMD5NiXxfmwW/uQwaMDP/yX2P+zQN1NBHChg==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Memory": "4.6.3",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "10.0.0",
"contentHash": "257hh1ep1Gqm1Lm0ulxf7vVBVMJuGN6EL4xSWjpi46DffXzm1058IiWsfSC06zSm7SniN+Tb5160UnXsSa8rRg==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.6.3",
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions test/NLogTarget.Tests/NLogTargetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

[TestClass]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposing the object on the TestCleanup method")]
public class ApplicationInsightsTargetTests

Check warning on line 22 in test/NLogTarget.Tests/NLogTargetTests.cs

View workflow job for this annotation

GitHub Actions / build

Because an application's API isn't typically referenced from outside the assembly, types can be made internal (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1515)
{
private AdapterHelper adapterHelper;

Expand Down Expand Up @@ -233,6 +233,88 @@
Assert.AreEqual("Value", telemetry.Properties["Name"]);
}

[TestMethod]
[TestCategory("NLogTarget")]
public void TraceHasComplexPropertiesSerializedAsJson()
{
var aiLogger = this.CreateTargetWithGivenConnectionString();

var complexObject = new
{
Name = "John Doe",
Age = 30,
Address = new
{
Street = "123 Main St",
City = "New York",
ZipCode = "10001"
},
Tags = new[] { "tag1", "tag2", "tag3" }
};

var eventInfo = new LogEventInfo(LogLevel.Trace, "TestLogger", "Hello!");
eventInfo.Properties["ComplexObject"] = complexObject;
eventInfo.Properties["SimpleString"] = "SimpleValue";
aiLogger.Log(eventInfo);

var telemetry = this.adapterHelper.Channel.SentItems.FirstOrDefault() as TraceTelemetry;
Assert.IsNotNull(telemetry, "Didn't get the log event from the channel");

// Simple string should remain as is
Assert.AreEqual("SimpleValue", telemetry.Properties["SimpleString"]);

// Complex object should be serialized to JSON
Assert.IsTrue(telemetry.Properties.ContainsKey("ComplexObject"), "ComplexObject property not found");
var complexJson = telemetry.Properties["ComplexObject"];

// Verify it's JSON and contains expected properties
Assert.IsTrue(complexJson.Contains("\"Name\""), "JSON should contain Name property");
Assert.IsTrue(complexJson.Contains("\"John Doe\""), "JSON should contain Name value");
Assert.IsTrue(complexJson.Contains("\"Age\""), "JSON should contain Age property");
Assert.IsTrue(complexJson.Contains("30"), "JSON should contain Age value");
Assert.IsTrue(complexJson.Contains("\"Address\""), "JSON should contain Address property");
Assert.IsTrue(complexJson.Contains("\"Street\""), "JSON should contain nested Street property");
Assert.IsTrue(complexJson.Contains("\"123 Main St\""), "JSON should contain nested Street value");
Assert.IsTrue(complexJson.Contains("\"Tags\""), "JSON should contain Tags array");
Assert.IsTrue(complexJson.Contains("\"tag1\""), "JSON should contain array values");
}

[TestMethod]
[TestCategory("NLogTarget")]
public void TraceHandlesVariousPropertyTypes()
{
var aiLogger = this.CreateTargetWithGivenConnectionString();

var eventInfo = new LogEventInfo(LogLevel.Info, "TestLogger", "Testing various types");
eventInfo.Properties["String"] = "Simple string";
eventInfo.Properties["Int"] = 42;
eventInfo.Properties["Bool"] = true;
eventInfo.Properties["Decimal"] = 3.14m;
eventInfo.Properties["DateTime"] = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
eventInfo.Properties["Array"] = new[] { 1, 2, 3 };
eventInfo.Properties["Dictionary"] = new Dictionary<string, object> { { "key1", "value1" }, { "key2", 123 } };
eventInfo.Properties["Null"] = null;

aiLogger.Log(eventInfo);

var telemetry = this.adapterHelper.Channel.SentItems.FirstOrDefault() as TraceTelemetry;
Assert.IsNotNull(telemetry, "Didn't get the log event from the channel");

// Simple types should be preserved as strings
Assert.AreEqual("Simple string", telemetry.Properties["String"]);
Assert.AreEqual("42", telemetry.Properties["Int"]);
Assert.AreEqual("True", telemetry.Properties["Bool"]);
Assert.IsTrue(telemetry.Properties["Decimal"].Contains("3.14"), "Decimal value should contain 3.14");

// Complex types should be serialized to JSON
Assert.IsTrue(telemetry.Properties["Array"].Contains("["), "Array should be JSON array");
Assert.IsTrue(telemetry.Properties["Array"].Contains("1"), "Array should contain values");
Assert.IsTrue(telemetry.Properties["Dictionary"].Contains("key1"), "Dictionary should be serialized");
Assert.IsTrue(telemetry.Properties["Dictionary"].Contains("value1"), "Dictionary should contain values");

// Null should be empty string
Assert.AreEqual(string.Empty, telemetry.Properties["Null"]);
}

[TestMethod]
[TestCategory("NLogTarget")]
Expand Down
Loading
Loading