diff --git a/.cspell/other.txt b/.cspell/other.txt
index c23c022084..1f37df497e 100644
--- a/.cspell/other.txt
+++ b/.cspell/other.txt
@@ -29,6 +29,7 @@ ILOGGER
inetsrv
JIT
LINQ
+longdate
MASSTRANSIT
metricsexporter
mkdir
@@ -41,6 +42,7 @@ mycompanymyproductmylibrary
MYSQLCONNECTOR
MYSQLDATA
NETRUNTIME
+NLOG
Npgsql
NSERVICEBUS
omnisharp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 996b695e15..27f4b7013d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ This component adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.h
- Add `-NoReset` switch parameter to skip service restart in PowerShell
registration functions.
+- 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
diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln
index 579e820207..2ef0e8b1e0 100644
--- a/OpenTelemetry.AutoInstrumentation.sln
+++ b/OpenTelemetry.AutoInstrumentation.sln
@@ -246,6 +246,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}"
@@ -1538,6 +1540,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
@@ -1692,6 +1710,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}
{500BF40F-EECB-4F6A-377B-EDBDFFDE09BE} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
diff --git a/build/LibraryVersions.g.cs b/build/LibraryVersions.g.cs
index dc2b2ef1b0..f045ce7374 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", additionalMetaData: new() { { "NLogExtensionsLogging", "5.0.0" } }),
+ new("5.3.4", additionalMetaData: new() { { "NLogExtensionsLogging", "5.3.15" } }),
+ new("6.0.0", additionalMetaData: new() { { "NLogExtensionsLogging", "6.0.0" } }),
+ new("6.0.6", additionalMetaData: new() { { "NLogExtensionsLogging", "6.1.0" } }),
+ ]
+ },
{
"TestApplication.MassTransit",
[
diff --git a/build/TargetFramework.cs b/build/TargetFramework.cs
index 51d53a5828..b6e1208594 100644
--- a/build/TargetFramework.cs
+++ b/build/TargetFramework.cs
@@ -19,7 +19,7 @@ public class TargetFramework : Enumeration
// should be in version order
public static readonly TargetFramework[] NetFramework = {
- NET462, NET47, NET471, NET472, NET472
+ NET462, NET47, NET471, NET472
};
public static implicit operator string(TargetFramework framework)
diff --git a/docs/config.md b/docs/config.md
index d8cb59e44a..cfe3d28c55 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -206,6 +206,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** | ≥8.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
@@ -215,6 +216,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/file-based-configuration.md b/docs/file-based-configuration.md
index 3476f2184f..9c24a44603 100644
--- a/docs/file-based-configuration.md
+++ b/docs/file-based-configuration.md
@@ -526,6 +526,7 @@ instrumentation/development:
logs:
ilogger: # Microsoft.Extensions.Logging
log4net: # Log4Net
+ nlog: # NLog
```
## Instrumentation options
@@ -567,6 +568,10 @@ instrumentation/development:
# Logs bridge is disabled by default
# More info about log4net bridge can be found at https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/main/docs/log4net-bridge.md
bridge_enabled: true
+ nlog:
+ # Logs bridge is disabled by default
+ # More info about NLog bridge can be found at https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/main/docs/nlog-bridge.md
+ bridge_enabled: true
```
## Instrumentation Additional Sources
diff --git a/docs/log-trace-correlation.md b/docs/log-trace-correlation.md
index 97c24fe249..e6471423f2 100644
--- a/docs/log-trace-correlation.md
+++ b/docs/log-trace-correlation.md
@@ -62,3 +62,27 @@ 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.
diff --git a/docs/nlog-bridge.md b/docs/nlog-bridge.md
new file mode 100644
index 0000000000..6719f4110d
--- /dev/null
+++ b/docs/nlog-bridge.md
@@ -0,0 +1,45 @@
+# `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 8b13789179..97cbd646f8 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
-
+OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration
+OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration
diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
index e69de29bb2..97cbd646f8 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,2 @@
+OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration
+OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
index c6420d49a9..2781167ff2 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
@@ -240,6 +240,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/FileBasedConfiguration/DotNetLogs.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/DotNetLogs.cs
index b854c06aca..3d97f6dd32 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/DotNetLogs.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/DotNetLogs.cs
@@ -17,7 +17,13 @@ internal class DotNetLogs
/// Gets or sets the Log4Net logs instrumentation configuration.
///
[YamlMember(Alias = "log4net")]
- public Log4NetBridgeEnabled? Log4Net { get; set; }
+ public LogBridgeEnabled? Log4Net { get; set; }
+
+ ///
+ /// Gets or sets the Log4Net logs instrumentation configuration.
+ ///
+ [YamlMember(Alias = "nlog")]
+ public LogBridgeEnabled? NLog { get; set; }
///
/// Returns the list of enabled log instrumentations.
@@ -36,6 +42,11 @@ public IReadOnlyList GetEnabledInstrumentations()
result.Add(LogInstrumentation.Log4Net);
}
+ if (NLog != null)
+ {
+ result.Add(LogInstrumentation.NLog);
+ }
+
return result;
}
}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/Log4NetBridgeEnabled.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/LogBridgeEnabled.cs
similarity index 93%
rename from src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/Log4NetBridgeEnabled.cs
rename to src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/LogBridgeEnabled.cs
index 288108f53e..84ab725719 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/Log4NetBridgeEnabled.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/FileBasedConfiguration/LogBridgeEnabled.cs
@@ -7,7 +7,7 @@
namespace OpenTelemetry.AutoInstrumentation.Configurations.FileBasedConfiguration;
[EmptyObjectOnEmptyYaml]
-internal class Log4NetBridgeEnabled
+internal class LogBridgeEnabled
{
///
/// Gets or sets a value indicating whether the Log4Net bridge is enabled.
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 e95c167b33..f5a80bd9c1 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
@@ -34,6 +34,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.
///
@@ -60,6 +65,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) ??
@@ -85,6 +91,11 @@ protected override void OnLoadFile(YamlConfiguration configuration)
{
EnableLog4NetBridge = logs?.Log4Net?.BridgeEnabled ?? false;
}
+
+ if (EnabledInstrumentations.Contains(LogInstrumentation.NLog))
+ {
+ EnableNLogBridge = logs?.NLog?.BridgeEnabled ?? false;
+ }
}
private static List ParseLogExporter(Configuration configuration)
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..e65aea14b0 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(42);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -101,6 +101,17 @@ 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", "WriteToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.TargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteLogEventToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.TargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteLogEventToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ }
}
// 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 e3f0643c0a..dcb4f6df17 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(45);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -104,6 +104,17 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray()
{
nativeCallTargetDefinitions.Add(new("Microsoft.Extensions.Logging", "Microsoft.Extensions.Logging.LoggingBuilder", ".ctor", ["System.Void", "Microsoft.Extensions.DependencyInjection.IServiceCollection"], 8, 0, 0, 10, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Logger.LoggingBuilderIntegration"));
}
+
+ // NLog
+ if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.NLog))
+ {
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.TargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteLogEventToTargets", ["System.Void", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.TargetWithFilterChain"], 5, 0, 0, 5, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ nativeCallTargetDefinitions.Add(new("NLog", "NLog.Logger", "WriteLogEventToTargets", ["System.Void", "System.Type", "NLog.LogEventInfo", "NLog.Internal.ITargetWithFilterChain"], 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection.Integrations.NLogWriteToTargetsWithWrapperTypeIntegration"));
+ }
}
// Metrics
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
index bfd8f03edb..e1078fc950 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
@@ -358,7 +358,18 @@ private static void InitializeBufferProcessing(TimeSpan exportInterval, TimeSpan
{
// ILogger bridge is initialized using ILogger-specific extension methods in LoggerInitializer class.
// That extension methods sets up its own LogProvider.
- if (LogSettings.Value.EnableLog4NetBridge && LogSettings.Value.LogsEnabled && LogSettings.Value.EnabledInstrumentations.Contains(LogInstrumentation.Log4Net))
+
+ Logger.Debug($"InitializeLoggerProvider called. LogsEnabled={LogSettings.Value.LogsEnabled}, EnableLog4NetBridge={LogSettings.Value.EnableLog4NetBridge}, EnableNLogBridge={LogSettings.Value.EnableNLogBridge}");
+ Logger.Debug($"EnabledInstrumentations: {string.Join(", ", LogSettings.Value.EnabledInstrumentations)}");
+
+ // Initialize logger provider if any bridge is enabled
+ var shouldInitialize = LogSettings.Value.LogsEnabled && (
+ (LogSettings.Value.EnableLog4NetBridge && LogSettings.Value.EnabledInstrumentations.Contains(LogInstrumentation.Log4Net)) ||
+ (LogSettings.Value.EnableNLogBridge && LogSettings.Value.EnabledInstrumentations.Contains(LogInstrumentation.NLog)));
+
+ Logger.Debug($"ShouldInitialize logger provider: {shouldInitialize}");
+
+ if (shouldInitialize)
{
// TODO: Replace reflection usage when Logs Api is made public in non-rc builds.
// Sdk.CreateLoggerProviderBuilder()
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..16a6f8fa5e
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryLogHelpers.cs
@@ -0,0 +1,356 @@
+// 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, [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(
+ [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);
+
+ Expression assignInstanceVar;
+
+ // If no parameterless constructor, try to find other constructors or use default for structs
+ if (constructorInfo == null)
+ {
+ // Try to find a constructor that takes an int (capacity)
+ constructorInfo = logRecordAttributesListType.GetConstructor([typeof(int)]);
+ if (constructorInfo != null)
+ {
+ assignInstanceVar = Expression.Assign(instanceVar, Expression.New(constructorInfo, Expression.Constant(4)));
+ }
+ else if (logRecordAttributesListType.IsValueType)
+ {
+ // For structs, use default value
+ assignInstanceVar = Expression.Assign(instanceVar, Expression.Default(logRecordAttributesListType));
+ }
+ else
+ {
+ throw new InvalidOperationException($"No suitable constructor found for {logRecordAttributesListType.Name}");
+ }
+ }
+ else
+ {
+ assignInstanceVar = Expression.Assign(instanceVar, Expression.New(constructorInfo));
+ }
+
+ var addAttributeMethodInfo = logRecordAttributesListType.GetMethod("Add", [typeof(string), typeof(object)])!;
+ var recordExceptionMethodInfo = logRecordAttributesListType.GetMethod("RecordException", BindingFlags.Instance | BindingFlags.Public)!;
+
+ var expressions = new List { assignInstanceVar };
+
+ // Record exception using RecordException which adds exception.type, exception.message, exception.stacktrace
+ var recordExceptionExpression = Expression.IfThen(
+ Expression.NotEqual(exception, Expression.Constant(null)),
+ Expression.Call(instanceVar, recordExceptionMethodInfo, exception));
+ expressions.Add(recordExceptionExpression);
+
+ // 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(
+ [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 ConditionalExpression 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(
+ [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 ConditionalExpression 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(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(
+ [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, [logRecordDataType.MakeByRefType(), logRecordAttributesListType.MakeByRefType()], null)!;
+
+ // Create local variables to hold the log record and attributes
+ // This is required for .NET Framework compatibility - expression trees with
+ // TryExpression cannot be passed directly to methods with ref parameters.
+ // By assigning to local variables first, we avoid this limitation.
+ var logRecordVar = Expression.Variable(logRecordDataType, "logRecord");
+ var attributesVar = Expression.Variable(logRecordAttributesListType, "attributes");
+
+ // Build the complete expression that:
+ // 1. Creates the log record and assigns to local variable
+ // 2. Creates attributes and assigns to local variable
+ // 3. Calls EmitLog with the local variables by reference
+ var completeExpression = Expression.Block(
+ [logRecordVar, attributesVar],
+ Expression.Assign(logRecordVar, logRecordExpression),
+ Expression.Assign(attributesVar, attributesExpression),
+ Expression.Call(
+ Expression.Convert(loggerInstance, loggerType),
+ emitLogRecordMethod,
+ logRecordVar,
+ attributesVar));
+
+ // 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..b624769a57
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/Bridge/OpenTelemetryNLogConverter.cs
@@ -0,0 +1,259 @@
+// 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;
+using OpenTelemetry.AutoInstrumentation.Logging;
+#if NET
+using OpenTelemetry.AutoInstrumentation.Logger; // Only needed for LoggerInitializer
+#endif
+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 = ["NLog.LogEventInfo, NLog"])]
+ public void WriteLogEvent(ILoggingEvent loggingEvent)
+ {
+ if (Sdk.SuppressInstrumentation || loggingEvent.Level.Ordinal == OffOrdinal)
+ {
+ return;
+ }
+
+ var emitter = OpenTelemetryLogHelpers.LogEmitter;
+ if (emitter == null)
+ {
+ 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 List>? GetProperties(ILoggingEvent loggingEvent)
+ {
+ var result = new List>();
+
+ try
+ {
+ var properties = loggingEvent.Properties;
+ if (properties != null)
+ {
+ // Try to cast to IDictionary first, if that fails, try IEnumerable
+ if (properties is IDictionary dict)
+ {
+ foreach (var prop in GetFilteredProperties(dict))
+ {
+ result.Add(prop);
+ }
+ }
+ else if (properties is IEnumerable> enumerable)
+ {
+ foreach (var prop in GetFilteredProperties(enumerable))
+ {
+ result.Add(prop);
+ }
+ }
+ else
+ {
+ // If it's some other type, try to duck cast it
+ if (properties.TryDuckCast(out var duckDict))
+ {
+ foreach (var prop in GetFilteredProperties(duckDict))
+ {
+ result.Add(prop);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Debug($"Failed to get event properties: {ex.Message}");
+ }
+
+ return result.Count > 0 ? result : null;
+ }
+
+ private static IEnumerable> GetFilteredProperties(IDictionary properties)
+ {
+ foreach (var propertyKey in properties.Keys)
+ {
+ if (propertyKey is not string key)
+ {
+ continue;
+ }
+
+ if (EvaluateFilterCondition(key))
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(key, properties[key]);
+ }
+ }
+
+ private static IEnumerable> GetFilteredProperties(IEnumerable> properties)
+ {
+ foreach (var property in properties)
+ {
+ if (EvaluateFilterCondition(property.Key))
+ {
+ continue;
+ }
+
+ yield return property;
+ }
+ }
+
+ private static bool EvaluateFilterCondition(string key)
+ {
+ return key.StartsWith("NLog.", StringComparison.Ordinal) ||
+ key.StartsWith("nlog:", StringComparison.Ordinal) ||
+ key == LogsTraceContextInjectionConstants.SpanIdPropertyName ||
+ key == LogsTraceContextInjectionConstants.TraceIdPropertyName ||
+ key == LogsTraceContextInjectionConstants.TraceFlagsPropertyName;
+ }
+
+ private static Func? CreateGetLoggerDelegate(LoggerProvider loggerProvider)
+ {
+ try
+ {
+ var methodInfo = typeof(LoggerProvider)
+ .GetMethod("GetLogger", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(string)], null)!;
+#if NET
+ return methodInfo.CreateDelegate>(loggerProvider);
+#else
+ return (Func)methodInfo.CreateDelegate(typeof(Func), loggerProvider);
+#endif
+ }
+ 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..eeb167fac5
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/ILoggingEvent.cs
@@ -0,0 +1,89 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
+#pragma warning disable SA1201 // Elements should appear in the correct order
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog;
+
+///
+/// Duck typing struct that wraps NLog's LogEventInfo struct.
+/// This struct 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
+///
+[DuckCopy]
+internal struct ILoggingEvent
+{
+ ///
+ /// Gets the logging level of the log event.
+ /// Maps to NLog's LogLevel property.
+ ///
+ public LoggingLevel Level;
+
+ ///
+ /// Gets the name of the logger that created the log event.
+ /// Maps to NLog's LoggerName property.
+ ///
+ public string? LoggerName;
+
+ ///
+ /// Gets the formatted log message.
+ /// Maps to NLog's FormattedMessage property.
+ ///
+ public string? FormattedMessage;
+
+ ///
+ /// Gets the exception associated with the log event, if any.
+ /// Maps to NLog's Exception property.
+ ///
+ public Exception? Exception;
+
+ ///
+ /// Gets the timestamp when the log event was created.
+ /// Maps to NLog's TimeStamp property.
+ ///
+ public DateTime TimeStamp;
+
+ ///
+ /// Gets the message object before formatting.
+ /// Maps to NLog's Message property.
+ ///
+ public object? Message;
+
+ ///
+ /// Gets the parameters for the log message.
+ /// Maps to NLog's Parameters property.
+ ///
+ public object?[]? Parameters;
+
+ ///
+ /// Gets the properties collection for custom properties.
+ /// Used for injecting trace context and storing additional metadata.
+ /// Maps to NLog's Properties property.
+ ///
+ public object? Properties;
+}
+
+///
+/// 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/README.md b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/README.md
new file mode 100644
index 0000000000..c730ecbd8e
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/README.md
@@ -0,0 +1,299 @@
+# NLog OpenTelemetry Auto-Instrumentation
+
+This directory contains the NLog instrumentation for OpenTelemetry .NET
+Auto-Instrumentation. This instrumentation provides automatic bytecode
+interception for bridging NLog logging events to OpenTelemetry using duck typing.
+
+## Overview
+
+The NLog instrumentation offers automatic integration through:
+
+1. **Bytecode Interception**: Automatically intercepts NLog's internal
+ `WriteToTargets` methods via bytecode instrumentation
+2. **Duck Typing Integration**: Uses duck typing to avoid direct NLog assembly references
+3. **Log Event Bridging**: Converting NLog log events to OpenTelemetry log
+ records (when bridge is enabled)
+4. **Structured Logging Support**: Leveraging NLog's layout abilities for enrichment
+5. **Trace Context Injection**: Automatically injects trace context into NLog
+ for all targets
+6. **Custom Properties**: Forwarding custom properties while filtering internal
+ NLog properties
+
+**Note**: No NLog configuration changes are required. The instrumentation works
+exclusively through bytecode interception and relies on OpenTelemetry
+environment variables for configuration.
+
+## Architecture
+
+### Bytecode Interception Path
+
+```text
+NLog Logger.Info/Debug/Warn/Error/etc. Call
+ ↓
+NLog Internal WriteToTargets/WriteLogEventToTargets
+ ↓
+WriteToTargetsIntegration / WriteLogEventToTargetsIntegration (CallTarget - Bytecode Interception)
+ ├─ ALWAYS: Inject trace context into NLog properties
+ │ (Available to ALL NLog targets: file, console, database, etc.)
+ │
+ └─ IF bridge enabled: Forward to OpenTelemetry
+ ↓
+ OpenTelemetryNLogConverter.WriteLogEvent()
+ ↓
+ OpenTelemetry LogRecord
+ ↓
+ OTLP Exporters
+```
+
+The instrumentation intercepts NLog's internal `WriteToTargets` (NLog 5.x) and
+`WriteLogEventToTargets` (NLog 6.x) methods at the bytecode level. This ensures
+log events are captured, including those from convenience methods like
+`log.Info()`, `log.Debug()`, etc.
+
+## Components
+
+### Core Components
+
+#### Duck Types (`ILoggingEvent.cs`)
+
+- **`ILoggingEvent`**: Duck typing struct for NLog's LogEventInfo
+- **`LoggingLevel`**: Duck typing struct for NLog's LogLevel
+
+#### Bridge Components (`Bridge/`)
+
+- **`OpenTelemetryNLogConverter.cs`**: Converts NLog events to OpenTelemetry
+ log records
+- **`OpenTelemetryLogHelpers.cs`**: Helper for creating OpenTelemetry log
+ records via expression trees
+
+### Trace Context Injection (`TraceContextInjection/`)
+
+#### Integrations
+
+- **`WriteToTargetsIntegration.cs`**: For NLog 5.3.0+ (uses
+ `ITargetWithFilterChain` interface)
+- **`WriteToTargetsLegacyIntegration.cs`**: For NLog 5.0.0-5.2.x (uses
+ `TargetWithFilterChain` class)
+- **`WriteToTargetsWithWrapperTypeIntegration.cs`**: For NLog 5.3.0+ with
+ wrapperType parameter
+- **`WriteToTargetsWithWrapperTypeLegacyIntegration.cs`**: For NLog 5.0.0-5.2.x
+ with wrapperType parameter
+- **`WriteLogEventToTargetsIntegration.cs`**: For NLog 6.x (method renamed to `WriteLogEventToTargets`)
+- **`WriteLogEventToTargetsWithWrapperTypeIntegration.cs`**: For NLog 6.x with
+ wrapperType parameter
+- **`NLogIntegrationHelper.cs`**: Shared helper with common trace context
+ injection and bridge logic
+
+The wrapperType integrations handle calls made via
+`Logger.Log(Type wrapperType, LogEventInfo logEvent)`.
+
+#### Supporting Types
+
+- **`ILogEventInfoProperties.cs`**: Duck-typed interface for accessing LogEventInfo.Properties
+- **`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 bridge 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 instrumentation automatically:
+
+- **Injects trace context** into NLog properties (TraceId, SpanId, TraceFlags)
+ for ALL NLog targets
+- Uses formatted message if available, otherwise raw message (when bridge enabled)
+- Includes event parameters when present (when bridge enabled)
+- Captures trace context from `Activity.Current`
+- Forwards custom properties while filtering internal NLog properties (when
+ bridge enabled)
+
+#### Trace Context Injection
+
+Trace context is **always injected** into NLog's LogEventInfo properties,
+regardless of whether the OpenTelemetry bridge is enabled. This allows NLog's
+own targets (file, console, database, etc.) to access trace context using NLog's
+layout renderers:
+
+```xml
+
+```
+
+The following properties are injected when an active `Activity` exists:
+
+- `TraceId`: The W3C trace ID
+- `SpanId`: The W3C span ID
+- `TraceFlags`: The W3C trace flags ("01" if recorded, "00" otherwise)
+
+#### OpenTelemetry Bridge
+
+When `OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true`, log events are
+additionally forwarded to OpenTelemetry's logging infrastructure for export via
+OTLP or other configured exporters.
+
+## Supported Versions
+
+- **NLog 5.0.0 - 5.2.x**: Uses `WriteToTargetsLegacyIntegration` (TargetWithFilterChain)
+- **NLog 5.3.0+**: Uses `WriteToTargetsIntegration` (ITargetWithFilterChain)
+- **NLog 6.x**: Uses `WriteLogEventToTargetsIntegration` (WriteLogEventToTargets
+ method)
+- **.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 | (skipped) | - |
+
+## Duck Typing
+
+The instrumentation uses duck typing to interact with NLog without requiring
+direct references:
+
+- **`ILoggingEvent`**: Maps to `NLog.LogEventInfo` (using `[DuckCopy]` struct)
+- **`LoggingLevel`**: Maps to `NLog.LogLevel` (using `[DuckCopy]` struct)
+- **`ILogEventInfoProperties`**: Maps to LogEventInfo.Properties for trace
+ context injection
+
+## Property Handling
+
+### Captured Properties
+
+- All custom properties from `LogEventInfo.Properties`
+- Message template arguments (indexed as "0", "1", "2", etc.)
+
+### Filtered Properties
+
+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`)
+
+### Exception Handling
+
+Exceptions are recorded using OpenTelemetry's `RecordException` method, which adds:
+
+- `exception.type`: The exception type name
+- `exception.message`: The exception message
+- `exception.stacktrace`: The full stack trace
+
+## Performance Considerations
+
+- **Logger Caching**: OpenTelemetry loggers are cached (up to 100 entries) to
+ avoid recreation overhead
+- **Lazy Initialization**: Components are initialized only when needed
+- **Minimal Overhead**: Bytecode interception adds minimal overhead to logging calls
+
+## 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 NLog bridge when ILogger
+ bridge is active to prevent duplicate logs
+
+## Testing
+
+### Unit Tests
+
+Tests are located in `test/OpenTelemetry.AutoInstrumentation.Tests/` and cover:
+
+- Level mapping verification
+- Edge case handling (invalid levels, off level)
+
+### Integration Tests
+
+Tests are located in `test/IntegrationTests/NLogBridgeTests.cs` and cover:
+
+- Direct NLog bridge logging (`SubmitLogs_ThroughNLogBridge_WhenNLogIsUsedDirectlyForLogging`)
+- ILogger bridge with NLog provider (`SubmitLogs_ThroughILoggerBridge_WhenNLogIsUsedAsILoggerProviderForLogging`)
+- Duplicate prevention (`SubmitLogs_ThroughILoggerBridge_WhenNLogIsUsedAsILoggerProviderForLogging_WithoutDuplicates`)
+- Trace context injection (`TraceContext_IsInjectedIntoCurrentNLogLogsDestination`)
+
+### Test Application
+
+A complete test application is available at
+`test/test-applications/integrations/TestApplication.NLogBridge/` that demonstrates:
+
+- Direct NLog usage
+- Microsoft.Extensions.Logging integration via NLogLoggerProvider
+- Structured logging scenarios
+- Exception logging
+- Trace context propagation
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Bridge Not Working**
+ - Verify `OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true`
+ - Check that NLog version is 5.0.0 or higher
+ - Ensure auto-instrumentation is properly loaded
+
+2. **Missing Trace Context**
+ - Verify `OTEL_DOTNET_AUTO_TRACES_ENABLED=true`
+ - Ensure an `Activity` is active when logging
+ - Check NLog layout includes `${event-properties:TraceId}` etc.
+
+3. **Missing Properties**
+ - Check NLog configuration for property capture
+ - Verify properties don't start with filtered prefixes
+
+4. **Duplicate Logs**
+ - If using NLog as ILogger provider, logs go through ILogger bridge
+ - NLog bridge automatically disables when ILogger bridge is active
+
+### Debug Information
+
+Enable OpenTelemetry auto-instrumentation logging:
+
+```bash
+export OTEL_DOTNET_AUTO_LOG_LEVEL=debug
+export OTEL_DOTNET_AUTO_LOG_DIRECTORY=/path/to/logs
+```
+
+## 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
+- Six separate integrations handle NLog version differences and method overloads:
+ - 2 for NLog 5.0-5.2 (with and without wrapperType parameter)
+ - 2 for NLog 5.3+ (with and without wrapperType parameter)
+ - 2 for NLog 6.x (with and without wrapperType parameter)
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/ILogEventInfoProperties.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/ILogEventInfoProperties.cs
new file mode 100644
index 0000000000..b5d0a70131
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/TraceContextInjection/ILogEventInfoProperties.cs
@@ -0,0 +1,21 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.NLog.TraceContextInjection;
+
+///
+/// Duck-typed interface for accessing NLog's LogEventInfo.Properties.
+/// This interface is used for trace context injection to add TraceId, SpanId,
+/// and TraceFlags to the log event properties.
+///
+///
+/// NLog's LogEventInfo.Properties is of type IDictionary{object, object}.
+/// We use the generic interface to match NLog's property type.
+///
+internal interface ILogEventInfoProperties
+{
+ ///
+ /// Gets the properties dictionary for the log event.
+ ///
+ public IDictionary