Skip to content

Add NLog instrumentation for OpenTelemetry .NET Auto-Instrumentation#4371

Merged
Kielek merged 89 commits intoopen-telemetry:mainfrom
danifitz:feature/nlog-instrumentation
Dec 17, 2025
Merged

Add NLog instrumentation for OpenTelemetry .NET Auto-Instrumentation#4371
Kielek merged 89 commits intoopen-telemetry:mainfrom
danifitz:feature/nlog-instrumentation

Conversation

@danifitz
Copy link
Contributor

@danifitz danifitz commented Aug 6, 2025

This commit implements a comprehensive NLog instrumentation that automatically bridges NLog logging events to OpenTelemetry without requiring code changes.

Features:

  • Automatic target injection into NLog's target collection
  • Complete log event bridging with level mapping
  • Structured logging support with message templates
  • Trace context integration
  • Custom properties forwarding with filtering
  • Comprehensive test coverage
  • End-to-end test application
  • Extensive documentation

The implementation follows the same patterns as the existing Log4Net instrumentation and supports NLog versions 4.0.0 through 6...

Configuration:

  • OTEL_DOTNET_AUTO_LOGS_ENABLED=true
  • OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true
  • OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE=true (optional)

Files added:

  • src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/
  • test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs
  • test/test-applications/integrations/TestApplication.NLog/

Files modified:

  • Configuration classes to support NLog bridge
  • Public API files to include new integration class

Why

#3938

Fixes #3938

What

This PR implements a comprehensive NLog instrumentation that automatically bridges NLog logging events to OpenTelemetry without requiring any code changes from users.

Tests

  • Unit Tests: 16 tests covering level mapping and edge cases
  • Integration Tests: Complete end-to-end test application
  • Compatibility: Verified on .NET 8.0 and .NET 9.0 with NLog 5.3.2

Checklist

  • CHANGELOG.md is updated.
  • Documentation is updated.
  • New features are covered by tests.

This commit implements a comprehensive NLog instrumentation that automatically
bridges NLog logging events to OpenTelemetry without requiring code changes.

Features:
- Automatic target injection into NLog's target collection
- Complete log event bridging with level mapping
- Structured logging support with message templates
- Trace context integration
- Custom properties forwarding with filtering
- Comprehensive test coverage
- End-to-end test application
- Extensive documentation

The implementation follows the same patterns as the existing Log4Net
instrumentation and supports NLog versions 4.0.0 through 6.*.*.

Configuration:
- OTEL_DOTNET_AUTO_LOGS_ENABLED=true
- OTEL_DOTNET_AUTO_LOGS_ENABLE_NLOG_BRIDGE=true
- OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE=true (optional)

Files added:
- src/OpenTelemetry.AutoInstrumentation/Instrumentations/NLog/
- test/OpenTelemetry.AutoInstrumentation.Tests/NLogTests.cs
- test/test-applications/integrations/TestApplication.NLog/

Files modified:
- Configuration classes to support NLog bridge
- Public API files to include new integration class
@danifitz danifitz requested a review from a team as a code owner August 6, 2025 15:08
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Aug 6, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

@danifitz
Copy link
Contributor Author

danifitz commented Aug 6, 2025

I will update the changelog and documentation once we've verified that the code itself looks okay

@danifitz danifitz mentioned this pull request Aug 7, 2025
Copy link
Member

@nrcventura nrcventura left a comment

Choose a reason for hiding this comment

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

Thank you for submitting this PR. In general, I think these changes look good, but I want to give more time for those that worked on the other bridge implementation to review this PR too.

[InstrumentMethod(
assemblyName: "NLog",
typeName: "NLog.Config.LoggingConfiguration",
methodName: "GetConfiguredNamedTargets",
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't find method with this name in any recent version of NLog.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry I made an incorrect assumption when following some of the work in Log4Net bridge instrumentation. I had to make quite a few changes in #1b5ee77

Copy link
Contributor

@lachmatt lachmatt Aug 8, 2025

Choose a reason for hiding this comment

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

We usually use test apps in integration tests, as in https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/186e653ad1a38e5c0b876f71aeef03bf3630cf7f/test/IntegrationTests/Log4NetBridgeTests.cs.
If we were to use this app for that purpose, it should probably be reworked.
Project should also be added to the solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I reviewed the approach and made some changes in 638a6aa

.ConfigureLogging(logging =>
{
// Clear default logging providers
logging.ClearProviders();
Copy link
Contributor

Choose a reason for hiding this comment

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

This clears all providers, including the one injected by autoinstrumentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this should be fixed by the test refactor I did in 638a6aa

/// The target integrates with NLog's architecture by implementing the target pattern,
/// allowing it to receive log events and forward them to OpenTelemetry for processing.
/// </summary>
internal class OpenTelemetryNLogTarget
Copy link

@snakefoot snakefoot Aug 10, 2025

Choose a reason for hiding this comment

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

Maybe choose a different class-name, since NLog Target is a reserved word. Maybe OpenTelemetryNLogOutput or OpenTelemetryNLogMapper or OpenTelemetryNLogConverter or OpenTelemetryNLogDestination.

Why not implement a standard NLog Target instead of all this reflection ?

NLog Logger -> NLog OpenTelemetryTarget -> OpenTelemetry Output.

Copy link

@snakefoot snakefoot Aug 10, 2025

Choose a reason for hiding this comment

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

Here is a very nice example of a NLog target for OpenTelemetryTarget using TargetWithContext, that allows one to enrich LogEvents with additional properties, and also configure from NLog.config-file:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @snakefoot thanks for your comments.

Given that this is for auto-instrumentation (zero-config), I feel like the current approach makes sense from that perspective. However, your points are spot on.
What do you think? Should we:

  • Keep the current auto-instrumentation approach but fix the naming?
  • Switch to a standard NLog Target approach (no longer zero-config)?
  • Implement both approaches - auto-instrumentation for zero-config, plus a Target for manual configuration?
    Your insight into using standard NLog patterns is helpful - it would result in cleaner code that follows NLog best practices.

Choose a reason for hiding this comment

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

NLog encourages the ability to configure the NLog Target from code or from NLog.config-file, instead of auto-instrumentation. Ofcourse good default values are encourages so the NLog Targets works out-of-the-box with no or little configuration.

When using TargetWithContext then one can use the NLog Layout abilities to enrich the OpenTelemetry-Logging-output with additional logeevent-properties. The NLog LoggingRules provides a lot of flexibility in filtering / routing NLog Logging output to the wanted target-destinations, but requires NLog targets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm taking a week off to go on holiday - i wil look to refactor this when I get back, thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi folks, I just now took a look at this PR and added a comment to request that we in fact go back to zero-code, so the original implementation of the OpenTelemetryNLogTarget looks like exactly what we want.

I apologize that I didn't comment sooner that zero-code is the direction we should take with this work. The reason for favoring this approach is that this project should not contain any other library dependencies except for the OTel SDK, so we should not be creating derived types that extend types from the NLog library.

…ogger.Log method

The original integration targeted a non-existent method
'GetConfiguredNamedTargets' in recent NLog versions. Changed to
intercept the actual NLog.Logger.Log(LogEventInfo) method which
is stable across NLog 4.0+ versions.

- Renamed TargetCollectionIntegration to LoggerIntegration
- Changed from OnMethodEnd to OnMethodBegin approach
- Updated public API references
- Fixes instrumentation for all NLog versions 4.0-6.*.*
…s following Log4NetBridge pattern

Rework TestApplication.NLog into TestApplication.NLogBridge with proper
integration test support. Add NLogBridgeTests.cs with complete coverage
of direct NLog usage, ILogger bridge, and trace context injection.

- Add to solution and LibraryVersionsGenerator
- Fix config to remove Windows-specific paths
- Support --api nlog and --api ILogger test modes
Copy link
Member

@Kielek Kielek left a comment

Choose a reason for hiding this comment

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

@danifitz, @snakefoot - I like the idea to introduce support for NLog here. If some/all cases cane be done by NLog directly, it is great (maybe context correlation can be done, without any changes here and we only need to document it/enable by default?)
The goal of the support should be also possibility to automatically also sent data through OTLP without any changes in the configuration/code.

Comment on lines 4 to 7
<PackageReference Include="NLog.Extensions.Logging" VersionOverride="$(LibraryVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
Copy link
Member

Choose a reason for hiding this comment

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

I am not fully familiar with NLog details. Is it necessary to bring all these dependencies to make it working? Can this list be reduced?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 799ef71

…tecture

- Replace CallTarget interception with standard NLog.Targets.TargetWithContext
- Add OpenTelemetry.AutoInstrumentation.NLogTarget project with OpenTelemetryTarget
- Implement NLogAutoInjector for zero-config auto-injection via CallTarget
- Rename OpenTelemetryNLogTarget to OpenTelemetryNLogConverter for clarity
- Update LoggerIntegration to trigger auto-injection and set GlobalDiagnosticsContext
- Rework TestApplication.NLogBridge to follow standard integration test pattern
- Update NLogBridgeTests to match Log4NetBridgeTests structure
- Add InternalsVisibleTo for new NLog target project access
- Update documentation to reflect dual-path architecture (auto-injection + manual config)
- Remove obsolete files and clean up project structure

This refactor aligns with NLog best practices by providing both automatic
instrumentation and a standard NLog Target that can be configured via
nlog.config or programmatically, leveraging NLog's native layout and
routing capabilities.
…NLogBridge

The package was not referenced in any source files and the test app implements
its own ILogger bridge for testing purposes.
@danifitz
Copy link
Contributor Author

I took onboard the comments from @snakefoot and @Kielek and undertook a refactor to align more with the NLog way of doing things.

The key aspects of this refactor:

  • Architecture Change: From pure CallTarget interception to a hybrid approach using a standard NLog Target
  • New Project: OpenTelemetry.AutoInstrumentation.NLogTarget with OpenTelemetryTarget
  • Auto-injection: NLogAutoInjector provides zero-config experience
  • Standards Compliance: Uses NLog.Targets.TargetWithContext following NLog conventions
  • Test Updates: Integration tests now follow the project's standard pattern
  • Documentation: Updated to reflect the new dual-path approach

206be87 represents an improvement in the NLog instrumentation's architecture, making it more maintainable and aligned with NLog's intended usage patterns.

}

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

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

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

public string? Headers { get; set; }

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757


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

public string? ServiceName { get; set; }

Choose a reason for hiding this comment

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

Change from string? to NLog Layout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

var current = Activity.Current;

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

Choose a reason for hiding this comment

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

Instead change to:

var renderedMessage = RenderLogEvent(Layout, logEvent);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757


// Emit using internal helpers via reflection delegate
var renderedMessage = logEvent.FormattedMessage;
var args = IncludeEventParameters && logEvent.Parameters is object[] p ? p : null;

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757


// Build properties from event properties and context
var properties = new List<KeyValuePair<string, object?>>();
if (IncludeEventProperties && logEvent.HasProperties && logEvent.Properties is not null)

Choose a reason for hiding this comment

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

logEvent.Properties is never null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

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

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

}
}

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

@snakefoot snakefoot Aug 20, 2025

Choose a reason for hiding this comment

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

Instead change to:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolved in 4401757

maximumVersion: "6.*.*",
integrationName: "NLog",
type: InstrumentationType.Log)]
public static class WriteLogEventToTargetsIntegration
Copy link
Member

@Kielek Kielek Dec 5, 2025

Choose a reason for hiding this comment

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

I think that this integration cover only part of the events going through NLog,

There are three WriteLogEventToTargets methods:

  • private void WriteToTargets(LogLevel level, Exception? ex, IFormatProvider? formatProvider, string message, object?[]? args) - covered, it goes through 2 parameter methos
  • private void WriteLogEventToTargets(LogEventInfo logEvent, ITargetWithFilterChain targetsForLevel) - covered
  • private void WriteLogEventToTargets(Type wrapperType, LogEventInfo logEvent, ITargetWithFilterChain targetsForLevel) - not covered

The missing part can be reached by public api: public void Log(Type wrapperType, LogEventInfo logEvent)

EDIT: Please cover this calls in our test application and add appropriate assertions in tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8a4483b

@Kielek Kielek force-pushed the feature/nlog-instrumentation branch from 187a0ac to 9ba276d Compare December 5, 2025 12:02

namespace TestApplication.NLogBridge;

internal class NLogLogger : Microsoft.Extensions.Logging.ILogger
Copy link
Member

Choose a reason for hiding this comment

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

I would consider using this package https://github.com/NLog/NLog.Extensions.Logging
It is common ILogger provider.
log4net implements it in our repository, because similar package was not available at the moment of writing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed a7a7068

danifitz and others added 8 commits December 8, 2025 17:43
Cover Logger.Log(Type wrapperType, LogEventInfo logEvent) API:
- WriteLogEventToTargetsWithWrapperTypeIntegration for NLog 6.x
- WriteToTargetsWithWrapperTypeIntegration for NLog 5.3.0+
- WriteToTargetsWithWrapperTypeLegacyIntegration for NLog 5.0.0-5.2.x
Replace custom NLogLoggerProvider with official NLog.Extensions.Logging
package for ILogger integration testing.
- Use "System.Type" string instead of non-existent ClrNames.Type
- Fix trailing newline in ILoggingEvent.cs
Use multiple [InstrumentMethod] attributes per class:
- NLogWriteToTargetsIntegration: 2-param methods (3 attributes)
- NLogWriteToTargetsWithWrapperTypeIntegration: 3-param methods (3 attributes)
@Kielek
Copy link
Member

Kielek commented Dec 9, 2025

Reviewed your changes (and pushed some fixes). The scope of the instrumented methods should be fine, but you missing coverage for them. You need to modify test application and test assertion to cover methods with Type wrapperType.

Kielek and others added 5 commits December 9, 2025 14:36
…load

- Add LogWithWrapperType method to test application to exercise 3-parameter
  WriteToTargets/WriteLogEventToTargets instrumentation
- Add test expectation for wrapperType log message in NLogBridgeTests
- Add NLog.Extensions.Logging to central package management
- Update AssertStandardOutputExpectations to verify wrapperType message
@danifitz
Copy link
Contributor Author

Added additional test coverage @Kielek

Copy link
Member

@Kielek Kielek left a comment

Choose a reason for hiding this comment

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

I have pushed some changes directly to the PR branch. I think it should be fine to merge.

@Kielek Kielek merged commit 3b51f6a into open-telemetry:main Dec 17, 2025
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[log-bridge] NLog

9 participants

Comments