diff --git a/.cspell/other.txt b/.cspell/other.txt
index a0b3134eed..99615fb1de 100644
--- a/.cspell/other.txt
+++ b/.cspell/other.txt
@@ -41,6 +41,7 @@ mycompanymyproductmylibrary
MYSQLCONNECTOR
MYSQLDATA
NETRUNTIME
+NLOG
Npgsql
NSERVICEBUS
omnisharp
@@ -58,9 +59,9 @@ protos
RABBITMQ
Serilog
spdlog
-srcs
SQLCLIENT
sqlserver
+srcs
STACKEXCHANGEREDIS
TMPDIR
tracesexporter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70a60dc041..d119cd1c82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ This component adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.h
### Added
+- Support for [`NLog`](https://www.nuget.org/packages/NLog/)
+ logs instrumentation for versions `5.*` and `6.*` on .NET using duck typing
+ for zero-config auto-injection.
+
### Changed
#### Dependency updates
diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln
index e723a054e7..02bcebad1e 100644
--- a/OpenTelemetry.AutoInstrumentation.sln
+++ b/OpenTelemetry.AutoInstrumentation.sln
@@ -247,6 +247,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkVersionAnalyzer", "tools
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Log4NetBridge", "test\test-applications\integrations\TestApplication.Log4NetBridge\TestApplication.Log4NetBridge.csproj", "{926B7C03-42C2-4192-94A7-CD0B1C693279}"
EndProject
+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}") = "TestApplication.SelectiveSampler", "test\test-applications\integrations\TestApplication.SelectiveSampler\TestApplication.SelectiveSampler.csproj", "{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.ProfilerSpanStoppageHandling", "test\test-applications\integrations\TestApplication.ProfilerSpanStoppageHandling\TestApplication.ProfilerSpanStoppageHandling.csproj", "{665280EB-F428-4C04-A293-33228C73BF8A}"
@@ -1535,6 +1537,22 @@ Global
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x64.Build.0 = Release|x64
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.ActiveCfg = Release|x86
{926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.Build.0 = Release|x86
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.Build.0 = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|ARM64.ActiveCfg = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|ARM64.Build.0 = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.ActiveCfg = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.Build.0 = Debug|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.ActiveCfg = Debug|x86
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.Build.0 = Debug|x86
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.ActiveCfg = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.Build.0 = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|ARM64.ActiveCfg = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|ARM64.Build.0 = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.ActiveCfg = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.Build.0 = Release|x64
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.ActiveCfg = Release|x86
+ {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.Build.0 = Release|x86
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|Any CPU.ActiveCfg = Debug|x64
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|Any CPU.Build.0 = Debug|x64
{FD1A1ABD-6A48-4E94-B5F7-2081AFCD1BBB}.Debug|ARM64.ActiveCfg = Debug|x64
@@ -1657,6 +1675,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}
{665280EB-F428-4C04-A293-33228C73BF8A} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
EndGlobalSection
diff --git a/build/LibraryVersions.g.cs b/build/LibraryVersions.g.cs
index 8cd4785459..7acacaa866 100644
--- a/build/LibraryVersions.g.cs
+++ b/build/LibraryVersions.g.cs
@@ -68,6 +68,15 @@ public static partial class LibraryVersion
new("3.2.0"),
]
},
+ {
+ "TestApplication.NLogBridge",
+ [
+ new("5.0.0"),
+ new("5.3.4"),
+ new("6.0.0"),
+ new("6.0.4"),
+ ]
+ },
{
"TestApplication.MassTransit",
[
diff --git a/docs/config.md b/docs/config.md
index 5c66d05509..0084e76c82 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -207,6 +207,7 @@ due to lack of stable semantic convention.
|-----------|---------------------------------------------------------------------------------------------------------------------------------|--------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `ILOGGER` | [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) **Not supported on .NET Framework** | ≥9.0.0 | bytecode or source \[1\] | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
| `LOG4NET` | [log4net](https://www.nuget.org/packages/log4net) \[2\] | ≥2.0.13 && < 4.0.0 | bytecode | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
+| `NLOG` | [NLog](https://www.nuget.org/packages/NLog) \[3\] | ≥5.0.0 && < 7.0.0 | bytecode | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) |
\[1\]: For ASP.NET Core applications, the `LoggingBuilder` instrumentation
can be enabled without using the .NET CLR Profiler by setting
@@ -216,6 +217,9 @@ the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable to
\[2\]: Instrumentation provides both [trace context injection](./log-trace-correlation.md#log4net-trace-context-injection)
and [logs bridge](./log4net-bridge.md).
+\[3\]: Instrumentation provides both [trace context injection](./log-trace-correlation.md#log4net-trace-context-injection)
+and [logs bridge](./nlog-bridge.md).
+
### Instrumentation options
| Environment variable | Description | Default value | Status |
diff --git a/docs/log-trace-correlation.md b/docs/log-trace-correlation.md
index 0be7785132..9bbd58f620 100644
--- a/docs/log-trace-correlation.md
+++ b/docs/log-trace-correlation.md
@@ -61,3 +61,26 @@ Following properties are set by default on the collection of logging event's pro
This allows for trace context to be logged into currently configured log destination,
e.g. a file. In order to use them, pattern needs to be updated.
+
+### `NLog`
+
+See [`nlog-bridge`](./nlog-bridge.md).
+
+## `NLog` trace context injection
+
+> [!IMPORTANT]
+> NLog trace context injection is an experimental feature.
+
+The `NLog` trace context injection is enabled by default.
+It can be disabled by setting `OTEL_DOTNET_AUTO_LOGS_NLOG_INSTRUMENTATION_ENABLED` to `false`.
+
+Context injection is supported for `NLOG` in versions >= 5.0.0 && < 7.0.0
+
+Following properties are set by default on the collection of logging event's properties:
+
+- `trace_id`
+- `span_id`
+- `trace_flags`
+
+This allows for trace context to be logged into currently configured log destination,
+ e.g. a file. In order to use them, pattern needs to be updated.
\ No newline at end of file
diff --git a/docs/log4net-bridge.md b/docs/log4net-bridge.md
index 44ca5e2da8..092ad27136 100644
--- a/docs/log4net-bridge.md
+++ b/docs/log4net-bridge.md
@@ -48,5 +48,3 @@ In order for the bridge to be added, at least 1 other appender has to be configu
Bridge should not be used when appenders are configured for both root and component loggers.
Enabling a bridge in such scenario would result in bridge being appended to both appender collections,
and logs duplication.
-
-
diff --git a/docs/nlog-bridge.md b/docs/nlog-bridge.md
new file mode 100644
index 0000000000..302768f3d5
--- /dev/null
+++ b/docs/nlog-bridge.md
@@ -0,0 +1,46 @@
+# `NLog` [logs bridge](https://opentelemetry.io/docs/specs/otel/glossary/#log-appender--bridge)
+
+> [!IMPORTANT]
+> NLog bridge is an experimental feature.
+
+The `NLog` logs bridge is disabled by default. In order to enable it,
+set `OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE` to `true`.
+
+Bridge is supported for `NLOG` in versions >= 5.0.0 && < 7.0.0
+
+If `NLOG` is used as a [logging provider](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging-providers),
+`NLOG` bridge should not be enabled, in order to reduce possibility of
+duplicated logs export.
+
+## `NLog` logging events conversion
+
+`NLog`'s `ILoggingEvent`s are converted to OpenTelemetry log records in
+a following way:
+
+- `TimeStamp` is set as a `Timestamp`
+- `Level.Name` is set as a `SeverityText`
+- `FormattedMessage` is set as a `Body` if it is available
+- Otherwise, `Message` is set as a `Body`
+- `LoggerName` is set as an `InstrumentationScope.Name`
+- `GetProperties()`, apart from builtin properties prefixed with `nlog:`, `NLog.`,
+ are added as attributes
+- `Exception` is used to populate the following properties: `exception.type`,
+ `exception.message`, `exception.stacktrace`
+- `Level.Value` is mapped to `SeverityNumber` as outlined in the next section
+
+### `NLog` level severity mapping
+
+`NLog` levels are mapped to OpenTelemetry severity types according to
+ following rules based on their numerical values.
+
+Levels with numerical values of:
+
+- Equal to `LogLevel.Fatal` is mapped to `LogRecordSeverity.Fatal`
+- Equal to `LogLevel.Error` is mapped to `LogRecordSeverity.Error`
+- Equal to `LogLevel.Warn` is mapped to `LogRecordSeverity.Warn`
+- Equal to `LogLevel.Info` is mapped to `LogRecordSeverity.Info`
+- Equal to `LogLevel.Debug` is mapped to `LogRecordSeverity.Debug`
+- Equal to `LogLevel.Trace` is mapped to `LogRecordSeverity.Trace`
+- Equal to `LogLevel.Off` is mapped to `LogRecordSeverity.Trace`
+- Any other is mapped to `LogRecordSeverity.Info`.
+
diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
index b2713fe956..97765a7426 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
@@ -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
diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
index b2713fe956..97765a7426 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
@@ -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
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
index 6ace88c28c..5195121219 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
@@ -245,6 +245,12 @@ public static class Logs
///
public const string EnableLog4NetBridge = "OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE";
+ ///
+ /// Configuration key for whether or not experimental NLog bridge
+ /// should be enabled.
+ ///
+ public const string EnableNLogBridge = "OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE";
+
///
/// Configuration key for disabling all log instrumentations.
///
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
index b6ea9f456e..20dbb7e356 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
@@ -17,4 +17,9 @@ internal enum LogInstrumentation
/// Log4Net instrumentation.
///
Log4Net = 1,
+
+ ///
+ /// NLog instrumentation.
+ ///
+ NLog = 2,
}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
index 0b15291aa5..9eedcdd1ff 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
@@ -33,6 +33,11 @@ internal class LogSettings : Settings
///
public bool EnableLog4NetBridge { get; private set; }
+ ///
+ /// Gets a value indicating whether the experimental NLog bridge is enabled.
+ ///
+ public bool EnableNLogBridge { get; private set; }
+
///
/// Gets the list of enabled instrumentations.
///
@@ -54,6 +59,7 @@ protected override void OnLoadEnvVar(Configuration configuration)
IncludeFormattedMessage = configuration.GetBool(ConfigurationKeys.Logs.IncludeFormattedMessage) ?? false;
EnableLog4NetBridge = configuration.GetBool(ConfigurationKeys.Logs.EnableLog4NetBridge) ?? false;
+ EnableNLogBridge = configuration.GetBool(ConfigurationKeys.Logs.EnableNLogBridge) ?? false;
var instrumentationEnabledByDefault =
configuration.GetBool(ConfigurationKeys.Logs.LogsInstrumentationEnabled) ??
diff --git a/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
index 0bbe29caa7..2bfad1b5b3 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
@@ -17,7 +17,7 @@ internal static partial class InstrumentationDefinitions
{
private static NativeCallTargetDefinition[] GetDefinitionsArray()
{
- var nativeCallTargetDefinitions = new List(36);
+ var nativeCallTargetDefinitions = new List(37);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -101,6 +101,12 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray()
nativeCallTargetDefinitions.Add(new("log4net", "log4net.Appender.AppenderCollection", "ToArray", ["log4net.Appender.IAppender[]"], 2, 0, 13, 3, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge.Integrations.AppenderCollectionIntegration"));
nativeCallTargetDefinitions.Add(new("log4net", "log4net.Util.AppenderAttachedImpl", "AppendLoopOnAppenders", ["System.Int32", "log4net.Core.LoggingEvent"], 2, 0, 13, 3, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations.AppenderAttachedImplIntegration"));
}
+
+ // NLog
+ if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.NLog))
+ {
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "Log", ["System.Void", "NLog.LogEventInfo"], 5, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.Integrations.LoggerIntegration"));
+ }
}
// Metrics
diff --git a/src/OpenTelemetry.AutoInstrumentation/Generated/net8.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/src/OpenTelemetry.AutoInstrumentation/Generated/net8.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
index c159ae2602..7a3db17089 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Generated/net8.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Generated/net8.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs
@@ -17,7 +17,7 @@ internal static partial class InstrumentationDefinitions
{
private static NativeCallTargetDefinition[] GetDefinitionsArray()
{
- var nativeCallTargetDefinitions = new List(39);
+ var nativeCallTargetDefinitions = new List(40);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -104,6 +104,12 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray()
{
nativeCallTargetDefinitions.Add(new("Microsoft.Extensions.Logging", "Microsoft.Extensions.Logging.LoggingBuilder", ".ctor", ["System.Void", "Microsoft.Extensions.DependencyInjection.IServiceCollection"], 9, 0, 0, 9, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Logger.LoggingBuilderIntegration"));
}
+
+ // NLog
+ if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.NLog))
+ {
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "Log", ["System.Void", "NLog.LogEventInfo"], 5, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.Integrations.LoggerIntegration"));
+ }
}
// Metrics
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/AutoInjection/NLogAutoInjector.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/AutoInjection/NLogAutoInjector.cs
new file mode 100644
index 0000000000..080a36539d
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/AutoInjection/NLogAutoInjector.cs
@@ -0,0 +1,80 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Reflection;
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+using OpenTelemetry.AutoInstrumentation.Logging;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.AutoInjection;
+
+internal static class NLogAutoInjector
+{
+ private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
+ private static int _attempted;
+
+ public static void EnsureConfigured()
+ {
+ if (Interlocked.Exchange(ref _attempted, 1) != 0)
+ {
+ return;
+ }
+
+ try
+ {
+ var nlogLogManager = Type.GetType("NLog.LogManager, NLog");
+ if (nlogLogManager is null)
+ {
+ return;
+ }
+
+ var configurationProperty = nlogLogManager.GetProperty("Configuration", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+ if (configurationProperty is null)
+ {
+ return;
+ }
+
+ var configuration = configurationProperty.GetValue(null);
+ if (configuration is null)
+ {
+ var configurationType = Type.GetType("NLog.Config.LoggingConfiguration, NLog");
+ configuration = Activator.CreateInstance(configurationType!);
+ configurationProperty.SetValue(null, configuration);
+ }
+
+ // Create the OpenTelemetry target instance and wrap it in a duck proxy
+ var otelTarget = new OpenTelemetryTarget();
+ var targetType = Type.GetType("NLog.Targets.TargetWithContext, NLog", false);
+ if (targetType is null)
+ {
+ Logger.Warning("NLog auto-injection skipped: TargetWithContext type not found.");
+ return;
+ }
+
+ var targetProxy = otelTarget.DuckImplement(targetType);
+
+ // Add target to configuration
+ var addTargetMethod = configuration!.GetType().GetMethod("AddTarget", BindingFlags.Instance | BindingFlags.Public);
+ addTargetMethod?.Invoke(configuration, new object?[] { "otlp", targetProxy });
+
+ // Create rule: * -> otlp (minlevel: Trace)
+ var loggingRuleType = Type.GetType("NLog.Config.LoggingRule, NLog");
+ var logLevelType = Type.GetType("NLog.LogLevel, NLog");
+ var traceLevel = logLevelType?.GetProperty("Trace", BindingFlags.Static | BindingFlags.Public)?.GetValue(null);
+ var rule = Activator.CreateInstance(loggingRuleType!, new object?[] { "*", traceLevel, targetProxy });
+
+ var loggingRulesProp = configuration.GetType().GetProperty("LoggingRules", BindingFlags.Instance | BindingFlags.Public);
+ var rulesList = loggingRulesProp?.GetValue(configuration) as System.Collections.IList;
+ rulesList?.Add(rule);
+
+ // Apply configuration
+ var reconfigMethod = nlogLogManager.GetMethod("ReconfigExistingLoggers", BindingFlags.Static | BindingFlags.Public);
+ reconfigMethod?.Invoke(null, null);
+
+ Logger.Information("NLog OpenTelemetryTarget auto-injected.");
+ }
+ catch (Exception ex)
+ {
+ Logger.Warning(ex, "NLog OpenTelemetryTarget auto-injection failed.");
+ }
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/Integrations/LoggerIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/Integrations/LoggerIntegration.cs
new file mode 100644
index 0000000000..1555430705
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/Integrations/LoggerIntegration.cs
@@ -0,0 +1,109 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using System.Reflection;
+using OpenTelemetry.AutoInstrumentation.CallTarget;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.AutoInjection;
+using OpenTelemetry.AutoInstrumentation.Logging;
+#if NET
+using OpenTelemetry.AutoInstrumentation.Logger;
+#endif
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge.Integrations;
+
+///
+/// NLog Logger integration that hooks into the actual logging process.
+/// This integration intercepts NLog's Logger.Log method calls to automatically
+/// capture log events and forward them to OpenTelemetry when the NLog bridge is enabled.
+///
+/// The integration targets NLog.Logger.Log method which is the core method called
+/// for all logging operations, allowing us to capture events without modifying configuration.
+///
+[InstrumentMethod(
+assemblyName: "NLog",
+typeName: "NLog.Logger",
+methodName: "Log",
+returnTypeName: ClrNames.Void,
+parameterTypeNames: new[] { "NLog.LogEventInfo" },
+minimumVersion: "5.0.0",
+maximumVersion: "6.*.*",
+integrationName: "NLog",
+type: InstrumentationType.Log)]
+public static class LoggerIntegration
+{
+#if NET
+ private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
+ private static int _warningLogged;
+#endif
+
+ ///
+ /// Intercepts NLog's Logger.Log method calls to capture log events.
+ /// This method is called before the original Log method executes,
+ /// allowing us to capture and forward log events to OpenTelemetry.
+ ///
+ /// The type of the logger instance.
+ /// The NLog Logger instance.
+ /// The NLog LogEventInfo being logged.
+ /// A CallTargetState (unused in this case).
+ internal static CallTargetState OnMethodBegin(TTarget instance, ILoggingEvent logEvent)
+ {
+#if NET
+ // Check if ILogger bridge has been initialized and warn if so
+ // This prevents conflicts between different logging bridges
+ if (LoggerInitializer.IsInitializedAtLeastOnce)
+ {
+ if (Interlocked.Exchange(ref _warningLogged, 1) != default)
+ {
+ return CallTargetState.GetDefault();
+ }
+
+ Logger.Warning("Disabling NLog bridge due to ILogger bridge initialization.");
+ return CallTargetState.GetDefault();
+ }
+#endif
+
+ // Only process the log event if the NLog bridge is enabled
+ if (Instrumentation.LogSettings.Value.EnableNLogBridge)
+ {
+ // Ensure the OpenTelemetry NLog target is configured (zero-config path)
+ NLogAutoInjector.EnsureConfigured();
+
+ // Inject trace context into NLog GlobalDiagnosticsContext for current destination outputs
+ TrySetTraceContext(Activity.Current);
+ }
+
+ // Return default state - we don't need to track anything between begin/end
+ return CallTargetState.GetDefault();
+ }
+
+ private static void TrySetTraceContext(Activity? activity)
+ {
+ try
+ {
+ var gdcType = Type.GetType("NLog.GlobalDiagnosticsContext, NLog");
+ if (gdcType is null)
+ {
+ return;
+ }
+
+ var setMethod = gdcType.GetMethod("Set", BindingFlags.Public | BindingFlags.Static, null, [typeof(string), typeof(string)], null);
+ if (setMethod is null)
+ {
+ return;
+ }
+
+ string spanId = activity?.SpanId.ToString() ?? "(null)";
+ string traceId = activity?.TraceId.ToString() ?? "(null)";
+ string traceFlags = activity is null ? "(null)" : ((byte)activity.ActivityTraceFlags).ToString("x2");
+
+ setMethod.Invoke(null, new object[] { "span_id", spanId });
+ setMethod.Invoke(null, new object[] { "trace_id", traceId });
+ setMethod.Invoke(null, new object[] { "trace_flags", traceFlags });
+ }
+ catch
+ {
+ // best-effort only
+ }
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryLogHelpers.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryLogHelpers.cs
new file mode 100644
index 0000000000..216e7dfd33
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryLogHelpers.cs
@@ -0,0 +1,320 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+using OpenTelemetry.AutoInstrumentation.Logging;
+using OpenTelemetry.Logs;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge;
+
+///
+/// Delegate for emitting log records to OpenTelemetry.
+/// This delegate signature matches the requirements for creating OpenTelemetry log records
+/// with all necessary metadata and context information.
+///
+/// The OpenTelemetry logger instance.
+/// The log message body or template.
+/// The timestamp when the log event occurred.
+/// The textual representation of the log level.
+/// The numeric severity level mapped to OpenTelemetry standards.
+/// The exception associated with the log event, if any.
+/// Additional properties to include in the log record.
+/// The current activity for trace context.
+/// Message template arguments for structured logging.
+/// The fully formatted message for inclusion as an attribute.
+internal delegate void EmitLog(object loggerInstance, string? body, DateTime timestamp, string? severityText, int severityLevel, Exception? exception, IEnumerable>? properties, Activity? current, object?[]? args, string? renderedMessage);
+
+///
+/// Helper class for creating OpenTelemetry log records from NLog events.
+/// This class provides the core functionality for bridging NLog logging to OpenTelemetry
+/// by dynamically creating log emission functions that work with OpenTelemetry's internal APIs.
+///
+/// TODO: Remove whole class when Logs Api is made public in non-rc builds.
+///
+internal static class OpenTelemetryLogHelpers
+{
+ private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
+
+ static OpenTelemetryLogHelpers()
+ {
+ try
+ {
+ // Use reflection to access OpenTelemetry's internal logging types
+ // This is necessary because the logging API is not yet public
+ var loggerProviderType = typeof(LoggerProvider);
+ var apiAssembly = loggerProviderType.Assembly;
+ var loggerType = typeof(Sdk).Assembly.GetType("OpenTelemetry.Logs.LoggerSdk");
+ var logRecordDataType = apiAssembly.GetType("OpenTelemetry.Logs.LogRecordData")!;
+ var logRecordAttributesListType = apiAssembly.GetType("OpenTelemetry.Logs.LogRecordAttributeList")!;
+
+ // Build the log emission delegate using expression trees
+ LogEmitter = BuildEmitLog(logRecordDataType, logRecordAttributesListType, loggerType!);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to initialize LogEmitter delegate for NLog bridge.");
+ }
+ }
+
+ ///
+ /// Gets the log emitter delegate that can create OpenTelemetry log records.
+ /// This delegate is constructed dynamically using reflection and expression trees
+ /// to work with OpenTelemetry's internal logging APIs.
+ ///
+ public static EmitLog? LogEmitter { get; }
+
+ ///
+ /// Builds an expression tree for creating OpenTelemetry log records.
+ /// This method constructs the necessary expressions to properly initialize
+ /// LogRecordData objects with all required properties and attributes.
+ ///
+ /// The type of LogRecordData from OpenTelemetry.
+ /// The type representing log severity levels.
+ /// Parameter expression for the log message body.
+ /// Parameter expression for the log timestamp.
+ /// Parameter expression for the severity text.
+ /// Parameter expression for the numeric severity level.
+ /// Parameter expression for the current activity.
+ /// A block expression that creates and initializes a LogRecordData object.
+ private static BlockExpression BuildLogRecord(
+ Type logRecordDataType,
+ Type severityType,
+ ParameterExpression body,
+ ParameterExpression timestamp,
+ ParameterExpression severityText,
+ ParameterExpression severityLevel,
+ ParameterExpression activity)
+ {
+ // Creates expression tree that generates code equivalent to:
+ // var instance = new LogRecordData(activity);
+ // if (body != null) instance.Body = body;
+ // instance.Timestamp = timestamp;
+ // if (severityText != null) instance.SeverityText = severityText;
+ // instance.Severity = (LogRecordSeverity?)severityLevel;
+ // return instance;
+
+ var timestampSetterMethodInfo = logRecordDataType.GetProperty("Timestamp")!.GetSetMethod()!;
+ var bodySetterMethodInfo = logRecordDataType.GetProperty("Body")!.GetSetMethod()!;
+ var severityTextSetterMethodInfo = logRecordDataType.GetProperty("SeverityText")!.GetSetMethod()!;
+ var severityLevelSetterMethodInfo = logRecordDataType.GetProperty("Severity")!.GetSetMethod()!;
+
+ var instanceVar = Expression.Variable(bodySetterMethodInfo.DeclaringType!, "instance");
+
+ var constructorInfo = logRecordDataType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.HasThis, new[] { typeof(Activity) }, null)!;
+ var assignInstanceVar = Expression.Assign(instanceVar, Expression.New(constructorInfo, activity));
+ var setBody = Expression.IfThen(Expression.NotEqual(body, Expression.Constant(null)), Expression.Call(instanceVar, bodySetterMethodInfo, body));
+ var setTimestamp = Expression.Call(instanceVar, timestampSetterMethodInfo, timestamp);
+ var setSeverityText = Expression.IfThen(Expression.NotEqual(severityText, Expression.Constant(null)), Expression.Call(instanceVar, severityTextSetterMethodInfo, severityText));
+ var setSeverityLevel = Expression.Call(instanceVar, severityLevelSetterMethodInfo, Expression.Convert(severityLevel, typeof(Nullable<>).MakeGenericType(severityType)));
+
+ return Expression.Block(
+ new[] { instanceVar },
+ assignInstanceVar,
+ setBody,
+ setTimestamp,
+ setSeverityText,
+ setSeverityLevel,
+ instanceVar);
+ }
+
+ ///
+ /// Builds an expression tree for creating and populating log record attributes.
+ /// This handles exceptions, custom properties, message template arguments, and rendered messages.
+ ///
+ /// The type of LogRecordAttributeList from OpenTelemetry.
+ /// Parameter expression for the exception.
+ /// Parameter expression for custom properties.
+ /// Parameter expression for message template arguments.
+ /// Parameter expression for the rendered message.
+ /// A block expression that creates and populates a LogRecordAttributeList.
+ private static BlockExpression BuildLogRecordAttributes(
+ Type logRecordAttributesListType,
+ ParameterExpression exception,
+ ParameterExpression properties,
+ ParameterExpression argsParam,
+ ParameterExpression renderedMessageParam)
+ {
+ // Creates expression tree that generates code to populate log attributes
+ // including exception details, custom properties, and structured logging parameters
+
+ var instanceVar = Expression.Variable(logRecordAttributesListType, "instance");
+ var constructorInfo = logRecordAttributesListType.GetConstructor(Type.EmptyTypes)!;
+ var assignInstanceVar = Expression.Assign(instanceVar, Expression.New(constructorInfo));
+
+ var addAttributeMethodInfo = logRecordAttributesListType.GetMethod("Add", new[] { typeof(string), typeof(object) })!;
+
+ var expressions = new List { assignInstanceVar };
+
+ // Add exception as an attribute if present
+ var addExceptionExpression = Expression.IfThen(
+ Expression.NotEqual(exception, Expression.Constant(null)),
+ Expression.Call(instanceVar, addAttributeMethodInfo, Expression.Constant("exception"), exception));
+ expressions.Add(addExceptionExpression);
+
+ // Add custom properties if present
+ var addPropertiesExpression = BuildAddPropertiesExpression(instanceVar, properties, addAttributeMethodInfo);
+ expressions.Add(addPropertiesExpression);
+
+ // Add structured logging arguments if present
+ var addArgsExpression = BuildAddArgsExpression(instanceVar, argsParam, addAttributeMethodInfo);
+ expressions.Add(addArgsExpression);
+
+ // Add rendered message if present
+ var addRenderedMessageExpression = Expression.IfThen(
+ Expression.NotEqual(renderedMessageParam, Expression.Constant(null)),
+ Expression.Call(instanceVar, addAttributeMethodInfo, Expression.Constant("RenderedMessage"), renderedMessageParam));
+ expressions.Add(addRenderedMessageExpression);
+
+ expressions.Add(instanceVar);
+
+ return Expression.Block(
+ new[] { instanceVar },
+ expressions);
+ }
+
+ ///
+ /// Builds an expression for adding custom properties to the log record attributes.
+ ///
+ /// The LogRecordAttributeList instance variable.
+ /// The properties parameter expression.
+ /// The Add method for adding attributes.
+ /// An expression that adds all custom properties to the attributes list.
+ private static Expression BuildAddPropertiesExpression(ParameterExpression instanceVar, ParameterExpression properties, MethodInfo addAttributeMethodInfo)
+ {
+ // Create a foreach loop to iterate over properties and add them as attributes
+ var enumerableType = typeof(IEnumerable>);
+ var kvpType = typeof(KeyValuePair);
+
+ var getEnumeratorMethod = enumerableType.GetMethod("GetEnumerator")!;
+ var enumeratorType = getEnumeratorMethod.ReturnType;
+ var moveNextMethod = typeof(System.Collections.IEnumerator).GetMethod("MoveNext")!;
+ var currentProperty = enumeratorType.GetProperty("Current")!;
+ var keyProperty = kvpType.GetProperty("Key")!;
+ var valueProperty = kvpType.GetProperty("Value")!;
+
+ var enumeratorVar = Expression.Variable(enumeratorType, "enumerator");
+ var currentVar = Expression.Variable(kvpType, "current");
+ var breakLabel = Expression.Label();
+
+ var loop = Expression.Loop(
+ Expression.Block(
+ Expression.IfThen(
+ Expression.IsFalse(Expression.Call(enumeratorVar, moveNextMethod)),
+ Expression.Break(breakLabel)),
+ Expression.Assign(currentVar, Expression.Property(enumeratorVar, currentProperty)),
+ Expression.Call(
+ instanceVar,
+ addAttributeMethodInfo,
+ Expression.Property(currentVar, keyProperty),
+ Expression.Property(currentVar, valueProperty))),
+ breakLabel);
+
+ return Expression.IfThen(
+ Expression.NotEqual(properties, Expression.Constant(null)),
+ Expression.Block(
+ new[] { enumeratorVar, currentVar },
+ Expression.Assign(enumeratorVar, Expression.Call(properties, getEnumeratorMethod)),
+ loop));
+ }
+
+ ///
+ /// Builds an expression for adding structured logging arguments to the log record attributes.
+ ///
+ /// The LogRecordAttributeList instance variable.
+ /// The arguments parameter expression.
+ /// The Add method for adding attributes.
+ /// An expression that adds structured logging arguments as attributes.
+ private static Expression BuildAddArgsExpression(ParameterExpression instanceVar, ParameterExpression argsParam, MethodInfo addAttributeMethodInfo)
+ {
+ // Create a for loop to iterate over args array and add them as indexed attributes
+ var lengthProperty = typeof(Array).GetProperty("Length")!;
+ var indexVar = Expression.Variable(typeof(int), "i");
+ var breakLabel = Expression.Label();
+
+ var loop = Expression.Loop(
+ Expression.Block(
+ Expression.IfThen(
+ Expression.GreaterThanOrEqual(indexVar, Expression.Property(argsParam, lengthProperty)),
+ Expression.Break(breakLabel)),
+ Expression.Call(
+ instanceVar,
+ addAttributeMethodInfo,
+ Expression.Call(
+ typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) })!,
+ Expression.Constant("arg_"),
+ Expression.Call(indexVar, typeof(int).GetMethod("ToString", Type.EmptyTypes)!)),
+ Expression.ArrayIndex(argsParam, indexVar)),
+ Expression.Assign(indexVar, Expression.Add(indexVar, Expression.Constant(1)))),
+ breakLabel);
+
+ return Expression.IfThen(
+ Expression.NotEqual(argsParam, Expression.Constant(null)),
+ Expression.Block(
+ new[] { indexVar },
+ Expression.Assign(indexVar, Expression.Constant(0)),
+ loop));
+ }
+
+ ///
+ /// Builds the complete EmitLog delegate using expression trees.
+ /// This method constructs a function that can create OpenTelemetry log records
+ /// from NLog event data.
+ ///
+ /// The LogRecordData type from OpenTelemetry.
+ /// The LogRecordAttributeList type from OpenTelemetry.
+ /// The Logger type from OpenTelemetry.
+ /// An EmitLog delegate that can create OpenTelemetry log records.
+ private static EmitLog BuildEmitLog(Type logRecordDataType, Type logRecordAttributesListType, Type loggerType)
+ {
+ // Get the LogRecordSeverity enum type
+ var severityType = logRecordDataType.Assembly.GetType("OpenTelemetry.Logs.LogRecordSeverity")!;
+
+ // Define parameters for the delegate
+ var loggerInstance = Expression.Parameter(typeof(object), "loggerInstance");
+ var body = Expression.Parameter(typeof(string), "body");
+ var timestamp = Expression.Parameter(typeof(DateTime), "timestamp");
+ var severityText = Expression.Parameter(typeof(string), "severityText");
+ var severityLevel = Expression.Parameter(typeof(int), "severityLevel");
+ var exception = Expression.Parameter(typeof(Exception), "exception");
+ var properties = Expression.Parameter(typeof(IEnumerable>), "properties");
+ var activity = Expression.Parameter(typeof(Activity), "activity");
+ var args = Expression.Parameter(typeof(object[]), "args");
+ var renderedMessage = Expression.Parameter(typeof(string), "renderedMessage");
+
+ // Build the log record creation expression
+ var logRecordExpression = BuildLogRecord(logRecordDataType, severityType, body, timestamp, severityText, severityLevel, activity);
+
+ // Build the attributes creation expression
+ var attributesExpression = BuildLogRecordAttributes(logRecordAttributesListType, exception, properties, args, renderedMessage);
+
+ // Get the EmitLog method from the logger
+ var emitLogRecordMethod = loggerType.GetMethod("EmitLog", BindingFlags.Instance | BindingFlags.Public, null, new[] { logRecordDataType.MakeByRefType(), logRecordAttributesListType.MakeByRefType() }, null)!;
+
+ // Build the complete expression that creates the log record, creates attributes, and emits the log
+ var completeExpression = Expression.Block(
+ Expression.Call(
+ Expression.Convert(loggerInstance, loggerType),
+ emitLogRecordMethod,
+ logRecordExpression,
+ attributesExpression));
+
+ // Compile the expression into a delegate
+ var lambda = Expression.Lambda(
+ completeExpression,
+ loggerInstance,
+ body,
+ timestamp,
+ severityText,
+ severityLevel,
+ exception,
+ properties,
+ activity,
+ args,
+ renderedMessage);
+
+ return lambda.Compile();
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryNLogConverter.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryNLogConverter.cs
new file mode 100644
index 0000000000..d62aff7a02
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryNLogConverter.cs
@@ -0,0 +1,199 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Reflection;
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection;
+#if NET
+using OpenTelemetry.AutoInstrumentation.Logger;
+#endif
+using OpenTelemetry.AutoInstrumentation.Logging;
+using OpenTelemetry.Logs;
+using Exception = System.Exception;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge;
+
+///
+/// Converts NLog LogEventInfo into OpenTelemetry LogRecords.
+///
+internal class OpenTelemetryNLogConverter
+{
+ private const int TraceOrdinal = 0;
+ private const int DebugOrdinal = 1;
+ private const int InfoOrdinal = 2;
+ private const int WarnOrdinal = 3;
+ private const int ErrorOrdinal = 4;
+ private const int FatalOrdinal = 5;
+ private const int OffOrdinal = 6;
+
+ private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
+ private static readonly Lazy InstanceField = new(InitializeTarget, true);
+
+ private readonly Func? _getLoggerFactory;
+ private readonly ConcurrentDictionary _loggers = new(StringComparer.Ordinal);
+
+#if NET
+ private int _warningLogged;
+#endif
+
+ private OpenTelemetryNLogConverter(LoggerProvider loggerProvider)
+ {
+ _getLoggerFactory = CreateGetLoggerDelegate(loggerProvider);
+ }
+
+ public static OpenTelemetryNLogConverter Instance => InstanceField.Value;
+
+ [DuckReverseMethod]
+ public string Name { get; set; } = nameof(OpenTelemetryNLogConverter);
+
+ [DuckReverseMethod(ParameterTypeNames = new[] { "NLog.LogEventInfo, NLog" })]
+ public void WriteLogEvent(ILoggingEvent loggingEvent)
+ {
+ if (Sdk.SuppressInstrumentation || loggingEvent.Level.Ordinal == OffOrdinal)
+ {
+ return;
+ }
+
+#if NET
+ if (LoggerInitializer.IsInitializedAtLeastOnce)
+ {
+ if (Interlocked.Exchange(ref _warningLogged, 1) != default)
+ {
+ return;
+ }
+
+ Logger.Warning("Disabling NLog bridge due to ILogger bridge initialization.");
+ return;
+ }
+#endif
+
+ var logger = GetLogger(loggingEvent.LoggerName);
+ var logEmitter = OpenTelemetryLogHelpers.LogEmitter;
+ if (logEmitter is null || logger is null)
+ {
+ return;
+ }
+
+ var mappedLogLevel = MapLogLevel(loggingEvent.Level.Ordinal);
+
+ string? messageTemplate = null;
+ string? formattedMessage = null;
+ object?[]? parameters = null;
+ var messageObject = loggingEvent.Message;
+ if (loggingEvent.Parameters is { Length: > 0 })
+ {
+ messageTemplate = messageObject?.ToString();
+ parameters = loggingEvent.Parameters;
+ }
+
+ if (messageTemplate is not null && Instrumentation.LogSettings.Value.IncludeFormattedMessage)
+ {
+ formattedMessage = loggingEvent.FormattedMessage;
+ }
+
+ logEmitter(
+ logger,
+ messageTemplate ?? loggingEvent.FormattedMessage,
+ loggingEvent.TimeStamp,
+ loggingEvent.Level.Name,
+ mappedLogLevel,
+ loggingEvent.Exception,
+ GetProperties(loggingEvent),
+ Activity.Current,
+ parameters,
+ formattedMessage);
+ }
+
+ internal static int MapLogLevel(int levelOrdinal)
+ {
+ return levelOrdinal switch
+ {
+ FatalOrdinal => 21,
+ ErrorOrdinal => 17,
+ WarnOrdinal => 13,
+ InfoOrdinal => 9,
+ DebugOrdinal => 5,
+ TraceOrdinal => 1,
+ _ => 1
+ };
+ }
+
+ private static IEnumerable>? GetProperties(ILoggingEvent loggingEvent)
+ {
+ try
+ {
+ var properties = loggingEvent.GetProperties();
+ return properties == null ? null : GetFilteredProperties(properties);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ private static IEnumerable> GetFilteredProperties(IDictionary properties)
+ {
+ foreach (var propertyKey in properties.Keys)
+ {
+ if (propertyKey is not string key)
+ {
+ continue;
+ }
+
+ if (key.StartsWith("NLog.") ||
+ key.StartsWith("nlog:") ||
+ key == LogsTraceContextInjectionConstants.SpanIdPropertyName ||
+ key == LogsTraceContextInjectionConstants.TraceIdPropertyName ||
+ key == LogsTraceContextInjectionConstants.TraceFlagsPropertyName)
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(key, properties[key]);
+ }
+ }
+
+ private static Func? CreateGetLoggerDelegate(LoggerProvider loggerProvider)
+ {
+ try
+ {
+ var methodInfo = typeof(LoggerProvider)
+ .GetMethod("GetLogger", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null)!;
+ return (Func)methodInfo.CreateDelegate(typeof(Func), loggerProvider);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to create logger factory delegate.");
+ return null;
+ }
+ }
+
+ private static OpenTelemetryNLogConverter InitializeTarget()
+ {
+ return new OpenTelemetryNLogConverter(Instrumentation.LoggerProvider!);
+ }
+
+ private object? GetLogger(string? loggerName)
+ {
+ if (_getLoggerFactory is null)
+ {
+ return null;
+ }
+
+ var name = loggerName ?? string.Empty;
+ if (_loggers.TryGetValue(name, out var logger))
+ {
+ return logger;
+ }
+
+ if (_loggers.Count < 100)
+ {
+ return _loggers.GetOrAdd(name, _getLoggerFactory!);
+ }
+
+ return _getLoggerFactory(name);
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/ILoggingEvent.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/ILoggingEvent.cs
new file mode 100644
index 0000000000..be86db6383
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/ILoggingEvent.cs
@@ -0,0 +1,112 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog;
+
+///
+/// Duck typing interface that wraps NLog's LogEventInfo class.
+/// This interface maps to NLog's LogEventInfo structure to extract logging information
+/// for conversion to OpenTelemetry log records.
+///
+/// Based on: https://github.com/NLog/NLog/blob/master/src/NLog/LogEventInfo.cs
+///
+internal interface ILoggingEvent
+{
+ ///
+ /// Gets the logging level of the log event.
+ /// Maps to NLog's LogLevel property.
+ ///
+ public LoggingLevel Level { get; }
+
+ ///
+ /// Gets the name of the logger that created the log event.
+ /// Maps to NLog's LoggerName property.
+ ///
+ public string? LoggerName { get; }
+
+ ///
+ /// Gets the formatted log message.
+ /// Maps to NLog's FormattedMessage property.
+ ///
+ public string? FormattedMessage { get; }
+
+ ///
+ /// Gets the exception associated with the log event, if any.
+ /// Maps to NLog's Exception property.
+ ///
+ public Exception? Exception { get; }
+
+ ///
+ /// Gets the timestamp when the log event was created.
+ /// Maps to NLog's TimeStamp property.
+ ///
+ public DateTime TimeStamp { get; }
+
+ ///
+ /// Gets the message object before formatting.
+ /// Maps to NLog's Message property.
+ ///
+ public object? Message { get; }
+
+ ///
+ /// Gets the parameters for the log message.
+ /// Maps to NLog's Parameters property.
+ ///
+ public object?[]? Parameters { get; }
+
+ ///
+ /// Gets the properties collection for custom properties.
+ /// Used for injecting trace context and storing additional metadata.
+ /// Maps to NLog's Properties property.
+ ///
+ public IDictionary? Properties { get; }
+
+ ///
+ /// Gets the context properties dictionary.
+ /// Maps to NLog's Properties property with read access.
+ ///
+ public IDictionary? GetProperties();
+}
+
+///
+/// Duck typing interface for NLog's message template structure.
+/// This represents structured logging information when using message templates.
+///
+internal interface IMessageTemplateParameters : IDuckType
+{
+ ///
+ /// Gets the message template format string.
+ ///
+ public string? MessageTemplate { get; }
+
+ ///
+ /// Gets the parameters for the message template.
+ ///
+ public object?[]? Parameters { get; }
+}
+
+///
+/// Duck typing structure that wraps NLog's LogLevel.
+/// This provides access to NLog's log level information for mapping to OpenTelemetry severity levels.
+///
+/// Based on: https://github.com/NLog/NLog/blob/master/src/NLog/LogLevel.cs
+///
+[DuckCopy]
+internal struct LoggingLevel
+{
+ ///
+ /// Gets the numeric value of the log level.
+ /// NLog uses ordinal values: Trace=0, Debug=1, Info=2, Warn=3, Error=4, Fatal=5
+ ///
+ public int Ordinal;
+
+ ///
+ /// Gets the string name of the log level.
+ ///
+ public string Name;
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/OpenTelemetryTarget.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/OpenTelemetryTarget.cs
new file mode 100644
index 0000000000..c532b42c8d
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/OpenTelemetryTarget.cs
@@ -0,0 +1,183 @@
+// 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.Reflection;
+using OpenTelemetry.AutoInstrumentation.Configurations;
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge;
+using OpenTelemetry.Logs;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog;
+
+///
+/// OpenTelemetry Target for NLog using duck typing to avoid direct NLog assembly references.
+/// This target is designed to be used through auto-injection and duck-typed proxies.
+///
+internal sealed class OpenTelemetryTarget
+{
+ private static readonly ConcurrentDictionary LoggerCache = new(StringComparer.Ordinal);
+
+ private static LoggerProvider? _loggerProvider;
+ private static Func? _getLoggerFactory;
+
+ [DuckReverseMethod]
+ public string Name { get; set; } = nameof(OpenTelemetryTarget);
+
+ [DuckReverseMethod]
+ public void InitializeTarget()
+ {
+ if (_loggerProvider != null)
+ {
+ return;
+ }
+
+ var createLoggerProviderBuilderMethod = typeof(Sdk).GetMethod("CreateLoggerProviderBuilder", BindingFlags.Static | BindingFlags.NonPublic)!;
+ var loggerProviderBuilder = (LoggerProviderBuilder)createLoggerProviderBuilderMethod.Invoke(null, null)!;
+
+ loggerProviderBuilder = loggerProviderBuilder
+ .SetResourceBuilder(ResourceConfigurator.CreateResourceBuilder(Instrumentation.ResourceSettings.Value));
+
+ loggerProviderBuilder = loggerProviderBuilder.AddOtlpExporter();
+
+ _loggerProvider = loggerProviderBuilder.Build();
+ _getLoggerFactory = CreateGetLoggerDelegate(_loggerProvider);
+ }
+
+ [DuckReverseMethod(ParameterTypeNames = new[] { "NLog.LogEventInfo, NLog" })]
+ public void Write(ILoggingEvent? logEvent)
+ {
+ if (logEvent is null || _loggerProvider is null)
+ {
+ return;
+ }
+
+ if (Sdk.SuppressInstrumentation)
+ {
+ return;
+ }
+
+ var logger = GetOrCreateLogger(logEvent.LoggerName);
+ if (logger is null)
+ {
+ return;
+ }
+
+ var properties = GetLogEventProperties(logEvent);
+
+ // Use formatted message if available, otherwise use raw message
+ var body = logEvent.FormattedMessage ?? logEvent.Message?.ToString();
+
+ var severityText = logEvent.Level.Name;
+ var severityNumber = MapLogLevelToSeverity(logEvent.Level.Ordinal);
+
+ // Use Activity.Current for trace context
+ var current = Activity.Current;
+
+ // Include event parameters if available
+ var args = logEvent.Parameters is object[] p ? p : null;
+
+ OpenTelemetryLogHelpers.LogEmitter?.Invoke(
+ logger,
+ body,
+ logEvent.TimeStamp,
+ severityText,
+ severityNumber,
+ logEvent.Exception,
+ properties,
+ current,
+ args,
+ logEvent.FormattedMessage);
+ }
+
+ private static int MapLogLevelToSeverity(int levelOrdinal)
+ {
+ // Map NLog ordinals 0..5 to OTEL severity 1..24 approximate buckets
+ return levelOrdinal switch
+ {
+ 0 => 1, // Trace
+ 1 => 5, // Debug
+ 2 => 9, // Info
+ 3 => 13, // Warn
+ 4 => 17, // Error
+ 5 => 21, // Fatal
+ 6 => 1, // Off
+ _ => 9
+ };
+ }
+
+ private static Func? CreateGetLoggerDelegate(LoggerProvider loggerProvider)
+ {
+ try
+ {
+ var methodInfo = typeof(LoggerProvider)
+ .GetMethod("GetLogger", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null)!;
+ return (Func)methodInfo.CreateDelegate(typeof(Func), loggerProvider);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static IEnumerable>? GetLogEventProperties(ILoggingEvent logEvent)
+ {
+ try
+ {
+ var properties = logEvent.GetProperties();
+ return properties == null ? null : GetFilteredProperties(properties);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ private static IEnumerable> GetFilteredProperties(System.Collections.IDictionary properties)
+ {
+ foreach (var propertyKey in properties.Keys)
+ {
+ if (propertyKey is not string key)
+ {
+ continue;
+ }
+
+ if (key.StartsWith("NLog.") ||
+ key.StartsWith("nlog:") ||
+ key == TraceContextInjection.LogsTraceContextInjectionConstants.SpanIdPropertyName ||
+ key == TraceContextInjection.LogsTraceContextInjectionConstants.TraceIdPropertyName ||
+ key == TraceContextInjection.LogsTraceContextInjectionConstants.TraceFlagsPropertyName)
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(key, properties[key]);
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/README.md b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/README.md
new file mode 100644
index 0000000000..1bf20b4de8
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/README.md
@@ -0,0 +1,198 @@
+# NLog OpenTelemetry Auto-Instrumentation
+
+This directory contains the NLog instrumentation for OpenTelemetry .NET Auto-Instrumentation. This instrumentation provides automatic zero-config injection for bridging NLog logging events to OpenTelemetry using duck typing.
+
+## Overview
+
+The NLog instrumentation offers automatic integration through:
+1. **Zero-Config Auto-Injection**: Automatically injects duck-typed `OpenTelemetryTarget` into existing NLog configurations
+2. **Duck Typing Integration**: Uses `[DuckReverseMethod]` to avoid direct NLog assembly references
+3. **Log Event Bridging**: Converting NLog log events to OpenTelemetry log records
+4. **Structured Logging Support**: Leveraging NLog's layout abilities for enrichment
+5. **Trace Context Integration**: Automatically including trace context in log records
+6. **Custom Properties**: Forwarding custom properties while filtering internal NLog properties
+
+**Note**: XML configuration via `nlog.config` is not supported. The target works exclusively through auto-injection and relies on OpenTelemetry environment variables for configuration.
+
+## Architecture
+
+### Zero-Config Path (Auto-Injection)
+```
+NLog Logger.Log() Call
+ ↓
+LoggerIntegration (CallTarget)
+ ↓
+NLogAutoInjector.EnsureConfigured()
+ ↓
+OpenTelemetryTarget → NLog Configuration
+ ↓
+OpenTelemetryNLogConverter
+ ↓
+OpenTelemetry LogRecord
+ ↓
+OTLP Exporters
+```
+
+### Auto-Injection Path (Duck Typing)
+```
+NLog Logger.Log() Call
+ ↓
+LoggerIntegration (CallTarget)
+ ↓
+NLogAutoInjector.EnsureConfigured()
+ ↓
+OpenTelemetryTarget (Duck-typed proxy) → NLog Configuration
+ ↓
+OpenTelemetry LogRecord
+ ↓
+OTLP Exporters
+```
+
+## Components
+
+### Core Components
+
+#### Auto-Instrumentation Components
+- **`ILoggingEvent.cs`**: Duck typing interface for NLog's LogEventInfo
+- **`OpenTelemetryNLogConverter.cs`**: Internal converter that transforms NLog events to OpenTelemetry log records
+- **`OpenTelemetryLogHelpers.cs`**: Helper for creating OpenTelemetry log records via expression trees
+- **`NLogAutoInjector.cs`**: Handles programmatic injection of OpenTelemetryTarget into NLog configuration
+
+#### Duck-Typed NLog Target
+- **`OpenTelemetryTarget.cs`**: Duck-typed NLog target using `[DuckReverseMethod]` to avoid direct NLog assembly references
+
+### Integration
+
+- **`LoggerIntegration.cs`**: CallTarget integration that intercepts `NLog.Logger.Log` to trigger auto-injection and GDC trace context
+
+### Trace Context
+
+- **`LogsTraceContextInjectionConstants.cs`**: Constants for trace context property names
+
+## Configuration
+
+The NLog instrumentation is configured entirely through OpenTelemetry environment variables. No programmatic configuration is supported to maintain assembly loading safety.
+
+### Environment Variables
+
+The NLog auto-injection is controlled by:
+
+- `OTEL_DOTNET_AUTO_LOGS_ENABLED=true`: Enables logging instrumentation
+- `OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true`: Enables the NLog bridge specifically
+
+Standard OpenTelemetry environment variables configure the OTLP exporter:
+
+```bash
+export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
+export OTEL_EXPORTER_OTLP_HEADERS="x-api-key=abc123"
+export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
+export OTEL_RESOURCE_ATTRIBUTES="service.name=MyApp,service.version=1.0.0"
+export OTEL_BSP_SCHEDULE_DELAY="5000"
+export OTEL_BSP_MAX_QUEUE_SIZE="2048"
+export OTEL_BSP_MAX_EXPORT_BATCH_SIZE="512"
+```
+
+### Behavior
+
+The target automatically:
+- Uses formatted message if available, otherwise raw message
+- Includes event parameters when present
+- Captures trace context from `Activity.Current`
+- Forwards custom properties while filtering internal NLog properties
+
+## Supported Versions
+
+- **NLog**: 5.0.0+ (required for Layout<T> typed layout support and .NET build-trimming)
+- **.NET Framework**: 4.6.2+
+- **.NET**: 8.0, 9.0
+
+## Level Mapping
+
+NLog levels are mapped to OpenTelemetry log record severity levels:
+
+| NLog Level | Ordinal | OpenTelemetry Severity | Value |
+|------------|---------|------------------------|-------|
+| Trace | 0 | Trace | 1 |
+| Debug | 1 | Debug | 5 |
+| Info | 2 | Info | 9 |
+| Warn | 3 | Warn | 13 |
+| Error | 4 | Error | 17 |
+| Fatal | 5 | Fatal | 21 |
+| Off | 6 | Trace | 1 |
+
+## Duck Typing
+
+The instrumentation uses duck typing to interact with NLog without requiring direct references:
+
+- **`ILoggingEvent`**: Maps to `NLog.LogEventInfo`
+- **`LoggingLevel`**: Maps to `NLog.LogLevel`
+- **`IMessageTemplateParameters`**: Maps to structured logging parameters
+
+## Property Filtering
+
+The following properties are filtered out when forwarding to OpenTelemetry:
+- Properties starting with `NLog.`
+- Properties starting with `nlog:`
+- OpenTelemetry trace context properties (`SpanId`, `TraceId`, `TraceFlags`)
+
+## Performance Considerations
+
+- **Logger Caching**: OpenTelemetry loggers are cached (up to 100) to avoid recreation overhead
+- **Lazy Initialization**: Components are initialized only when needed
+- **Minimal Overhead**: The target is injected once during configuration loading
+
+## Error Handling
+
+- **Graceful Degradation**: If OpenTelemetry components fail to initialize, logging continues normally
+- **Property Safety**: Property extraction is wrapped in try-catch to handle potential NLog configuration issues
+- **Instrumentation Conflicts**: Automatically disables when other logging bridges are active
+
+## Testing
+
+Tests are located in `test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs` and cover:
+- Level mapping verification
+- Edge case handling (invalid levels, off level)
+- Custom level support
+- Range-based mapping logic
+
+## Integration Testing
+
+A complete test application is available at `test/test-applications/integrations/TestApplication.NLogBridge/` that demonstrates:
+- Direct NLog usage
+- Microsoft.Extensions.Logging integration via custom provider
+- Structured logging scenarios
+- Exception logging
+- Custom properties
+- Trace context propagation
+- Both auto-injection and manual target configuration paths
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Bridge Not Working**
+ - Verify `OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true`
+ - Check that NLog version is supported
+ - Ensure auto-instrumentation is properly loaded
+
+2. **Missing Properties**
+ - Check NLog configuration for property capture
+ - Verify properties don't start with filtered prefixes
+
+3. **Performance Impact**
+ - Monitor logger cache efficiency
+ - Consider adjusting cache size if many dynamic logger names are used
+
+### Debug Information
+
+Enable debug logging to see:
+- Target injection success/failure
+- Logger creation and caching
+- Property filtering decisions
+
+## Implementation Notes
+
+- Uses reflection to access internal OpenTelemetry logging APIs (until public APIs are available)
+- Builds expression trees dynamically for efficient log record creation
+- Follows the same patterns as Log4Net instrumentation for consistency
+- Designed to be thread-safe and performant in high-throughput scenarios
\ No newline at end of file
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/LogsTraceContextInjectionConstants.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/LogsTraceContextInjectionConstants.cs
new file mode 100644
index 0000000000..9e751efd0c
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/LogsTraceContextInjectionConstants.cs
@@ -0,0 +1,27 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection;
+
+///
+/// Constants used for injecting trace context information into NLog log events.
+/// These constants define the property names used to store OpenTelemetry trace data
+/// within NLog's properties collection.
+///
+internal static class LogsTraceContextInjectionConstants
+{
+ ///
+ /// Property name for storing the OpenTelemetry span ID in NLog properties.
+ ///
+ public const string SpanIdPropertyName = "SpanId";
+
+ ///
+ /// Property name for storing the OpenTelemetry trace ID in NLog properties.
+ ///
+ public const string TraceIdPropertyName = "TraceId";
+
+ ///
+ /// Property name for storing the OpenTelemetry trace flags in NLog properties.
+ ///
+ public const string TraceFlagsPropertyName = "TraceFlags";
+}
diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props
index 3fe673b2a6..e4188dc8dc 100644
--- a/test/Directory.Packages.props
+++ b/test/Directory.Packages.props
@@ -35,6 +35,7 @@
+
diff --git a/test/IntegrationTests/LibraryVersions.g.cs b/test/IntegrationTests/LibraryVersions.g.cs
index aa72fb9bb4..06fad2b87f 100644
--- a/test/IntegrationTests/LibraryVersions.g.cs
+++ b/test/IntegrationTests/LibraryVersions.g.cs
@@ -124,6 +124,24 @@ public static TheoryData log4net
#else
"2.0.13",
"3.2.0",
+#endif
+ ];
+ return theoryData;
+ }
+ }
+ public static TheoryData NLog
+ {
+ get
+ {
+ TheoryData theoryData =
+ [
+#if DEFAULT_TEST_PACKAGE_VERSIONS
+ string.Empty,
+#else
+ "5.0.0",
+ "5.3.4",
+ "6.0.0",
+ "6.0.4",
#endif
];
return theoryData;
@@ -403,6 +421,7 @@ public static TheoryData Kafka
{ "GraphQL", GraphQL },
{ "GrpcNetClient", GrpcNetClient },
{ "log4net", log4net },
+ { "NLog", NLog },
{ "MassTransit", MassTransit },
{ "SqlClientMicrosoft", SqlClientMicrosoft },
{ "SqlClientSystem", SqlClientSystem },
diff --git a/test/IntegrationTests/NLogBridgeTests.cs b/test/IntegrationTests/NLogBridgeTests.cs
new file mode 100644
index 0000000000..3e9adfd5c5
--- /dev/null
+++ b/test/IntegrationTests/NLogBridgeTests.cs
@@ -0,0 +1,189 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.RegularExpressions;
+using Google.Protobuf;
+using IntegrationTests.Helpers;
+using OpenTelemetry.Proto.Logs.V1;
+using Xunit.Abstractions;
+
+namespace IntegrationTests;
+
+public class NLogBridgeTests : TestHelper
+{
+ public NLogBridgeTests(ITestOutputHelper output)
+ : base("NLogBridge", output)
+ {
+ }
+
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.NLog), MemberType = typeof(LibraryVersion))]
+ public void SubmitLogs_ThroughNLogBridge_WhenNLogIsUsedDirectlyForLogging(string packageVersion)
+ {
+ using var collector = new MockLogsCollector(Output);
+ SetExporter(collector);
+
+ // Logged in scope of an activity
+ collector.Expect(
+ logRecord =>
+ VerifyBody(logRecord, "{0}, {1} at {2:t}!") &&
+ VerifyTraceContext(logRecord) &&
+ logRecord is { SeverityText: "Info", SeverityNumber: SeverityNumber.Info } &&
+ VerifyAttributes(logRecord) &&
+ logRecord.Attributes.Count == 4,
+ "Expected Info record.");
+
+ // Logged with exception
+ collector.Expect(
+ logRecord =>
+ VerifyBody(logRecord, "Exception occured") &&
+ logRecord is { SeverityText: "Error", SeverityNumber: SeverityNumber.Error } &&
+ VerifyExceptionAttributes(logRecord) &&
+ logRecord.Attributes.Count == 4,
+ "Expected Error record.");
+
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE", "true");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api nlog"
+ });
+
+ AssertStandardOutputExpectations(standardOutput);
+
+ collector.AssertExpectations();
+ }
+
+#if NET
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.NLog), MemberType = typeof(LibraryVersion))]
+ public void SubmitLogs_ThroughILoggerBridge_WhenNLogIsUsedAsILoggerProviderForLogging(string packageVersion)
+ {
+ using var collector = new MockLogsCollector(Output);
+ SetExporter(collector);
+
+ // Logged in scope of an activity
+ collector.Expect(
+ logRecord =>
+ VerifyBody(logRecord, "{0}, {1} at {2:t}!") &&
+ VerifyTraceContext(logRecord) &&
+ logRecord is { SeverityText: "Info", SeverityNumber: SeverityNumber.Info } &&
+ // 0 : "Hello"
+ // 1 : "world"
+ // 2 : timestamp
+ logRecord.Attributes.Count == 3,
+ "Expected Info record.");
+
+ // Logged with exception
+ collector.Expect(
+ logRecord =>
+ VerifyBody(logRecord, "Exception occured") &&
+ // OtlpLogExporter adds exception related attributes (ConsoleExporter doesn't show them)
+ logRecord is { SeverityText: "Error", SeverityNumber: SeverityNumber.Error } &&
+ VerifyExceptionAttributes(logRecord) &&
+ logRecord.Attributes.Count == 3,
+ "Expected Error record.");
+
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE", "true");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api ILogger"
+ });
+
+ AssertStandardOutputExpectations(standardOutput);
+
+ collector.AssertExpectations();
+ }
+
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.NLog), MemberType = typeof(LibraryVersion))]
+ public async Task SubmitLogs_ThroughILoggerBridge_WhenNLogIsUsedAsILoggerProviderForLogging_WithoutDuplicates(string packageVersion)
+ {
+ using var collector = new MockLogsCollector(Output);
+ SetExporter(collector);
+
+ collector.ExpectCollected(records => records.Count == 3, "App logs should be exported once.");
+
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE", "true");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api ILogger"
+ });
+
+ AssertStandardOutputExpectations(standardOutput);
+
+ // wait for fixed amount of time for logs to be collected before asserting
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ collector.AssertCollected();
+ }
+
+#endif
+
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.NLog), MemberType = typeof(LibraryVersion))]
+ public void TraceContext_IsInjectedIntoCurrentNLogLogsDestination(string packageVersion)
+ {
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE", "false");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api nlog"
+ });
+
+ var regex = new Regex(@"INFO TestApplication\.NLogBridge\.Program - Hello, world at \d{2}\:\d{2}\! span_id=[a-f0-9]{16} trace_id=[a-f0-9]{32} trace_flags=01");
+ var output = standardOutput;
+ Assert.Matches(regex, output);
+ Assert.Contains("ERROR TestApplication.NLogBridge.Program - Exception occured span_id=(null) trace_id=(null) trace_flags=(null)", output);
+ }
+
+ private static bool VerifyAttributes(LogRecord logRecord)
+ {
+ var firstArgAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "0");
+ var secondArgAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "1");
+ var customAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "test_key");
+ return firstArgAttribute?.Value.StringValue == "Hello" &&
+ secondArgAttribute?.Value.StringValue == "world" &&
+ logRecord.Attributes.Count(value => value.Key == "2") == 1 &&
+ customAttribute?.Value.StringValue == "test_value";
+ }
+
+ private static bool VerifyTraceContext(LogRecord logRecord)
+ {
+ return logRecord.TraceId != ByteString.Empty &&
+ logRecord.SpanId != ByteString.Empty &&
+ logRecord.Flags != 0;
+ }
+
+ private static void AssertStandardOutputExpectations(string standardOutput)
+ {
+ Assert.Contains("INFO TestApplication.NLogBridge.Program - Hello, world at", standardOutput);
+ Assert.Contains("ERROR TestApplication.NLogBridge.Program - Exception occured", standardOutput);
+ }
+
+ private static bool VerifyBody(LogRecord logRecord, string expectedBody)
+ {
+ return Convert.ToString(logRecord.Body) == $"{{ \"stringValue\": \"{expectedBody}\" }}";
+ }
+
+ private static bool VerifyExceptionAttributes(LogRecord logRecord)
+ {
+ return logRecord.Attributes.Count(value => value.Key == "exception.stacktrace") == 1 &&
+ logRecord.Attributes.Count(value => value.Key == "exception.message") == 1 &&
+ logRecord.Attributes.Count(value => value.Key == "exception.type") == 1;
+ }
+}
diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs
index 1b5ebd8fab..b5cdf3c76c 100644
--- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs
@@ -348,6 +348,7 @@ internal void MeterSettings_Instrumentations_SupportedValues(string meterInstrum
[Theory]
[InlineData("ILOGGER", LogInstrumentation.ILogger)]
[InlineData("LOG4NET", LogInstrumentation.Log4Net)]
+ [InlineData("NLOG", LogInstrumentation.NLog)]
internal void LogSettings_Instrumentations_SupportedValues(string logInstrumentation, LogInstrumentation expectedLogInstrumentation)
{
Environment.SetEnvironmentVariable(ConfigurationKeys.Logs.LogsInstrumentationEnabled, "false");
diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs
new file mode 100644
index 0000000000..af79b84948
--- /dev/null
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs
@@ -0,0 +1,144 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.Bridge;
+using OpenTelemetry.Trace;
+using Xunit;
+
+namespace OpenTelemetry.AutoInstrumentation.Tests;
+
+///
+/// Unit tests for NLog instrumentation functionality.
+/// These tests verify that NLog log levels are correctly mapped to OpenTelemetry severity levels
+/// and that the NLog bridge functions properly.
+///
+public class NLogTests
+{
+ // TODO: Remove when Logs Api is made public in non-rc builds.
+ private static readonly Type OpenTelemetryLogSeverityType = typeof(Tracer).Assembly.GetType("OpenTelemetry.Logs.LogRecordSeverity")!;
+
+ ///
+ /// Provides test data for NLog level mapping tests.
+ /// This includes all standard NLog levels and their expected OpenTelemetry severity mappings.
+ ///
+ /// Theory data containing NLog level ordinals and expected OpenTelemetry severity values.
+ public static TheoryData GetLevelMappingData()
+ {
+ var theoryData = new TheoryData
+ {
+ // NLog.LogLevel.Trace (0) -> LogRecordSeverity.Trace (1)
+ { 0, GetOpenTelemetrySeverityValue("Trace") },
+
+ // NLog.LogLevel.Debug (1) -> LogRecordSeverity.Debug (5)
+ { 1, GetOpenTelemetrySeverityValue("Debug") },
+
+ // NLog.LogLevel.Info (2) -> LogRecordSeverity.Info (9)
+ { 2, GetOpenTelemetrySeverityValue("Info") },
+
+ // NLog.LogLevel.Warn (3) -> LogRecordSeverity.Warn (13)
+ { 3, GetOpenTelemetrySeverityValue("Warn") },
+
+ // NLog.LogLevel.Error (4) -> LogRecordSeverity.Error (17)
+ { 4, GetOpenTelemetrySeverityValue("Error") },
+
+ // NLog.LogLevel.Fatal (5) -> LogRecordSeverity.Fatal (21)
+ { 5, GetOpenTelemetrySeverityValue("Fatal") }
+ };
+
+ return theoryData;
+ }
+
+ ///
+ /// Tests that standard NLog log levels are correctly mapped to OpenTelemetry severity levels.
+ /// This verifies that the bridge correctly translates NLog's ordinal-based level system
+ /// to OpenTelemetry's severity enumeration.
+ ///
+ /// The NLog level ordinal value.
+ /// The expected OpenTelemetry severity level.
+ [Theory]
+ [MemberData(nameof(GetLevelMappingData))]
+ public void StandardNLogLevels_AreMappedCorrectly(int nlogLevelOrdinal, int expectedOpenTelemetrySeverity)
+ {
+ // Act
+ var actualSeverity = OpenTelemetryNLogConverter.MapLogLevel(nlogLevelOrdinal);
+
+ // Assert
+ Assert.Equal(expectedOpenTelemetrySeverity, actualSeverity);
+ }
+
+ ///
+ /// Tests that the NLog "Off" level (6) is handled correctly.
+ /// The "Off" level should be mapped to Trace severity, though typically
+ /// log events with "Off" level should be filtered out before reaching the target.
+ ///
+ [Fact]
+ public void OffLevel_IsMappedToTrace()
+ {
+ // Arrange
+ const int offLevelOrdinal = 6;
+ var expectedSeverity = GetOpenTelemetrySeverityValue("Trace");
+
+ // Act
+ var actualSeverity = OpenTelemetryNLogConverter.MapLogLevel(offLevelOrdinal);
+
+ // Assert
+ Assert.Equal(expectedSeverity, actualSeverity);
+ }
+
+ ///
+ /// Tests that unknown or invalid log level ordinals are mapped to Trace severity.
+ /// This ensures the bridge is resilient to unexpected level values.
+ ///
+ /// An invalid or unknown level ordinal.
+ [Theory]
+ [InlineData(-1)] // Negative ordinal
+ [InlineData(7)] // Beyond "Off"
+ [InlineData(100)] // Arbitrary high value
+ [InlineData(int.MaxValue)] // Maximum integer value
+ public void InvalidLevelOrdinals_AreMappedToTrace(int invalidOrdinal)
+ {
+ // Arrange
+ var expectedSeverity = GetOpenTelemetrySeverityValue("Trace");
+
+ // Act
+ var actualSeverity = OpenTelemetryNLogConverter.MapLogLevel(invalidOrdinal);
+
+ // Assert
+ Assert.Equal(expectedSeverity, actualSeverity);
+ }
+
+ ///
+ /// Tests that unknown or custom NLog levels are mapped to the default Trace severity.
+ /// This verifies that the fallback logic works correctly for non-standard level ordinals.
+ /// The mapping logic uses a switch expression with a default case that returns 1 (Trace severity)
+ /// for any ordinal that doesn't match the standard NLog levels (0-5).
+ ///
+ /// The NLog ordinal value for an unknown/custom level.
+ /// The expected OpenTelemetry severity level (should be 1 for Trace).
+ [Theory]
+ [InlineData(7, 1)] // Beyond standard levels (after Off=6) -> Trace
+ [InlineData(10, 1)] // Custom level -> Trace
+ [InlineData(-1, 1)] // Invalid negative ordinal -> Trace
+ [InlineData(100, 1)] // High custom level -> Trace
+ [InlineData(999, 1)] // Very high custom level -> Trace
+ public void UnknownCustomLevels_AreMappedToTraceSeverity(int nlogOrdinal, int expectedSeverity)
+ {
+ // Act
+ var actualSeverity = OpenTelemetryNLogConverter.MapLogLevel(nlogOrdinal);
+
+ // Assert
+ Assert.Equal(expectedSeverity, actualSeverity);
+ }
+
+ ///
+ /// Gets the numeric value of an OpenTelemetry log severity level by name.
+ /// This helper method uses reflection to access the internal LogRecordSeverity enum
+ /// since the Logs API is not yet public.
+ ///
+ /// The name of the severity level (e.g., "Info", "Error").
+ /// The numeric value of the severity level.
+ private static int GetOpenTelemetrySeverityValue(string severityName)
+ {
+ return (int)Enum.Parse(OpenTelemetryLogSeverityType, severityName);
+ }
+}
diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/OtelLoggingTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/OtelLoggingTests.cs
index 6a74665eda..40f7459604 100644
--- a/test/OpenTelemetry.AutoInstrumentation.Tests/OtelLoggingTests.cs
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/OtelLoggingTests.cs
@@ -147,7 +147,7 @@ public void WhenConsoleSinkIsUsed_Then_ConsoleContentIsDetected()
Environment.SetEnvironmentVariable("OTEL_LOG_LEVEL", "debug");
Environment.SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGGER", "console");
- var currentWritter = Console.Out;
+ var currentWriter = Console.Out;
using var ms = new MemoryStream();
using var tw = new StreamWriter(ms);
@@ -174,7 +174,7 @@ public void WhenConsoleSinkIsUsed_Then_ConsoleContentIsDetected()
}
finally
{
- Console.SetOut(currentWritter);
+ Console.SetOut(currentWriter);
}
}
@@ -184,7 +184,7 @@ public void AfterLoggerIsClosed_ConsecutiveLogCallsWithTheSameLoggerAreNotWritte
Environment.SetEnvironmentVariable("OTEL_LOG_LEVEL", "debug");
Environment.SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGGER", "console");
- var currentWritter = Console.Out;
+ var currentWriter = Console.Out;
using var ms = new MemoryStream();
using var tw = new StreamWriter(ms);
@@ -219,7 +219,7 @@ public void AfterLoggerIsClosed_ConsecutiveLogCallsWithTheSameLoggerAreNotWritte
}
finally
{
- Console.SetOut(currentWritter);
+ Console.SetOut(currentWriter);
}
}
@@ -229,7 +229,7 @@ public void AfterLoggerIsClosed_ConsecutiveCallsToGetLoggerReturnNoopLogger()
Environment.SetEnvironmentVariable("OTEL_LOG_LEVEL", "debug");
Environment.SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGGER", "console");
- var currentWritter = Console.Out;
+ var currentWriter = Console.Out;
using var ms = new MemoryStream();
using var tw = new StreamWriter(ms);
@@ -265,7 +265,7 @@ public void AfterLoggerIsClosed_ConsecutiveCallsToGetLoggerReturnNoopLogger()
}
finally
{
- Console.SetOut(currentWritter);
+ Console.SetOut(currentWriter);
}
}
diff --git a/test/test-applications/integrations/TestApplication.NLogBridge/NLogLogger.cs b/test/test-applications/integrations/TestApplication.NLogBridge/NLogLogger.cs
new file mode 100644
index 0000000000..73724d8f19
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.NLogBridge/NLogLogger.cs
@@ -0,0 +1,51 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using Microsoft.Extensions.Logging;
+using NLog;
+
+namespace TestApplication.NLogBridge;
+
+internal class NLogLogger : Microsoft.Extensions.Logging.ILogger
+{
+ private readonly NLog.ILogger _log;
+
+ public NLogLogger(string categoryName)
+ {
+ _log = LogManager.GetLogger(categoryName);
+ }
+
+ public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ switch (logLevel)
+ {
+ case Microsoft.Extensions.Logging.LogLevel.Information:
+ _log.Info(formatter(state, exception));
+ break;
+ case Microsoft.Extensions.Logging.LogLevel.Error:
+ _log.Error(exception, formatter(state, exception));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
+ }
+ }
+
+ public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ Microsoft.Extensions.Logging.LogLevel.Critical => _log.IsFatalEnabled,
+ Microsoft.Extensions.Logging.LogLevel.Debug or Microsoft.Extensions.Logging.LogLevel.Trace => _log.IsDebugEnabled,
+ Microsoft.Extensions.Logging.LogLevel.Error => _log.IsErrorEnabled,
+ Microsoft.Extensions.Logging.LogLevel.Information => _log.IsInfoEnabled,
+ Microsoft.Extensions.Logging.LogLevel.Warning => _log.IsWarnEnabled,
+ _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
+ };
+ }
+
+ public IDisposable? BeginScope(TState state)
+ where TState : notnull
+ {
+ return null;
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.NLogBridge/NLogLoggerProvider.cs b/test/test-applications/integrations/TestApplication.NLogBridge/NLogLoggerProvider.cs
new file mode 100644
index 0000000000..7f88b907a2
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.NLogBridge/NLogLoggerProvider.cs
@@ -0,0 +1,18 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using Microsoft.Extensions.Logging;
+
+namespace TestApplication.NLogBridge;
+
+public class NLogLoggerProvider : ILoggerProvider
+{
+ public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
+ {
+ return new NLogLogger(categoryName);
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.NLogBridge/Program.cs b/test/test-applications/integrations/TestApplication.NLogBridge/Program.cs
new file mode 100644
index 0000000000..633d3bf1f8
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.NLogBridge/Program.cs
@@ -0,0 +1,84 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using NLog;
+
+namespace TestApplication.NLogBridge;
+
+internal static class Program
+{
+ private static readonly ActivitySource Source = new("TestApplication.NLogBridge");
+
+ private static void Main(string[] args)
+ {
+ if (args.Length == 2)
+ {
+ // Set global context property for testing
+ GlobalDiagnosticsContext.Set("test_key", "test_value");
+
+ var logApiName = args[1];
+ switch (logApiName)
+ {
+ case "nlog":
+ LogUsingNLogDirectly();
+ break;
+ case "ILogger":
+ LogUsingILogger();
+ break;
+ default:
+ throw new NotSupportedException($"{logApiName} is not supported.");
+ }
+ }
+ else
+ {
+ throw new ArgumentException("Invalid arguments.");
+ }
+ }
+
+ private static void LogUsingILogger()
+ {
+ var l = LogManager.GetLogger("TestApplication.NLogBridge");
+ l.Warn("Before logger factory is built.");
+
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddProvider(new NLogLoggerProvider());
+ });
+ var logger = loggerFactory.CreateLogger(typeof(Program));
+
+ LogInsideActiveScope(() => logger.LogInformation("{0}, {1} at {2:t}!", "Hello", "world", DateTime.Now));
+
+ var (message, ex) = GetException();
+ logger.LogError(ex, message);
+ }
+
+ private static void LogInsideActiveScope(Action action)
+ {
+ using var activity = Source.StartActivity("ManuallyStarted");
+ action();
+ }
+
+ private static void LogUsingNLogDirectly()
+ {
+ var log = LogManager.GetLogger(typeof(Program).FullName!);
+
+ LogInsideActiveScope(() => log.Info("{0}, {1} at {2:t}!", "Hello", "world", DateTime.Now));
+
+ var (message, ex) = GetException();
+ log.Error(ex, message);
+ }
+
+ private static (string Message, Exception Exception) GetException()
+ {
+ try
+ {
+ throw new InvalidOperationException("Example exception for testing");
+ }
+ catch (Exception ex)
+ {
+ return ("Exception occured", ex);
+ }
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.NLogBridge/TestApplication.NLogBridge.csproj b/test/test-applications/integrations/TestApplication.NLogBridge/TestApplication.NLogBridge.csproj
new file mode 100644
index 0000000000..229575f924
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.NLogBridge/TestApplication.NLogBridge.csproj
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/test-applications/integrations/TestApplication.NLogBridge/nlog.config b/test/test-applications/integrations/TestApplication.NLogBridge/nlog.config
new file mode 100644
index 0000000000..2976cdc7c7
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.NLogBridge/nlog.config
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
index 06a04fa2f8..d51aafed1b 100644
--- a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
+++ b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
@@ -101,6 +101,20 @@ all lower versions than 8.15.10 contains references impacted by
}
},
new()
+ {
+ IntegrationName = "NLog",
+ NugetPackageName = "NLog",
+ TestApplicationName = "TestApplication.NLogBridge",
+ Versions = new List
+ {
+ // NLog 5.0+ required for Layout typed layout support and .NET build-trimming
+ new("5.0.0"),
+ new("5.3.4"),
+ new("6.0.0"),
+ new("*")
+ }
+ },
+ new()
{
IntegrationName = "MassTransit",
NugetPackageName = "MassTransit",