// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 using System.Text.RegularExpressions; using Google.Protobuf; using IntegrationTests.Helpers; using OpenTelemetry.Proto.Logs.V1; using Xunit.Abstractions; namespace IntegrationTests; public class 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 == 3, "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; Assert.Matches(regex, output); Assert.Contains("ERROR TestApplication.Log4NetBridge.Program - Exception occured span_id=(null) trace_id=(null) trace_flags=(null)", output); } private static bool VerifyAttributes(LogRecord logRecord) { var firstArgAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "0"); var secondArgAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "1"); var customAttribute = logRecord.Attributes.SingleOrDefault(value => value.Key == "test_key"); return firstArgAttribute?.Value.StringValue == "Hello" && secondArgAttribute?.Value.StringValue == "world" && logRecord.Attributes.Count(value => value.Key == "2") == 1 && customAttribute?.Value.StringValue == "test_value"; } private static bool VerifyTraceContext(LogRecord logRecord) { return logRecord.TraceId != ByteString.Empty && logRecord.SpanId != ByteString.Empty && logRecord.Flags != 0; } private static void AssertStandardOutputExpectations(string standardOutput) { Assert.Contains("INFO TestApplication.Log4NetBridge.Program - Hello, world at", standardOutput); Assert.Contains("ERROR TestApplication.Log4NetBridge.Program - Exception occured", standardOutput); } private static bool VerifyBody(LogRecord logRecord, string expectedBody) { return Convert.ToString(logRecord.Body) == $"{{ \"stringValue\": \"{expectedBody}\" }}"; } private static bool VerifyExceptionAttributes(LogRecord logRecord) { return logRecord.Attributes.Count(value => value.Key == "exception.stacktrace") == 1 && logRecord.Attributes.Count(value => value.Key == "exception.message") == 1 && logRecord.Attributes.Count(value => value.Key == "exception.type") == 1; } }