Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
12b3643
Add NLog instrumentation for OpenTelemetry .NET Auto-Instrumentation
danifitz Aug 6, 2025
dd0eee6
Fix: https://github.com/open-telemetry/opentelemetry-dotnet-instrumen…
danifitz Aug 11, 2025
1b5ee77
Fix https://github.com/open-telemetry/opentelemetry-dotnet-instrument…
danifitz Aug 11, 2025
489fa3b
Fix: https://github.com/open-telemetry/opentelemetry-dotnet-instrumen…
danifitz Aug 11, 2025
638a6aa
Test: https://github.com/open-telemetry/opentelemetry-dotnet-instrume…
danifitz Aug 11, 2025
206be87
feat: refactor NLog instrumentation to use standard NLog Target archi…
danifitz Aug 20, 2025
799ef71
refactor: remove unused NLog.Extensions.Logging from TestApplication.…
danifitz Aug 20, 2025
4401757
refactor: optimize NLog target for async compatibility and performance
danifitz Aug 21, 2025
0fcf644
feat: add hybrid typed layout support for NLog version compatibility
danifitz Aug 27, 2025
4ffa40a
feat: implement NLog v5.3.4+ typed layouts for OpenTelemetryTarget
danifitz Sep 10, 2025
f6cd7d5
fix test coverage
danifitz Sep 10, 2025
665fb09
fix: correct NLog bridge EmitLog method call to match OpenTelemetry SDK
danifitz Sep 10, 2025
3a79c85
refactor: simplify NLog target configuration to use environment varia…
danifitz Sep 11, 2025
f6957e6
Removed NLog from AssemblyInfo
danifitz Sep 24, 2025
b078f85
feat: add NLog instrumentation with duck typing and NLog 6.x support
danifitz Sep 24, 2025
4a0d5a3
Merge branch 'main' into feature/nlog-instrumentation
danifitz Sep 24, 2025
bf571f5
fix CHANGELOG
Kielek Sep 30, 2025
16b004e
Fix build errors
Kielek Sep 30, 2025
e650142
commit generated file
Kielek Sep 30, 2025
c41f73c
Merge branch 'main' into feature/nlog-instrumentation
Kielek Sep 30, 2025
e66da6a
Move NLog version to test folder
Kielek Sep 30, 2025
3d5aff8
cleanup solution
Kielek Sep 30, 2025
ad1c62f
add NLOG to dictionary
Kielek Sep 30, 2025
54dd6da
typo fixes
Kielek Sep 30, 2025
0602818
remove reference to NLog.Extensions.Logging
Kielek Sep 30, 2025
903ab69
Update tested versions
Kielek Sep 30, 2025
4702a68
Minimal assembly version set to 4.0.0
Kielek Sep 30, 2025
951c355
fix sln file
Kielek Sep 30, 2025
29d4251
remove reference to System.Private.Uri
Kielek Sep 30, 2025
29bde76
Fix compilation for tests app
Kielek Sep 30, 2025
911fbd8
Apply suggestions from code review
lachmatt Oct 1, 2025
60311e1
Merge branch 'main' into feature/nlog-instrumentation
Kielek Oct 1, 2025
d912c7e
Merge branch 'main' into feature/nlog-instrumentation
Kielek Oct 1, 2025
c6b7bde
Merge branch 'main' into feature/nlog-instrumentation
Kielek Oct 2, 2025
fad8ea4
Fix issue occurring in VS
Kielek Oct 2, 2025
e3d9274
Add missing settings test case
Kielek Oct 2, 2025
e95e972
remove redundant lines
Kielek Oct 2, 2025
62f933d
Sync implementation with available documentation
Kielek Oct 2, 2025
33546ee
user facing documentation
Kielek Oct 2, 2025
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.25" />
<PackageVersion Include="log4net" Version="3.1.0" />
<PackageVersion Include="MinVer" Version="6.0.0" />
<PackageVersion Include="NLog" Version="5.3.2" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.11" />
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
Expand Down
20 changes: 20 additions & 0 deletions OpenTelemetry.AutoInstrumentation.sln
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkVersionAnalyzer", "tools\SdkVersionAnalyzer\SdkVersionAnalyzer.csproj", "{C75FA076-D460-414B-97F7-6F8D0E85AE74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Log4NetBridge", "test\test-applications\integrations\TestApplication.Log4NetBridge\TestApplication.Log4NetBridge.csproj", "{926B7C03-42C2-4192-94A7-CD0B1C693279}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.NLogBridge", "test\test-applications\integrations\TestApplication.NLogBridge\TestApplication.NLogBridge.csproj", "{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.AutoInstrumentation.NLogTarget", "src\OpenTelemetry.AutoInstrumentation.NLogTarget\OpenTelemetry.AutoInstrumentation.NLogTarget.csproj", "{3C7A3F7B-77E5-4C55-9B2D-1A4A9E7B1D33}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we need to delete this project reference since the .csproj is no longer present. Sorry about that, I had to authorize the workflows to start in order to hit this failure in CI

EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.SelectiveSampler", "test\test-applications\integrations\TestApplication.SelectiveSampler\TestApplication.SelectiveSampler.csproj", "{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}"
EndProject
Expand Down Expand Up @@ -1533,6 +1536,22 @@ Global
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x64.Build.0 = Release|Any CPU
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.ActiveCfg = Release|Any CPU
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.Build.0 = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|ARM64.Build.0 = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.ActiveCfg = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.Build.0 = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.ActiveCfg = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.Build.0 = Debug|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.Build.0 = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|ARM64.ActiveCfg = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|ARM64.Build.0 = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.ActiveCfg = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.Build.0 = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.ActiveCfg = Release|Any CPU
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.Build.0 = Release|Any CPU
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -1639,6 +1658,7 @@ Global
{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
{C75FA076-D460-414B-97F7-6F8D0E85AE74} = {00F4C92D-6652-4BD8-A334-B35D3E711BE6}
{926B7C03-42C2-4192-94A7-CD0B1C693279} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
7 changes: 7 additions & 0 deletions build/LibraryVersions.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ public static partial class LibraryVersion
new("3.1.0"),
]
},
{
"TestApplication.NLogBridge",
[
new("4.7.15"),
new("5.3.2"),
]
},
{
"TestApplication.MassTransit",
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#nullable enable
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventParameters.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventParameters.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxExportBatchSize.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxExportBatchSize.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxQueueSize.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxQueueSize.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.OpenTelemetryTarget() -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Resources.get -> System.Collections.Generic.IList<NLog.Targets.TargetPropertyWithContext!>!
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ScheduledDelayMilliseconds.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ScheduledDelayMilliseconds.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.SpanIdLayout.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.SpanIdLayout.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.TraceIdLayout.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.TraceIdLayout.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.UseHttp.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.UseHttp.set -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#nullable enable
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventParameters.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventParameters.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxExportBatchSize.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxExportBatchSize.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxQueueSize.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.MaxQueueSize.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.OpenTelemetryTarget() -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Resources.get -> System.Collections.Generic.IList<NLog.Targets.TargetPropertyWithContext!>!
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ScheduledDelayMilliseconds.get -> int
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ScheduledDelayMilliseconds.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.SpanIdLayout.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.SpanIdLayout.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.TraceIdLayout.get -> NLog.Layouts.Layout?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.TraceIdLayout.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.UseHttp.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.UseHttp.set -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworks)</TargetFrameworks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<Description>OpenTelemetry NLog target that forwards NLog LogEvents to OpenTelemetry Logs.</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\OpenTelemetry.AutoInstrumentation\OpenTelemetry.AutoInstrumentation.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NLog" />
</ItemGroup>
</Project>

Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NLog;
using NLog.Config;
using NLog.Layouts;
using NLog.Targets;
using OpenTelemetry;
using OpenTelemetry.AutoInstrumentation;
using OpenTelemetry.AutoInstrumentation.Configurations;
using OpenTelemetry.Logs;

namespace OpenTelemetry.AutoInstrumentation.NLogTarget;

[Target("OpenTelemetryTarget")]
public sealed class OpenTelemetryTarget : TargetWithContext
{
private static readonly ConcurrentDictionary<string, object> LoggerCache = new(StringComparer.Ordinal);
private static readonly string EmptyTraceIdToHexString = default(ActivityTraceId).ToHexString();
private static readonly string EmptySpanIdToHexString = default(ActivitySpanId).ToHexString();
private static LoggerProvider? _loggerProvider;
private static Func<string?, object?>? _getLoggerFactory;

public OpenTelemetryTarget()
{
Layout = "${message}";
TraceIdLayout = "${activity:property=TraceId}";
SpanIdLayout = "${activity:property=SpanId}";
}

[RequiredParameter]
public Layout? Endpoint { get; set; }

public Layout? Headers { get; set; }

public bool UseHttp { get; set; } = true;

public Layout? ServiceName { get; set; }

[ArrayParameter(typeof(TargetPropertyWithContext), "resource")]
public IList<TargetPropertyWithContext> Resources { get; } = new List<TargetPropertyWithContext>();

public bool IncludeFormattedMessage { get; set; } = true;

public bool IncludeEventParameters { get; set; } = true;

public Layout? TraceIdLayout { get; set; }

public Layout? SpanIdLayout { get; set; }

public int ScheduledDelayMilliseconds { get; set; } = 5000;

public int MaxQueueSize { get; set; } = 2048;

public int MaxExportBatchSize { get; set; } = 512;

protected override void InitializeTarget()
{
base.InitializeTarget();

if (_loggerProvider != null)
{
return;
}

var createLoggerProviderBuilderMethod = typeof(Sdk).GetMethod("CreateLoggerProviderBuilder", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!;
var loggerProviderBuilder = (LoggerProviderBuilder)createLoggerProviderBuilderMethod.Invoke(null, null)!;

loggerProviderBuilder = loggerProviderBuilder
.SetResourceBuilder(ResourceConfigurator.CreateResourceBuilder(Instrumentation.GeneralSettings.Value.EnabledResourceDetectors));

loggerProviderBuilder = loggerProviderBuilder.AddOtlpExporter(options =>
{
var endpoint = RenderLogEvent(Endpoint, LogEventInfo.CreateNullEvent());
if (string.IsNullOrEmpty(endpoint))
{
endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
}

if (!string.IsNullOrEmpty(endpoint))
{
options.Endpoint = new Uri(endpoint!, UriKind.RelativeOrAbsolute);
}

var headers = RenderLogEvent(Headers, LogEventInfo.CreateNullEvent());
if (!string.IsNullOrEmpty(headers))
{
options.Headers = headers;
}

options.Protocol = UseHttp ? OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf : OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = ScheduledDelayMilliseconds;
options.BatchExportProcessorOptions.MaxQueueSize = MaxQueueSize;
options.BatchExportProcessorOptions.MaxExportBatchSize = MaxExportBatchSize;
});

_loggerProvider = loggerProviderBuilder.Build();
_getLoggerFactory = CreateGetLoggerDelegate(_loggerProvider);
}

protected override void Write(LogEventInfo logEvent)
{
if (_loggerProvider is null)
{
return;
}

if (Sdk.SuppressInstrumentation)
{
return;
}

var logger = GetOrCreateLogger(logEvent.LoggerName);
if (logger is null)
{
return;
}

var properties = GetLogEventProperties(logEvent);

var body = IncludeFormattedMessage ? RenderLogEvent(Layout, logEvent) : Convert.ToString(logEvent.Message);

var severityText = logEvent.Level.Name;
var severityNumber = MapLogLevelToSeverity(logEvent.Level);

// Resolve trace context using Layout properties (works with AsyncWrapper)
Activity? current = null;
var traceIdString = RenderLogEvent(TraceIdLayout, logEvent);
var spanIdString = RenderLogEvent(SpanIdLayout, logEvent);

if (!string.IsNullOrEmpty(traceIdString) && !string.IsNullOrEmpty(spanIdString) &&
traceIdString != EmptyTraceIdToHexString && spanIdString != EmptySpanIdToHexString)
{
try
{
var traceId = ActivityTraceId.CreateFromString(traceIdString);
var spanId = ActivitySpanId.CreateFromString(spanIdString);
var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded);
Copy link

@snakefoot snakefoot Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for not using typed Layout, instead of introducing the overhead of parsing from string?

public Layout<System.Diagnostics.ActivityTraceId?> TraceId { get; set; }
public Layout<System.Diagnostics.ActivitySpanId?> SpanId { get; set; }

Feature introduced with NLog v5.3.4

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snakefoot I think we want to support older versions of NLog too? I've also been testing against 4.x versions. I can introduce some conditional behaviour by trying to detect the version through reflection or runtime version detection but that might add complexity we don't necessarily want. WDYT?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NLog v5.3.4 is from 12 September 2024. Almost a year old now. Sounds strange to support even older versions when wanting to use "new" technology like OpenTelemetry.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My observation is that there are plenty of enterprise companies with older versions of .NET/Nlog who want to use OpenTelemetry. However I am fine with using typed Layout for now and see if we get any issues raised on the repo looking for support for older NLog version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 0fcf644

I tried to take a hybrid approach:

NLog Hybrid Layout Implementation

Version Detection

  • Runtime Check: Detects NLog version using typeof(Layout).Assembly.GetName().Version
  • Threshold: NLog 5.3.4+ supports Layout<T> typed layouts
  • Fail-Safe: Defaults to string parsing if version detection fails

Backward Compatibility

  • Public API: Unchanged Layout? TraceIdLayout and Layout? SpanIdLayout properties
  • Version Range: Supports NLog 4.0.0 through 6..

Fallback Strategy

NLog 5.3.4+ (Modern)

// Direct typed rendering - no parsing overhead
traceId = _typedTraceIdLayout.Render(logEvent);
spanId = _typedSpanIdLayout.Render(logEvent);

NLog 4.0.0 - 5.3.3 (Legacy)

// String rendering + parsing with error handling
var traceIdString = RenderLogEvent(TraceIdLayout, logEvent);
traceId = ActivityTraceId.CreateFromString(traceIdString);

Let me know your thoughts!

Copy link

@snakefoot snakefoot Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimization with type Layout comes from using Layout<System.Diagnostics.ActivityTraceId?>.FromMethod(...)

By assigning _typedTraceIdLayout = "${activity:property=TraceId}" then it will attempt to parse from string (and will probably fail)

There are many bonuses from using NLog v5, one is the support for .NET build-trimming.

current = new Activity("OpenTelemetryTarget").SetParentId(activityContext.TraceId, activityContext.SpanId, activityContext.TraceFlags);
}
catch
{
// If parsing fails, fall back to Activity.Current
current = Activity.Current;
}
}
else
{
current = Activity.Current;
}

// Emit using internal helpers via reflection delegate
var renderedMessage = IncludeFormattedMessage ? RenderLogEvent(Layout, logEvent) : logEvent.Message;
var args = IncludeEventParameters && !logEvent.HasProperties && logEvent.Parameters is object[] p ? p : null;

OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.OpenTelemetryLogHelpers.LogEmitter?.Invoke(
logger,
body,
logEvent.TimeStamp,
severityText,
severityNumber,
logEvent.Exception,
properties,
current,
args,
renderedMessage);
}

private static int MapLogLevelToSeverity(LogLevel level)
{
// Map NLog ordinals 0..5 to OTEL severity 1..24 approximate buckets
return level.Ordinal switch
{
0 => 1, // Trace
1 => 5, // Debug
2 => 9, // Info
3 => 13, // Warn
4 => 17, // Error
5 => 21, // Fatal
_ => 9
};
}

private static Func<string?, object?>? CreateGetLoggerDelegate(LoggerProvider loggerProvider)
{
try
{
var methodInfo = typeof(LoggerProvider)
.GetMethod("GetLogger", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new[] { typeof(string) }, null)!;
return (Func<string?, object?>)methodInfo.CreateDelegate(typeof(Func<string?, object?>), loggerProvider);
}
catch
{
return null;
}
}

private object? GetOrCreateLogger(string? loggerName)
{
var key = loggerName ?? string.Empty;
if (LoggerCache.TryGetValue(key, out var logger))
{
return logger;
}

var factory = _getLoggerFactory;
if (factory is null)
{
return null;
}

logger = factory(loggerName);
if (logger is not null)
{
LoggerCache[key] = logger;
}

return logger;
}

private IEnumerable<KeyValuePair<string, object?>>? GetLogEventProperties(LogEventInfo logEvent)
{
// Check HasProperties first to avoid allocating empty dictionary
if (!logEvent.HasProperties && ContextProperties.Count == 0)
{
return null;
}

var allProperties = GetAllProperties(logEvent);
if (allProperties.Count == 0)
{
return null;
}

return allProperties.Select(kvp => new KeyValuePair<string, object?>(Convert.ToString(kvp.Key)!, kvp.Value));
Copy link

@snakefoot snakefoot Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice GetAllProperties returns Dictionary<string, object>, so you can just return the dictionary without doing Linq.Select and Convert.ToString

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 0fcf644

}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.Integrations.LoggerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.AsyncDefaultBasicConsumerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.DefaultBasicConsumerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.ModelBaseBasicGetIntegration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.Integrations.LoggerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.AsyncDefaultBasicConsumerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.DefaultBasicConsumerIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMqLegacy.Integrations.ModelBaseBasicGetIntegration
Expand Down
Loading