diff --git a/.cspell/company-terms.txt b/.cspell/company-terms.txt
index 3f7ab9fb8e..dbb45209b1 100644
--- a/.cspell/company-terms.txt
+++ b/.cspell/company-terms.txt
@@ -1,9 +1,11 @@
+appender
+appenders
CNCF
opentelemetry
OTEL
OTLP
-tracecontext
-triager
-Zipkin
parentbased
+tracecontext
traceidratio
+triager
+Zipkin
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92547b0934..d83a17c675 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ This component adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.h
- Support for .NET9.
- Support for [RabbitMQ.Client](https://www.nuget.org/packages/RabbitMQ.Client/)
traces instrumentation for versions `7.0.0`+.
+- [log4net](https://www.nuget.org/packages/log4net/) [OpenTelemetry appender](https://opentelemetry.io/docs/concepts/signals/logs/#log-appender--bridge)
+ for versions >= `2.0.13` && < `4.0.0`
- Support for SqlClient metrics.
### Changed
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5c5de554a5..3e887794c1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,9 +2,9 @@
true
-
+
@@ -13,4 +13,4 @@
-
+
\ No newline at end of file
diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln
index f24b7789b6..e13bce2933 100644
--- a/OpenTelemetry.AutoInstrumentation.sln
+++ b/OpenTelemetry.AutoInstrumentation.sln
@@ -245,6 +245,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Owin.IIS.Ne
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkVersionAnalyzer", "tools\SdkVersionAnalyzer\SdkVersionAnalyzer.csproj", "{C75FA076-D460-414B-97F7-6F8D0E85AE74}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Log4NetBridge", "test\test-applications\integrations\TestApplication.Log4NetBridge\TestApplication.Log4NetBridge.csproj", "{926B7C03-42C2-4192-94A7-CD0B1C693279}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1513,6 +1515,22 @@ Global
{C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x64.Build.0 = Release|Any CPU
{C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x86.ActiveCfg = Release|Any CPU
{C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x86.Build.0 = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|x64.Build.0 = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Debug|x86.Build.0 = Debug|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|Any CPU.Build.0 = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|ARM64.Build.0 = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x64.ActiveCfg = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x64.Build.0 = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.ActiveCfg = Release|Any CPU
+ {926B7C03-42C2-4192-94A7-CD0B1C693279}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1602,6 +1620,7 @@ Global
{91D883EC-069E-46BC-B6F7-67C94299851E} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F}
diff --git a/build/LibraryVersions.g.cs b/build/LibraryVersions.g.cs
index 5014f2f808..1e0617ba8d 100644
--- a/build/LibraryVersions.g.cs
+++ b/build/LibraryVersions.g.cs
@@ -67,6 +67,14 @@ public static partial class LibraryVersion
new("2.67.0"),
}
},
+ {
+ "TestApplication.Log4NetBridge",
+ new List
+ {
+ new("2.0.13"),
+ new("3.0.3"),
+ }
+ },
{
"TestApplication.MassTransit",
new List
diff --git a/docs/config.md b/docs/config.md
index 91fa96a7e2..584ed0ab27 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -207,6 +207,7 @@ due to lack of stable semantic convention.
| ID | Instrumented library | Supported versions | Instrumentation type | Status |
|-----------|---------------------------------------------------------------------------------------------------------------------------------|--------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `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.0.13 && < 4.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
diff --git a/docs/internal/log4net.md b/docs/internal/log4net.md
new file mode 100644
index 0000000000..c75b211124
--- /dev/null
+++ b/docs/internal/log4net.md
@@ -0,0 +1,62 @@
+# `log4net` instrumentation
+
+> [!IMPORTANT]
+> log4net bridge and trace context injection are experimental features.
+> Both instrumentations can be disabled by setting `OTEL_DOTNET_AUTO_LOGS_LOG4NET_INSTRUMENTATION_ENABLED` to `false`.
+
+Both bridge and trace context injection are supported for `log4net` in versions >= 2.0.13 && < 4.0.0
+
+## `log4net` [logs bridge](https://opentelemetry.io/docs/specs/otel/glossary/#log-appender--bridge)
+
+The `log4net` logs bridge is disabled by default. In order to enable it, set `OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE` to `true`.
+When `log4net` logs bridge is enabled, and `log4net` is configured with at least 1 appender, application logs are exported in OTLP
+format by default to the local instance of OpenTelemetry Collector, in addition to being written into their currently configured destination (e.g. a file).
+
+### `log4net` logging events conversion
+
+`log4net`'s `LoggingEvent`s are converted to OpenTelemetry log records in a following way:
+
+- `TimeStampUtc` is set as a `Timestamp`
+- `Level.Name` is set as a `SeverityText`
+- If formatted strings were used for logging (e.g. by using `InfoFormat` or similar), format string is set as a `Body`
+- Otherwise, `RenderedMessage` is set as a `Body`
+- If formatted strings were used for logging, format arguments are added as attributes, with indexes as their names
+- If formatted strings were used for logging, and `OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE` is set, rendered message
+is added as `log4net.rendered_message` attribute
+- `LoggerName` is set as an `InstrumentationScope.Name`
+- `Properties`, apart from builtin properties prefixed with `log4net:`, 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
+
+#### `log4net` level severity mapping
+
+`log4net` levels are mapped to OpenTelemetry severity types according to the following rules based on their numerical values.
+
+Levels with numerical values of:
+
+- Equal to `Level.Fatal` or higher are mapped to `LogRecordSeverity.Fatal`
+- Higher than or equal to `Level.Error` but lower than `Level.Fatal` are mapped to `LogRecordSeverity.Error`
+- Higher than or equal to `Level.Warn` but lower than `Level.Error` are mapped to `LogRecordSeverity.Warn`
+- Higher than or equal to `Level.Info` but lower than `Level.Warn` are mapped to `LogRecordSeverity.Info`
+- Higher than or equal to `Level.Debug` but lower than `Level.Info` are mapped to `LogRecordSeverity.Debug`
+- Lower than `Level.Debug` are mapped to `LogRecordSeverity.Trace`
+
+## `log4net` trace context injection
+
+Following properties are added by default to 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.
+
+## Known limitations of `log4net` bridge
+
+In order for the bridge to be added, at least 1 other appender has to be configured.
+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/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
index aa82ba4ff9..0d2d414852 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt
@@ -1,2 +1,4 @@
+OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge.Integrations.AppenderCollectionIntegration
+OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations.AppenderAttachedImplIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecute
-OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecuteAsync
\ No newline at end of file
+OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecuteAsync
diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
index aa82ba4ff9..0d2d414852 100644
--- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net8.0/PublicAPI.Unshipped.txt
@@ -1,2 +1,4 @@
+OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge.Integrations.AppenderCollectionIntegration
+OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations.AppenderAttachedImplIntegration
OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecute
-OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecuteAsync
\ No newline at end of file
+OpenTelemetry.AutoInstrumentation.Instrumentations.MongoDB.Integrations.MongoClientIntegrationExecuteAsync
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
index 03e987a42d..3a14495716 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/ConfigurationKeys.cs
@@ -217,6 +217,12 @@ public static class Logs
///
public const string IncludeFormattedMessage = "OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE";
+ ///
+ /// Configuration key for whether or not experimental log4net bridge
+ /// should be enabled.
+ ///
+ public const string EnableLog4NetBridge = "OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE";
+
///
/// Configuration key for disabling all log instrumentations.
///
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationLogHelper.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationLogHelper.cs
new file mode 100644
index 0000000000..93523d66ae
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/EnvironmentConfigurationLogHelper.cs
@@ -0,0 +1,54 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Runtime.CompilerServices;
+using OpenTelemetry.AutoInstrumentation.Loading;
+using OpenTelemetry.AutoInstrumentation.Plugins;
+using OpenTelemetry.Logs;
+
+namespace OpenTelemetry.AutoInstrumentation.Configurations;
+
+internal static class EnvironmentConfigurationLogHelper
+{
+ public static LoggerProviderBuilder UseEnvironmentVariables(
+ this LoggerProviderBuilder builder,
+ LazyInstrumentationLoader lazyInstrumentationLoader,
+ LogSettings settings,
+ PluginManager pluginManager)
+ {
+ SetExporter(builder, settings, pluginManager);
+ return builder;
+ }
+
+ private static void SetExporter(LoggerProviderBuilder builder, LogSettings settings, PluginManager pluginManager)
+ {
+ foreach (var logExporter in settings.LogExporters)
+ {
+ builder = logExporter switch
+ {
+ LogExporter.Otlp => Wrappers.AddOtlpExporter(builder, settings, pluginManager),
+ LogExporter.Console => Wrappers.AddConsoleExporter(builder, pluginManager),
+ _ => throw new ArgumentOutOfRangeException($"Log exporter '{logExporter}' is incorrect")
+ };
+ }
+ }
+
+ private static class Wrappers
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static LoggerProviderBuilder AddConsoleExporter(LoggerProviderBuilder builder, PluginManager pluginManager)
+ {
+ return builder.AddConsoleExporter(pluginManager.ConfigureLogsOptions);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static LoggerProviderBuilder AddOtlpExporter(LoggerProviderBuilder builder, LogSettings settings, PluginManager pluginManager)
+ {
+ return builder.AddOtlpExporter(options =>
+ {
+ settings.OtlpSettings?.CopyTo(options);
+ pluginManager?.ConfigureLogsOptions(options);
+ });
+ }
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
index f83c33b094..b6ea9f456e 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogInstrumentation.cs
@@ -11,5 +11,10 @@ internal enum LogInstrumentation
///
/// ILogger instrumentation.
///
- ILogger
+ ILogger = 0,
+
+ ///
+ /// Log4Net instrumentation.
+ ///
+ Log4Net = 1,
}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
index 947df54c3a..7d98e346e2 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/LogSettings.cs
@@ -28,6 +28,11 @@ internal class LogSettings : Settings
///
public bool IncludeFormattedMessage { get; private set; }
+ ///
+ /// Gets a value indicating whether the experimental log4net bridge is enabled.
+ ///
+ public bool EnableLog4NetBridge { get; private set; }
+
///
/// Gets the list of enabled instrumentations.
///
@@ -48,6 +53,7 @@ protected override void OnLoad(Configuration configuration)
}
IncludeFormattedMessage = configuration.GetBool(ConfigurationKeys.Logs.IncludeFormattedMessage) ?? false;
+ EnableLog4NetBridge = configuration.GetBool(ConfigurationKeys.Logs.EnableLog4NetBridge) ?? 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 ccce9d9272..72b7361d6a 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(33);
+ var nativeCallTargetDefinitions = new List(35);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -90,6 +90,18 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray()
}
}
+ // Logs
+ var logSettings = Instrumentation.LogSettings.Value;
+ if (logSettings.LogsEnabled)
+ {
+ // Log4Net
+ if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.Log4Net))
+ {
+ nativeCallTargetDefinitions.Add(new("log4net", "log4net.Appender.AppenderCollection", "ToArray", new[] {"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", new[] {"System.Int32", "log4net.Core.LoggingEvent"}, 2, 0, 13, 3, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations.AppenderAttachedImplIntegration"));
+ }
+ }
+
// Metrics
var metricSettings = Instrumentation.MetricSettings.Value;
if (metricSettings.MetricsEnabled)
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 3666bb3523..0df3ed101f 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(36);
+ var nativeCallTargetDefinitions = new List(38);
// Traces
var tracerSettings = Instrumentation.TracerSettings.Value;
if (tracerSettings.TracesEnabled)
@@ -91,6 +91,13 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray()
var logSettings = Instrumentation.LogSettings.Value;
if (logSettings.LogsEnabled)
{
+ // Log4Net
+ if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.Log4Net))
+ {
+ nativeCallTargetDefinitions.Add(new("log4net", "log4net.Appender.AppenderCollection", "ToArray", new[] {"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", new[] {"System.Int32", "log4net.Core.LoggingEvent"}, 2, 0, 13, 3, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations.AppenderAttachedImplIntegration"));
+ }
+
// ILogger
if (logSettings.EnabledInstrumentations.Contains(LogInstrumentation.ILogger))
{
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
index b1973df75d..8d4cc5e028 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs
@@ -4,6 +4,7 @@
#if NET
using System.Diagnostics;
#endif
+using System.Reflection;
using OpenTelemetry.AutoInstrumentation.Configurations;
#if NET
using OpenTelemetry.AutoInstrumentation.ContinuousProfiler;
@@ -12,6 +13,7 @@
using OpenTelemetry.AutoInstrumentation.Loading;
using OpenTelemetry.AutoInstrumentation.Logging;
using OpenTelemetry.AutoInstrumentation.Plugins;
+using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
@@ -25,18 +27,26 @@ internal static class Instrumentation
private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
private static readonly LazyInstrumentationLoader LazyInstrumentationLoader = new();
+ private static readonly Lazy LoggerProviderFactory = new(InitializeLoggerProvider, true);
+
private static int _initialized;
private static int _isExiting;
private static SdkSelfDiagnosticsEventListener? _sdkEventListener;
private static TracerProvider? _tracerProvider;
private static MeterProvider? _meterProvider;
+
private static PluginManager? _pluginManager;
#if NET
private static ContinuousProfilerProcessor? _profilerProcessor;
#endif
+ internal static LoggerProvider? LoggerProvider
+ {
+ get => LoggerProviderFactory.Value;
+ }
+
internal static PluginManager? PluginManager => _pluginManager;
internal static ILifespanManager LifespanManager => LazyInstrumentationLoader.LifespanManager;
@@ -217,6 +227,29 @@ public static void Initialize()
}
}
+ private static LoggerProvider? InitializeLoggerProvider()
+ {
+ // 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))
+ {
+ // TODO: Replace reflection usage when Logs Api is made public in non-rc builds.
+ // Sdk.CreateLoggerProviderBuilder()
+ var createLoggerProviderBuilderMethod = typeof(Sdk).GetMethod("CreateLoggerProviderBuilder", BindingFlags.Static | BindingFlags.NonPublic)!;
+ var loggerProviderBuilder = createLoggerProviderBuilderMethod.Invoke(null, null) as LoggerProviderBuilder;
+
+ // TODO: plugins support
+ var loggerProvider = loggerProviderBuilder!
+ .SetResourceBuilder(ResourceConfigurator.CreateResourceBuilder(GeneralSettings.Value.EnabledResourceDetectors))
+ .UseEnvironmentVariables(LazyInstrumentationLoader, LogSettings.Value, _pluginManager!)
+ .Build();
+ Logger.Information("OpenTelemetry logger provider initialized.");
+ return loggerProvider;
+ }
+
+ return null;
+ }
+
#if NET
private static void InitializeContinuousProfiling(
object continuousProfilerExporter,
@@ -408,6 +441,11 @@ private static void OnExit(object? sender, EventArgs e)
#endif
_tracerProvider?.Dispose();
_meterProvider?.Dispose();
+ if (LoggerProviderFactory.IsValueCreated)
+ {
+ LoggerProvider?.Dispose();
+ }
+
_sdkEventListener?.Dispose();
Logger.Information("OpenTelemetry Automatic Instrumentation exit.");
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/Integrations/AppenderCollectionIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/Integrations/AppenderCollectionIntegration.cs
new file mode 100644
index 0000000000..01a8eed2d5
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/Integrations/AppenderCollectionIntegration.cs
@@ -0,0 +1,43 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using OpenTelemetry.AutoInstrumentation.CallTarget;
+#if NET
+using OpenTelemetry.AutoInstrumentation.Logger;
+#endif
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge.Integrations;
+
+///
+/// Log4Net AppenderCollection integration.
+///
+[InstrumentMethod(
+assemblyName: "log4net",
+typeName: "log4net.Appender.AppenderCollection",
+methodName: "ToArray",
+returnTypeName: "log4net.Appender.IAppender[]",
+parameterTypeNames: new string[0],
+minimumVersion: "2.0.13",
+maximumVersion: "3.*.*",
+integrationName: "Log4Net",
+type: InstrumentationType.Log)]
+public static class AppenderCollectionIntegration
+{
+ internal static CallTargetReturn OnMethodEnd(TTarget instance, TReturn returnValue, Exception exception, in CallTargetState state)
+ {
+ if (
+ Instrumentation.LogSettings.Value.EnableLog4NetBridge &&
+ #if NET
+#pragma warning disable SA1003
+ !LoggerInitializer.IsInitializedAtLeastOnce &&
+#pragma warning restore SA1003
+#endif
+ returnValue is Array responseArray)
+ {
+ var finalArray = OpenTelemetryAppenderInitializer.Initialize(responseArray);
+ return new CallTargetReturn(finalArray);
+ }
+
+ return new CallTargetReturn(returnValue);
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryAppenderInitializer.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryAppenderInitializer.cs
new file mode 100644
index 0000000000..ae91e0e87e
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryAppenderInitializer.cs
@@ -0,0 +1,29 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge;
+
+internal static class OpenTelemetryAppenderInitializer
+{
+ // ReSharper disable StaticMemberInGenericType
+ private static readonly Type AppenderType;
+
+ private static object? _otelAppender;
+
+ static OpenTelemetryAppenderInitializer()
+ {
+ AppenderType = typeof(TAppenderArray).GetElementType()!;
+ }
+
+ public static TAppenderArray Initialize(Array initial)
+ {
+ var newArray = Array.CreateInstance(AppenderType, initial.Length + 1);
+ Array.Copy(initial, newArray, initial.Length);
+ _otelAppender ??= OpenTelemetryLog4NetAppender.Instance.DuckImplement(AppenderType);
+
+ newArray.SetValue(_otelAppender, newArray.Length - 1);
+ return (TAppenderArray)(object)newArray;
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLog4NetAppender.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLog4NetAppender.cs
new file mode 100644
index 0000000000..0bbdc3369e
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLog4NetAppender.cs
@@ -0,0 +1,186 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Collections;
+using System.Diagnostics;
+using System.Reflection;
+using OpenTelemetry.AutoInstrumentation.DuckTyping;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection;
+using OpenTelemetry.AutoInstrumentation.Logging;
+using OpenTelemetry.Logs;
+using Exception = System.Exception;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge;
+
+internal class OpenTelemetryLog4NetAppender
+{
+ // Level thresholds, as defined in https://github.com/apache/logging-log4net/blob/2d68abc25dd77a69926b16234510377c9b63acad/src/log4net/Core/Level.cs
+ private const int FatalThreshold = 110_000;
+ private const int ErrorThreshold = 70_000;
+ private const int WarningThreshold = 60_000;
+ private const int InfoThreshold = 40_000;
+ private const int DebugThreshold = 30_000;
+ private const int LevelOffValue = int.MaxValue;
+ private const string SystemStringFormatTypeName = "log4net.Util.SystemStringFormat";
+
+ private static readonly IOtelLogger Logger = OtelLogging.GetLogger();
+ private static readonly Lazy InstanceField = new(InitializeAppender, true);
+
+ private readonly Func? _getLoggerFactory;
+
+ private OpenTelemetryLog4NetAppender(LoggerProvider loggerProvider)
+ {
+ _getLoggerFactory = CreateGetLoggerDelegate(loggerProvider);
+ }
+
+ public static OpenTelemetryLog4NetAppender Instance => InstanceField.Value;
+
+ [DuckReverseMethod]
+ public string Name { get; set; } = nameof(OpenTelemetryLog4NetAppender);
+
+ [DuckReverseMethod(ParameterTypeNames = new[] { "log4net.Core.LoggingEvent, log4net" })]
+ public void DoAppend(ILoggingEvent loggingEvent)
+ {
+ if (Sdk.SuppressInstrumentation || loggingEvent.Level.Value == LevelOffValue)
+ {
+ return;
+ }
+
+ object? logger = null;
+
+ if (_getLoggerFactory is not null)
+ {
+ logger = _getLoggerFactory(loggingEvent.LoggerName);
+ }
+
+ var logEmitter = OpenTelemetryLogHelpers.LogEmitter;
+
+ if (logEmitter is null || logger is null)
+ {
+ return;
+ }
+
+ var level = loggingEvent.Level;
+ var mappedLogLevel = MapLogLevel(level.Value);
+
+ string? format = null;
+ string? renderedMessage = null;
+ object?[]? args = null;
+ var messageObject = loggingEvent.MessageObject;
+
+ // Try to extract message format and args used
+ // when *Format methods are used for logging, e.g. InfoFormat
+ if (messageObject.TryDuckCast(out var stringFormat) && stringFormat.Type is { FullName: SystemStringFormatTypeName })
+ {
+ format = stringFormat.Format;
+ args = stringFormat.Args;
+ }
+ else if (messageObject.TryDuckCast(out var stringFormatOld) && stringFormatOld.Type is { FullName: SystemStringFormatTypeName })
+ {
+ format = stringFormatOld.Format;
+ args = stringFormatOld.Args;
+ }
+
+ // Add rendered message as an attribute only if format was extracted successfully,
+ // and addition of rendered message was requested.
+ if (format is not null && Instrumentation.LogSettings.Value.IncludeFormattedMessage)
+ {
+ renderedMessage = loggingEvent.RenderedMessage;
+ }
+
+ logEmitter(
+ logger,
+ format ?? loggingEvent.RenderedMessage,
+ loggingEvent.TimeStampUtc,
+ loggingEvent.Level.Name,
+ mappedLogLevel,
+ loggingEvent.ExceptionObject,
+ GetProperties(loggingEvent),
+ Activity.Current,
+ args,
+ renderedMessage);
+ }
+
+ [DuckReverseMethod]
+ public void Close()
+ {
+ }
+
+ private static IEnumerable>? GetProperties(ILoggingEvent loggingEvent)
+ {
+ // Due to known issues, attempt to retrieve properties
+ // might throw on operating systems other than Windows.
+ // This seems to be fixed for versions 2.0.13 and above.
+ 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("log4net:") ||
+ 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 OpenTelemetryLog4NetAppender InitializeAppender()
+ {
+ return new OpenTelemetryLog4NetAppender(Instrumentation.LoggerProvider!);
+ }
+
+#pragma warning disable SA1202
+ internal static int MapLogLevel(int levelValue)
+#pragma warning restore SA1202
+ {
+ return levelValue switch
+ {
+ // Fatal and above -> LogRecordSeverity.Fatal
+ >= FatalThreshold => 21,
+ // Between Error and Fatal -> LogRecordSeverity.Error
+ >= ErrorThreshold => 17,
+ // Between Warn and Error -> LogRecordSeverity.Warn
+ >= WarningThreshold => 13,
+ // Between Info and Warn -> LogRecordSeverity.Info
+ >= InfoThreshold => 9,
+ // Between Debug and Info -> LogRecordSeverity.Debug
+ >= DebugThreshold => 5,
+ // Smaller than Debug -> LogRecordSeverity.Trace
+ _ => 1
+ };
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLogHelpers.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLogHelpers.cs
new file mode 100644
index 0000000000..2b25a33d95
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/Bridge/OpenTelemetryLogHelpers.cs
@@ -0,0 +1,317 @@
+// 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.Log4Net.Bridge;
+
+internal delegate void EmitLog(object loggerInstance, string? body, DateTime timestamp, string? severityText, int severityLevel, Exception? exception, IEnumerable>? properties, Activity? current, object?[]? args, string? renderedMessage);
+
+// 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
+ {
+ 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")!;
+
+ LogEmitter = BuildEmitLog(logRecordDataType, logRecordAttributesListType, loggerType!);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to initialize LogEmitter delegate.");
+ }
+ }
+
+ public static EmitLog? LogEmitter { get; }
+
+ private static BlockExpression BuildLogRecord(
+ Type logRecordDataType,
+ Type severityType,
+ ParameterExpression body,
+ ParameterExpression timestamp,
+ ParameterExpression severityText,
+ ParameterExpression severityLevel,
+ ParameterExpression activity)
+ {
+ // Creates expression:
+ // .Block(OpenTelemetry.Logs.LogRecordData $instance) {
+ // $instance = .New OpenTelemetry.Logs.LogRecordData($activity);
+ // .If ($body != null) {
+ // .Call $instance.set_Body($body)
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // .Call $instance.set_Timestamp($timestamp);
+ // .If ($severityText != null) {
+ // .Call $instance.set_SeverityText($severityText)
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // .Call $instance.set_Severity((System.Nullable`1[OpenTelemetry.Logs.LogRecordSeverity])$severityLevel);
+ // $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);
+ }
+
+ private static BlockExpression BuildLogRecordAttributes(
+ Type logRecordAttributesListType,
+ ParameterExpression exception,
+ ParameterExpression properties,
+ ParameterExpression argsParam,
+ ParameterExpression renderedMessageParam)
+ {
+ // Creates expression:
+ // .Block(OpenTelemetry.Logs.LogRecordAttributeList $instance) {
+ // $instance = .New OpenTelemetry.Logs.LogRecordAttributeList();
+ // .If ($exception != null) {
+ // .Call $instance.RecordException($exception)
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // .If ($renderedMessage != null) {
+ // .Call $instance.Add(
+ // "log4net.rendered_message",
+ // $renderedMessage)
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // .If ($properties != null) {
+ // .Block(System.Collections.Generic.IEnumerator`1[System.Collections.Generic.KeyValuePair`2[System.String,System.Object]] $enumerator)
+ // {
+ // $enumerator = .Call $properties.GetEnumerator();
+ // .Try {
+ // .Loop {
+ // .If (.Call $enumerator.MoveNext() == True) {
+ // .Block(
+ // System.Collections.Generic.KeyValuePair`2[System.String,System.Object] $loopVar,
+ // System.String $key) {
+ // $loopVar = $enumerator.Current;
+ // $key = (System.String)$loopVar.Key;
+ // .Call $instance.Add(
+ // $key,
+ // $loopVar.Value)
+ // }
+ // } .Else {
+ // .Break #Label1 { }
+ // }
+ // }
+ // .LabelTarget #Label1:
+ // } .Finally {
+ // .Block(System.IDisposable $disposable) {
+ // $disposable = $enumerator .As System.IDisposable;
+ // .If ($disposable != null) {
+ // .Call ((System.IDisposable)$enumerator).Dispose()
+ // } .Else {
+ // .Default(System.Void)
+ // }
+ // }
+ // }
+ // }
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // .If ($args != null) {
+ // .Loop {
+ // .Block(
+ // System.Int32 $index,
+ // System.Object $value) {
+ // .If ($index < $args.Length) {
+ // .Block() {
+ // $value = $args[$index];
+ // .Call $instance.Add(
+ // .Call $index.ToString(),
+ // $value);
+ // $index++
+ // }
+ // } .Else {
+ // .Break #Label2 { }
+ // }
+ // }
+ // }
+ // .LabelTarget #Label2:
+ // } .Else {
+ // .Default(System.Void)
+ // };
+ // $instance
+ // }
+
+ var stringType = typeof(string);
+
+ var enumeratorInterface = typeof(IEnumerator);
+ var disposableInterface = typeof(IDisposable);
+
+ var dictionaryEnumerator = typeof(IEnumerator>);
+
+ var exceptionRecordMethod = logRecordAttributesListType.GetMethod("RecordException", BindingFlags.Instance | BindingFlags.Public)!;
+ var addAttributeMethod = logRecordAttributesListType.GetMethod("Add", BindingFlags.Instance | BindingFlags.Public, null, new[] { stringType, typeof(object) }, null)!;
+ var disposeMethod = disposableInterface.GetMethod(nameof(IDisposable.Dispose))!;
+ var moveNextMethod = enumeratorInterface.GetMethod(nameof(IEnumerator.MoveNext))!;
+ var getEnumeratorMethod = typeof(IEnumerable>).GetMethod(nameof(IEnumerable.GetEnumerator))!;
+ var toStringMethod = typeof(object).GetMethod(nameof(ToString))!;
+
+ var exitLabel = Expression.Label();
+
+ var instanceVar = Expression.Variable(logRecordAttributesListType, "instance");
+ var enumeratorVar = Expression.Variable(dictionaryEnumerator, "enumerator");
+ var loopVar = Expression.Variable(typeof(KeyValuePair), "loopVar");
+ var disposable = Expression.Variable(typeof(IDisposable), "disposable");
+ var keyVar = Expression.Variable(stringType, "key");
+
+ var assignInstanceVar = Expression.Assign(instanceVar, Expression.New(logRecordAttributesListType));
+ var recordExceptionIfNotNull = Expression.IfThen(Expression.NotEqual(exception, Expression.Constant(null)), Expression.Call(instanceVar, exceptionRecordMethod, exception));
+ var setRenderedMessageIfNotNull = Expression.IfThen(Expression.NotEqual(renderedMessageParam, Expression.Constant(null)), Expression.Call(instanceVar, addAttributeMethod, Expression.Constant("log4net.rendered_message"), renderedMessageParam));
+
+ var getPropertiesEnumerator = Expression.Call(properties, getEnumeratorMethod);
+ var enumeratorAssign = Expression.Assign(enumeratorVar, getPropertiesEnumerator);
+ var enumeratorDispose = Expression.Call(Expression.Convert(enumeratorVar, disposableInterface), disposeMethod);
+
+ var moveNext = Expression.Call(enumeratorVar, moveNextMethod);
+
+ var getKeyProperty = Expression.Convert(Expression.Property(loopVar, "Key"), stringType);
+
+ var loopAndAddProperties = Expression.Loop(
+ Expression.IfThenElse(
+ Expression.Equal(moveNext, Expression.Constant(true)),
+ Expression.Block(
+ new[] { loopVar, keyVar },
+ Expression.Assign(loopVar, Expression.Property(enumeratorVar, nameof(IEnumerator.Current))),
+ Expression.Assign(keyVar, getKeyProperty),
+ Expression.Call(instanceVar, addAttributeMethod, keyVar, Expression.Property(loopVar, "Value"))),
+ Expression.Break(exitLabel)),
+ exitLabel);
+
+ var addPropertiesWithForeach =
+ Expression.Block(
+ new[] { enumeratorVar },
+ enumeratorAssign,
+ Expression.TryFinally(
+ loopAndAddProperties,
+ Expression.Block(
+ new[] { disposable },
+ Expression.Assign(disposable, Expression.TypeAs(enumeratorVar, disposableInterface)),
+ Expression.IfThen(Expression.NotEqual(disposable, Expression.Constant(null)), enumeratorDispose))));
+
+ var exitLabel2 = Expression.Label();
+
+ var argsIndexVar = Expression.Variable(typeof(int), "index");
+ var argsValueVar = Expression.Variable(typeof(object), "value");
+
+ var loopAndAddArgs = Expression.Loop(
+ Expression.Block(
+ new[] { argsIndexVar, argsValueVar },
+ Expression.IfThenElse(
+ Expression.LessThan(argsIndexVar, Expression.Property(argsParam, nameof(Array.Length))),
+ Expression.Block(
+ Expression.Assign(argsValueVar, Expression.ArrayIndex(argsParam, argsIndexVar)),
+ Expression.Call(instanceVar, addAttributeMethod, Expression.Call(argsIndexVar, toStringMethod), argsValueVar),
+ Expression.PostIncrementAssign(argsIndexVar)),
+ Expression.Break(exitLabel2))),
+ exitLabel2);
+
+ var addPropertiesIfNotNull = Expression.IfThen(Expression.NotEqual(properties, Expression.Constant(null)), addPropertiesWithForeach);
+ var addArgsIfNotNull = Expression.IfThen(Expression.NotEqual(argsParam, Expression.Constant(null)), loopAndAddArgs);
+ return Expression.Block(
+ new[] { instanceVar },
+ assignInstanceVar,
+ recordExceptionIfNotNull,
+ setRenderedMessageIfNotNull,
+ addPropertiesIfNotNull,
+ addArgsIfNotNull,
+ instanceVar);
+ }
+
+ private static EmitLog? BuildEmitLog(Type logRecordDataType, Type logRecordAttributesListType, Type loggerType)
+ {
+ // Creates expression:
+ // .Lambda #Lambda1(
+ // System.Object $instance,
+ // System.String $body,
+ // System.DateTime $timestamp,
+ // System.String $severityText,
+ // System.Int32 $severityLevel,
+ // System.Exception $exception,
+ // System.Collections.Generic.IEnumerable`1[System.Collections.Generic.KeyValuePair`2[System.String,System.Object]] $properties,
+ // System.Diagnostics.Activity $activity,
+ // System.Object[] $args,
+ // System.String $renderedMessage) {
+ // .Block(
+ // OpenTelemetry.Logs.LogRecordData $logRecordData,
+ // OpenTelemetry.Logs.LogRecordAttributeList $logRecordAttributes) {
+ // $logRecordData = BuildLogRecord expression's value;
+ // $logRecordAttributes = BuildLogRecordAttributes expression's value;
+ // .Call ((OpenTelemetry.Logs.LoggerSdk)$instance).EmitLog(
+ // $logRecordData,
+ // $logRecordAttributes)
+ // }
+ // }
+
+ var stringType = typeof(string);
+
+ var instance = Expression.Parameter(typeof(object), "instance");
+ var bodyParam = Expression.Parameter(stringType, "body");
+ var timestampParam = Expression.Parameter(typeof(DateTime), "timestamp");
+ var severityTextParam = Expression.Parameter(stringType, "severityText");
+ var severityLevelParam = Expression.Parameter(typeof(int), "severityLevel");
+ var activityParam = Expression.Parameter(typeof(Activity), "activity");
+ var argsParam = Expression.Parameter(typeof(object[]), "args");
+ var renderedMessageParam = Expression.Parameter(typeof(string), "renderedMessage");
+
+ var exceptionParam = Expression.Parameter(typeof(Exception), "exception");
+ var propertiesParam = Expression.Parameter(typeof(IEnumerable>), "properties");
+
+ var logRecordDataVar = Expression.Variable(logRecordDataType, "logRecordData");
+ var logRecordAttributesVar = Expression.Variable(logRecordAttributesListType, "logRecordAttributes");
+
+ var instanceCasted = Expression.Convert(instance, loggerType);
+
+ var methodInfo = loggerType.GetMethod("EmitLog", BindingFlags.Instance | BindingFlags.Public, null, new[] { logRecordDataType.MakeByRefType(), logRecordAttributesListType.MakeByRefType() }, null);
+
+ var logRecord = BuildLogRecord(logRecordDataType, typeof(LoggerProvider).Assembly.GetType("OpenTelemetry.Logs.LogRecordSeverity")!, bodyParam, timestampParam, severityTextParam, severityLevelParam, activityParam);
+ var logRecordAttributes = BuildLogRecordAttributes(logRecordAttributesListType, exceptionParam, propertiesParam, argsParam, renderedMessageParam);
+
+ var block = Expression.Block(
+ new[] { logRecordDataVar, logRecordAttributesVar },
+ Expression.Assign(logRecordDataVar, logRecord),
+ Expression.Assign(logRecordAttributesVar, logRecordAttributes),
+ Expression.Call(instanceCasted, methodInfo!, logRecordDataVar, logRecordAttributesVar));
+
+ var expr = Expression.Lambda(block, instance, bodyParam, timestampParam, severityTextParam, severityLevelParam, exceptionParam, propertiesParam, activityParam, argsParam, renderedMessageParam);
+
+ return expr.Compile();
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/ILoggingEvent.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/ILoggingEvent.cs
new file mode 100644
index 0000000000..2803875ace
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/ILoggingEvent.cs
@@ -0,0 +1,55 @@
+// 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.Log4Net;
+
+// Wraps https://github.com/apache/logging-log4net/blob/2d68abc25dd77a69926b16234510377c9b63acad/src/log4net/Core/LoggingEvent.cs#L168
+internal interface ILoggingEvent
+{
+ public LoggingLevel Level { get; }
+
+ public string? LoggerName { get; }
+
+ public string? RenderedMessage { get; }
+
+ public Exception? ExceptionObject { get; }
+
+ public DateTime TimeStampUtc { get; }
+
+ public object MessageObject { get; }
+
+ // used for injecting trace context
+ public IDictionary? Properties { get; }
+
+ public IDictionary? GetProperties();
+}
+
+internal interface IStringFormatNew : IDuckType
+{
+ public string Format { get; set; }
+
+ public object?[]? Args { get; set; }
+}
+
+internal interface IStringFormatOld : IDuckType
+{
+ [DuckField(Name = "m_args")]
+ public object[] Args { get; }
+
+ [DuckField(Name = "m_format")]
+ public string Format { get; }
+}
+
+// Wraps https://github.com/apache/logging-log4net/blob/2d68abc25dd77a69926b16234510377c9b63acad/src/log4net/Core/Level.cs#L86
+[DuckCopy]
+internal struct LoggingLevel
+{
+ public int Value;
+
+ public string Name;
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/Integrations/AppenderAttachedImplIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/Integrations/AppenderAttachedImplIntegration.cs
new file mode 100644
index 0000000000..ed0af2236a
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/Integrations/AppenderAttachedImplIntegration.cs
@@ -0,0 +1,38 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using OpenTelemetry.AutoInstrumentation.CallTarget;
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection.Integrations;
+
+///
+/// Log4Net AppenderAttachedImplIntegration integration.
+///
+[InstrumentMethod(
+ assemblyName: "log4net",
+ typeName: "log4net.Util.AppenderAttachedImpl",
+ methodName: "AppendLoopOnAppenders",
+ returnTypeName: ClrNames.Int32,
+ parameterTypeNames: new[] { "log4net.Core.LoggingEvent" },
+ minimumVersion: "2.0.13",
+ maximumVersion: "3.*.*",
+ integrationName: "Log4Net",
+ type: InstrumentationType.Log)]
+public static class AppenderAttachedImplIntegration
+{
+ internal static CallTargetState OnMethodBegin(TTarget instance, TLoggingEvent loggingEvent)
+ where TLoggingEvent : ILoggingEvent
+ {
+ var current = Activity.Current;
+ if (current == null || loggingEvent.Properties == null)
+ {
+ return CallTargetState.GetDefault();
+ }
+
+ loggingEvent.Properties[LogsTraceContextInjectionConstants.SpanIdPropertyName] = current.SpanId.ToHexString();
+ loggingEvent.Properties[LogsTraceContextInjectionConstants.TraceIdPropertyName] = current.TraceId.ToHexString();
+ loggingEvent.Properties[LogsTraceContextInjectionConstants.TraceFlagsPropertyName] = (current.Context.TraceFlags & ActivityTraceFlags.Recorded) != 0 ? "01" : "00";
+ return CallTargetState.GetDefault();
+ }
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/LogsTraceContextInjectionConstants.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/LogsTraceContextInjectionConstants.cs
new file mode 100644
index 0000000000..23a0e4dcff
--- /dev/null
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Log4Net/TraceContextInjection/LogsTraceContextInjectionConstants.cs
@@ -0,0 +1,11 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.TraceContextInjection;
+
+internal static class LogsTraceContextInjectionConstants
+{
+ public const string SpanIdPropertyName = "span_id";
+ public const string TraceIdPropertyName = "trace_id";
+ public const string TraceFlagsPropertyName = "trace_flags";
+}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Logger/LoggingBuilderIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Logger/LoggingBuilderIntegration.cs
index 0baa2b7230..cd736f2992 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Logger/LoggingBuilderIntegration.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Logger/LoggingBuilderIntegration.cs
@@ -33,8 +33,8 @@ internal static CallTargetReturn OnMethodEnd(TTarget instance, Exceptio
{
if (instance is not null)
{
- var logBuilderExtensionsType = Type.GetType("OpenTelemetry.AutoInstrumentation.Logger.LogBuilderExtensions, OpenTelemetry.AutoInstrumentation");
- var methodInfo = logBuilderExtensionsType?.GetMethod("AddOpenTelemetryLogsFromIntegration");
+ var loggerInitializer = Type.GetType("OpenTelemetry.AutoInstrumentation.Logger.LoggerInitializer, OpenTelemetry.AutoInstrumentation");
+ var methodInfo = loggerInitializer?.GetMethod("AddOpenTelemetryLogsFromIntegration");
methodInfo?.Invoke(null, [instance]);
}
diff --git a/src/OpenTelemetry.AutoInstrumentation/Logger/LogBuilderExtensions.cs b/src/OpenTelemetry.AutoInstrumentation/Logger/LoggerInitializer.cs
similarity index 94%
rename from src/OpenTelemetry.AutoInstrumentation/Logger/LogBuilderExtensions.cs
rename to src/OpenTelemetry.AutoInstrumentation/Logger/LoggerInitializer.cs
index f79a51fdd6..57a5020e28 100644
--- a/src/OpenTelemetry.AutoInstrumentation/Logger/LogBuilderExtensions.cs
+++ b/src/OpenTelemetry.AutoInstrumentation/Logger/LoggerInitializer.cs
@@ -10,10 +10,13 @@
namespace OpenTelemetry.AutoInstrumentation.Logger;
-internal static class LogBuilderExtensions
+internal static class LoggerInitializer
{
+ private static volatile bool _initializedAtLeastOnce;
private static Type? _loggingProviderSdkType;
+ public static bool IsInitializedAtLeastOnce => _initializedAtLeastOnce;
+
// this method is only called from LoggingBuilderIntegration
public static void AddOpenTelemetryLogsFromIntegration(ILoggingBuilder builder)
{
@@ -85,6 +88,7 @@ private static void AddOpenTelemetryLogs(ILoggingBuilder builder)
}
}
});
+ _initializedAtLeastOnce = true;
AutoInstrumentationEventSource.Log.Information($"Logs: Loaded AddOpenTelemetry from LoggingBuilder.");
}
diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props
index ff3a97330c..bb88eca59e 100644
--- a/test/Directory.Packages.props
+++ b/test/Directory.Packages.props
@@ -26,6 +26,7 @@
+
diff --git a/test/IntegrationTests/LibraryVersions.g.cs b/test/IntegrationTests/LibraryVersions.g.cs
index df8a22dfa5..7bddd5148e 100644
--- a/test/IntegrationTests/LibraryVersions.g.cs
+++ b/test/IntegrationTests/LibraryVersions.g.cs
@@ -97,6 +97,20 @@ public static TheoryData GrpcNetClient
#else
theoryData.Add("2.52.0");
theoryData.Add("2.67.0");
+#endif
+ return theoryData;
+ }
+ }
+ public static TheoryData log4net
+ {
+ get
+ {
+ var theoryData = new TheoryData();
+#if DEFAULT_TEST_PACKAGE_VERSIONS
+ theoryData.Add(string.Empty);
+#else
+ theoryData.Add("2.0.13");
+ theoryData.Add("3.0.3");
#endif
return theoryData;
}
@@ -349,6 +363,7 @@ public static TheoryData Kafka_x64
{ "EntityFrameworkCorePomeloMySql", EntityFrameworkCorePomeloMySql },
{ "GraphQL", GraphQL },
{ "GrpcNetClient", GrpcNetClient },
+ { "log4net", log4net },
{ "MassTransit", MassTransit },
{ "SqlClientMicrosoft", SqlClientMicrosoft },
{ "SqlClientSystem", SqlClientSystem },
diff --git a/test/IntegrationTests/Log4NetBridgeTests.cs b/test/IntegrationTests/Log4NetBridgeTests.cs
new file mode 100644
index 0000000000..b537c79e37
--- /dev/null
+++ b/test/IntegrationTests/Log4NetBridgeTests.cs
@@ -0,0 +1,190 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.RegularExpressions;
+using FluentAssertions;
+using Google.Protobuf;
+using IntegrationTests.Helpers;
+using OpenTelemetry.Proto.Logs.V1;
+using Xunit.Abstractions;
+
+namespace IntegrationTests;
+
+public class Log4NetBridgeTests : TestHelper
+{
+ public Log4NetBridgeTests(ITestOutputHelper output)
+ : base("Log4NetBridge", output)
+ {
+ }
+
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.log4net), MemberType = typeof(LibraryVersion))]
+ public void SubmitLogs_ThroughLog4NetBridge_WhenLog4NetIsUsedDirectlyForLogging(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_LOG4NET_BRIDGE", "true");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api log4net"
+ });
+
+ AssertStandardOutputExpectations(standardOutput);
+
+ collector.AssertExpectations();
+ }
+
+#if NET
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.log4net), MemberType = typeof(LibraryVersion))]
+ public void SubmitLogs_ThroughILoggerBridge_WhenLog4NetIsUsedAsILoggerAppenderForLogging(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: "Information", 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_LOG4NET_BRIDGE", "true");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api ILogger"
+ });
+
+ AssertStandardOutputExpectations(standardOutput);
+
+ collector.AssertExpectations();
+ }
+
+ [Theory]
+ [Trait("Category", "EndToEnd")]
+ [MemberData(nameof(LibraryVersion.log4net), MemberType = typeof(LibraryVersion))]
+ public async Task SubmitLogs_ThroughILoggerBridge_WhenLog4NetIsUsedAsILoggerAppenderForLogging_WithoutDuplicates(string packageVersion)
+ {
+ using var collector = new MockLogsCollector(Output);
+ SetExporter(collector);
+
+ collector.ExpectCollected(records => records.Count == 2, "App logs should be exported once.");
+
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_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.log4net), MemberType = typeof(LibraryVersion))]
+ public void TraceContext_IsInjectedIntoCurrentLog4NetLogsDestination(string packageVersion)
+ {
+ EnableBytecodeInstrumentation();
+ SetEnvironmentVariable("OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE", "false");
+
+ var (standardOutput, _, _) = RunTestApplication(new()
+ {
+ PackageVersion = packageVersion,
+ Arguments = "--api log4net"
+ });
+
+ var regex = new Regex(@"INFO TestApplication\.Log4NetBridge\.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;
+ regex.IsMatch(output).Should().BeTrue();
+ output.Should().Contain("ERROR TestApplication.Log4NetBridge.Program - Exception occured span_id=(null) trace_id=(null) trace_flags=(null)");
+ }
+
+ 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)
+ {
+ standardOutput.Should().Contain("INFO TestApplication.Log4NetBridge.Program - Hello, world at");
+ standardOutput.Should().Contain("ERROR TestApplication.Log4NetBridge.Program - Exception occured");
+ }
+
+ 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 d319d2ee01..f10809c863 100644
--- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs
@@ -358,6 +358,7 @@ internal void MeterSettings_Instrumentations_SupportedValues(string meterInstrum
[Theory]
[InlineData("ILOGGER", LogInstrumentation.ILogger)]
+ [InlineData("LOG4NET", LogInstrumentation.Log4Net)]
internal void LogSettings_Instrumentations_SupportedValues(string logInstrumentation, LogInstrumentation expectedLogInstrumentation)
{
Environment.SetEnvironmentVariable(ConfigurationKeys.Logs.LogsInstrumentationEnabled, "false");
diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Log4NetTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/Log4NetTests.cs
new file mode 100644
index 0000000000..4e29b5ab83
--- /dev/null
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Log4NetTests.cs
@@ -0,0 +1,62 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using FluentAssertions;
+using log4net.Core;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net;
+using OpenTelemetry.AutoInstrumentation.Instrumentations.Log4Net.Bridge;
+using OpenTelemetry.Trace;
+using Xunit;
+
+namespace OpenTelemetry.AutoInstrumentation.Tests;
+
+public class Log4NetTests
+{
+ // TODO: Remove when Logs Api is made public in non-rc builds.
+ private static readonly Type OpenTelemetryLogSeverityType = typeof(Tracer).Assembly.GetType("OpenTelemetry.Logs.LogRecordSeverity")!;
+
+ public static TheoryData GetData()
+ {
+ var theoryData = new TheoryData
+ {
+ { Level.Emergency.Value, GetOpenTelemetrySeverityValue("Fatal") },
+ { Level.Fatal.Value, GetOpenTelemetrySeverityValue("Fatal") },
+ { Level.Alert.Value, GetOpenTelemetrySeverityValue("Error") },
+ { Level.Critical.Value, GetOpenTelemetrySeverityValue("Error") },
+ { Level.Severe.Value, GetOpenTelemetrySeverityValue("Error") },
+ { Level.Error.Value, GetOpenTelemetrySeverityValue("Error") },
+ { Level.Warn.Value, GetOpenTelemetrySeverityValue("Warn") },
+ { Level.Notice.Value, GetOpenTelemetrySeverityValue("Info") },
+ { Level.Info.Value, GetOpenTelemetrySeverityValue("Info") },
+ { Level.Debug.Value, GetOpenTelemetrySeverityValue("Debug") },
+ { Level.Trace.Value, GetOpenTelemetrySeverityValue("Trace") },
+ { Level.Verbose.Value, GetOpenTelemetrySeverityValue("Trace") }
+ };
+
+ return theoryData;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetData))]
+ public void BuiltinLog4NetLevelValues_AreMapped(int log4NetLevelValue, int expectedOpenTelemetrySeverity)
+ {
+ OpenTelemetryLog4NetAppender.MapLogLevel(log4NetLevelValue).Should().Be(expectedOpenTelemetrySeverity);
+ }
+
+ [Theory]
+ // LogLevel.Warn(60000) + 10, LogRecordSeverity.Warn (13)
+ [InlineData(60010, 13)]
+ // LogLevel.Info(40000) + 10, LogRecordSeverity.Info (9)
+ [InlineData(40010, 9)]
+ // Everything below Debug(30000) threshold is mapped to LogRecordSeverity.Trace
+ [InlineData(29900, 1)]
+ public void Log4NetLevelValuesWithoutADirectMatch_AreMappedToALessSevereValue(int log4NetLevelValue, int expectedOpenTelemetrySeverity)
+ {
+ OpenTelemetryLog4NetAppender.MapLogLevel(log4NetLevelValue).Should().Be(expectedOpenTelemetrySeverity);
+ }
+
+ private static int GetOpenTelemetrySeverityValue(string val)
+ {
+ return (int)Enum.Parse(OpenTelemetryLogSeverityType, val);
+ }
+}
diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/OpenTelemetry.AutoInstrumentation.Tests.csproj b/test/OpenTelemetry.AutoInstrumentation.Tests/OpenTelemetry.AutoInstrumentation.Tests.csproj
index 04f4a93f04..3323675c6f 100644
--- a/test/OpenTelemetry.AutoInstrumentation.Tests/OpenTelemetry.AutoInstrumentation.Tests.csproj
+++ b/test/OpenTelemetry.AutoInstrumentation.Tests/OpenTelemetry.AutoInstrumentation.Tests.csproj
@@ -3,6 +3,7 @@
+
diff --git a/test/test-applications/integrations/TestApplication.Log4NetBridge/App.config b/test/test-applications/integrations/TestApplication.Log4NetBridge/App.config
new file mode 100644
index 0000000000..b2f99c8f2a
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/App.config
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/test-applications/integrations/TestApplication.Log4NetBridge/Log4NetLogger.cs b/test/test-applications/integrations/TestApplication.Log4NetBridge/Log4NetLogger.cs
new file mode 100644
index 0000000000..5d7b55ba06
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/Log4NetLogger.cs
@@ -0,0 +1,52 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using log4net;
+using log4net.Config;
+using Microsoft.Extensions.Logging;
+
+namespace TestApplication.Log4NetBridge;
+
+internal class Log4NetLogger : ILogger
+{
+ private readonly ILog _log;
+
+ public Log4NetLogger(string categoryName)
+ {
+ _log = LogManager.GetLogger(categoryName);
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.Information:
+ _log.Info(formatter(state, exception));
+ break;
+ case LogLevel.Error:
+ _log.Error(formatter(state, exception));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
+ }
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ LogLevel.Critical => _log.IsFatalEnabled,
+ LogLevel.Debug or LogLevel.Trace => _log.IsDebugEnabled,
+ LogLevel.Error => _log.IsErrorEnabled,
+ LogLevel.Information => _log.IsInfoEnabled,
+ 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.Log4NetBridge/Log4NetLoggerProvider.cs b/test/test-applications/integrations/TestApplication.Log4NetBridge/Log4NetLoggerProvider.cs
new file mode 100644
index 0000000000..202cf8892a
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/Log4NetLoggerProvider.cs
@@ -0,0 +1,18 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using Microsoft.Extensions.Logging;
+
+namespace TestApplication.Log4NetBridge;
+
+public class Log4NetLoggerProvider : ILoggerProvider
+{
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new Log4NetLogger(categoryName);
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.Log4NetBridge/Program.cs b/test/test-applications/integrations/TestApplication.Log4NetBridge/Program.cs
new file mode 100644
index 0000000000..eeb7a42db6
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/Program.cs
@@ -0,0 +1,73 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using log4net.Config;
+using Microsoft.Extensions.Logging;
+
+[assembly: log4net.Config.XmlConfigurator(Watch = true)]
+
+namespace TestApplication.Log4NetBridge;
+
+internal static class Program
+{
+ private static readonly ActivitySource Source = new("TestApplication.Log4NetBridge");
+
+ private static void Main(string[] args)
+ {
+ if (args.Length == 2)
+ {
+ log4net.GlobalContext.Properties["test_key"] = "test_value";
+ var logApiName = args[1];
+ switch (logApiName)
+ {
+ case "log4net":
+ LogUsingLog4NetDirectly();
+ break;
+ case "ILogger":
+ LogUsingILogger();
+ break;
+ default:
+ throw new NotSupportedException($"{logApiName} is not supported.");
+ }
+ }
+ else
+ {
+ throw new ArgumentException("Invalid arguments.");
+ }
+ }
+
+ private static void LogUsingILogger()
+ {
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddProvider(new Log4NetLoggerProvider());
+ });
+ 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 LogUsingLog4NetDirectly()
+ {
+ var log = log4net.LogManager.GetLogger(typeof(Program));
+ LogInsideActiveScope(() => log.InfoFormat("{0}, {1} at {2:t}!", "Hello", "world", DateTime.Now));
+
+ var (message, ex) = GetException();
+ log.Error(message, ex);
+ }
+
+ private static (string Message, Exception Exception) GetException()
+ {
+ return ("Exception occured", new Exception());
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.Log4NetBridge/Properties/launchSettings.json b/test/test-applications/integrations/TestApplication.Log4NetBridge/Properties/launchSettings.json
new file mode 100644
index 0000000000..4a6451abb5
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "TestApplication.Log4NetBridge": {
+ "commandLineArgs": "--api log4net",
+ "commandName": "Project",
+ "environmentVariables": {
+ "COR_ENABLE_PROFILING": "1",
+ "COR_PROFILER": "{918728DD-259F-4A6A-AC2B-B85E1B658318}",
+ "COR_PROFILER_PATH": "$(SolutionDir)bin\\tracer-home\\win-x64\\OpenTelemetry.AutoInstrumentation.Native.dll",
+ "CORECLR_ENABLE_PROFILING": "1",
+ "CORECLR_PROFILER": "{918728DD-259F-4A6A-AC2B-B85E1B658318}",
+ "CORECLR_PROFILER_PATH": "$(SolutionDir)bin\\tracer-home\\win-x64\\OpenTelemetry.AutoInstrumentation.Native.dll",
+ "DOTNET_ADDITIONAL_DEPS": "$(SolutionDir)bin\\tracer-home\\AdditionalDeps",
+ "DOTNET_SHARED_STORE": "$(SolutionDir)bin\\tracer-home\\store",
+ "DOTNET_STARTUP_HOOKS": "$(SolutionDir)bin\\tracer-home\\net\\OpenTelemetry.AutoInstrumentation.StartupHook.dll",
+ "OTEL_DOTNET_AUTO_HOME": "$(SolutionDir)bin\\tracer-home",
+ "OTEL_SERVICE_NAME": "TestApplication.Log4NetBridge",
+ "OTEL_LOG_LEVEL": "debug",
+ "OTEL_LOGS_EXPORTER": "otlp,console",
+ "OTEL_TRACES_EXPORTER": "none",
+ "OTEL_DOTNET_AUTO_LOGS_ENABLE_LOG4NET_BRIDGE": "true",
+ "OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE": "true",
+ "OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES": "TestApplication.Log4NetBridge"
+ }
+ }
+ }
+}
diff --git a/test/test-applications/integrations/TestApplication.Log4NetBridge/TestApplication.Log4NetBridge.csproj b/test/test-applications/integrations/TestApplication.Log4NetBridge/TestApplication.Log4NetBridge.csproj
new file mode 100644
index 0000000000..1db72e03de
--- /dev/null
+++ b/test/test-applications/integrations/TestApplication.Log4NetBridge/TestApplication.Log4NetBridge.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
index 53607846eb..11c0b77e21 100644
--- a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
+++ b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs
@@ -88,6 +88,19 @@ all lower versions than 8.15.10 contains references impacted by
}
},
new()
+ {
+ IntegrationName = "log4net",
+ NugetPackageName = "log4net",
+ TestApplicationName = "TestApplication.Log4NetBridge",
+ Versions = new List
+ {
+ // versions below 2.0.10 have critical vulnerabilities
+ // versions below 2.0.13 have known bugs e.g. https://issues.apache.org/jira/browse/LOG4NET-652
+ new("2.0.13"),
+ new("*")
+ }
+ },
+ new()
{
IntegrationName = "MassTransit",
NugetPackageName = "MassTransit",