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",