Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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,27 @@
#nullable enable
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Attributes.get -> System.Collections.Generic.IList<NLog.Targets.TargetPropertyWithContext!>!
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.get -> string
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.get -> string
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.IncludeEventProperties.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventProperties.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeScopeProperties.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeScopeProperties.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 -> string
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.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,27 @@
#nullable enable
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Attributes.get -> System.Collections.Generic.IList<NLog.Targets.TargetPropertyWithContext!>!
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.get -> string?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Endpoint.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.Headers.get -> string?
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.IncludeEventProperties.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeEventProperties.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeFormattedMessage.set -> void
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeScopeProperties.get -> bool
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.IncludeScopeProperties.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 -> string?
OpenTelemetry.AutoInstrumentation.NLogTarget.OpenTelemetryTarget.ServiceName.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,211 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using NLog;
using NLog.Config;
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 LoggerProvider? _loggerProvider;
private static Func<string?, object?>? _getLoggerFactory;

public OpenTelemetryTarget()
{
Layout = "${message}";
}

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

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

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 4401757


public string? Headers { get; set; }

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

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 4401757


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

public string? ServiceName { get; set; }

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

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 4401757


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

Choose a reason for hiding this comment

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

Any reason for defining new Attributes, instead of just relying on existing from base-class ? (Called Properties)

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 4401757


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

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

public new bool IncludeEventProperties { get; set; } = true;

Choose a reason for hiding this comment

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

Any reason for defining new IncludeEventProperties, instead of just relying on existing from base-class ?

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 4401757


public new bool IncludeScopeProperties { get; set; } = true;
Copy link

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

Any reason for defining new IncludeScopeProperties, instead of just relying on existing from base-class ?

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 4401757


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

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 = Endpoint;
Copy link

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

When using NLog Layout, then do it like this:

var endpoint = RenderLogEvent(Endpoint, LogEventInfo.CreateNullEvent());

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 4401757

if (string.IsNullOrEmpty(endpoint))
{
endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
}

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

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);

// Build properties from event properties and context
var properties = new List<KeyValuePair<string, object?>>();
Copy link

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

Maybe extract the logic of filling out var properties into a method. And then optimize that method to skip List-allocation when no properties or IncludeEventProperties = false.

If you remove the overrides of IncludeEventProperties + IncludeScopeProperties + Attributes, and instead call base-class-method. GetAllProperties(logEvent) then it will follow the recommended style

See also: https://github.com/NLog/NLog/wiki/How-to-write-a-custom-target-for-structured-logging

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 4401757

if (IncludeEventProperties && logEvent.HasProperties && logEvent.Properties is not null)

Choose a reason for hiding this comment

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

logEvent.Properties is never null

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 4401757

{
foreach (var kvp in logEvent.Properties)
{
properties.Add(new KeyValuePair<string, object?>(Convert.ToString(kvp.Key)!, kvp.Value));
}
}

// Scope properties can be added via explicit <attribute> entries or NLog's contexts (GDC/MDLC)
foreach (var attribute in Attributes)
{
var value = attribute.Layout?.Render(logEvent);
Copy link

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

instead use the following, or just remove Attributes and instead use GetAllProperties(logEvent).

var value = RenderLogEvent(attribute.Layout, logEvent);
if (!attribute.IncludeEmptyValue && string.IsNullOrEmpty(value))
     continue;

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 4401757

if (!string.IsNullOrEmpty(attribute.Name))
{
properties.Add(new KeyValuePair<string, object?>(attribute.Name!, value));
}
}

var body = IncludeFormattedMessage ? logEvent.FormattedMessage : Convert.ToString(logEvent.Message);
Copy link

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

Instead change to:

var renderedMessage = IncludeFormattedMessage ? RenderLogEvent(Layout, logEvent) : logEvent.Message;

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 4401757


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

var current = Activity.Current;

Choose a reason for hiding this comment

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

This will not work when using NLog AsyncWrapper. Maybe change API to forward SpanId + TraceId to OpenTelemertry ?

        private static readonly string EmptyTraceIdToHexString = default(System.Diagnostics.ActivityTraceId).ToHexString();
        private static readonly string EmptySpanIdToHexString = default(System.Diagnostics.ActivitySpanId).ToHexString();

        public Layout<System.Diagnostics.ActivityTraceId?> TraceId { get; set; } = Layout<System.Diagnostics.ActivityTraceId?>.FromMethod(static evt => System.Diagnostics.Activity.Current?.TraceId is System.Diagnostics.ActivityTraceId activityTraceId && !ReferenceEquals(EmptyTraceIdToHexString, activityTraceId.ToHexString()) ? activityTraceId : null);

        public Layout<System.Diagnostics.ActivitySpanId?> SpanId { get; set; } = Layout<System.Diagnostics.ActivitySpanId?>.FromMethod(static evt => System.Diagnostics.Activity.Current?.SpanId is System.Diagnostics.ActivitySpanId activitySpanId && !ReferenceEquals(EmptySpanIdToHexString, activitySpanId.ToHexString()) ? activitySpanId : null);

And then resolve like this:

            var spanId = RenderLogEvent(SpanId, logEvent);
            if (spanId.HasValue)
                data.SpanId = spanId.Value;
            var traceId = RenderLogEvent(TraceId, logEvent);
            if (traceId.HasValue)
                data.TraceId = traceId.Value;

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 4401757


// Emit using internal helpers via reflection delegate
var renderedMessage = logEvent.FormattedMessage;

Choose a reason for hiding this comment

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

Instead change to:

var renderedMessage = RenderLogEvent(Layout, logEvent);

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 4401757

var args = IncludeEventParameters && logEvent.Parameters is object[] p ? p : null;

Choose a reason for hiding this comment

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

Think you should only IncludeEventParameters when logEvent.HasProperties == false

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 4401757


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 new object();

Choose a reason for hiding this comment

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

This looks weird. Why not allow object? and just discards LogEvents when GetOrCreateLogger returns null ?

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 4401757

}

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

return logger ?? new object();
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ public static class Logs
/// </summary>
public const string EnableLog4NetBridge = "OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE";

/// <summary>
/// Configuration key for whether or not experimental NLog bridge
/// should be enabled.
/// </summary>
public const string EnableNLogBridge = "OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE";

/// <summary>
/// Configuration key for disabling all log instrumentations.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ internal enum LogInstrumentation
/// Log4Net instrumentation.
/// </summary>
Log4Net = 1,

/// <summary>
/// NLog instrumentation.
/// </summary>
NLog = 2,
}
Loading