diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md index 3817c4cc899a..280287003f55 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md @@ -4,6 +4,11 @@ ### Features Added +* Added `EnableTraceBasedLogSampler` property to `AzureMonitorOptions` to enable + filtering logs based on trace sampling decisions, reducing log volume while + maintaining trace-log correlation. + ([#53441](https://github.com/Azure/azure-sdk-for-net/pull/53441)) + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.net8.0.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.net8.0.cs index 11ae3a19f1e5..3a59d85b44d5 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.net8.0.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.net8.0.cs @@ -7,6 +7,7 @@ public AzureMonitorOptions() { } public Azure.Core.TokenCredential Credential { get { throw null; } set { } } public bool DisableOfflineStorage { get { throw null; } set { } } public bool EnableLiveMetrics { get { throw null; } set { } } + public bool EnableTraceBasedLogsSampler { get { throw null; } set { } } public float SamplingRatio { get { throw null; } set { } } public string StorageDirectory { get { throw null; } set { } } public double? TracesPerSecond { get { throw null; } set { } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.netstandard2.0.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.netstandard2.0.cs index 11ae3a19f1e5..3a59d85b44d5 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.netstandard2.0.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/api/Azure.Monitor.OpenTelemetry.AspNetCore.netstandard2.0.cs @@ -7,6 +7,7 @@ public AzureMonitorOptions() { } public Azure.Core.TokenCredential Credential { get { throw null; } set { } } public bool DisableOfflineStorage { get { throw null; } set { } } public bool EnableLiveMetrics { get { throw null; } set { } } + public bool EnableTraceBasedLogsSampler { get { throw null; } set { } } public float SamplingRatio { get { throw null; } set { } } public string StorageDirectory { get { throw null; } set { } } public double? TracesPerSecond { get { throw null; } set { } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj index ebb14cac4436..45f66342e574 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj @@ -1,4 +1,4 @@ - + An OpenTelemetry .NET distro that exports to Azure Monitor AzureMonitor OpenTelemetry ASP.NET Core Distro @@ -24,10 +24,10 @@ - + - + diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorOptions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorOptions.cs index b63e743f008d..8c12e41e59ae 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorOptions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorOptions.cs @@ -48,6 +48,16 @@ public class AzureMonitorOptions : ClientOptions /// public bool EnableLiveMetrics { get; set; } = true; + /// + /// Enables or disables filtering logs based on trace sampling decisions. + /// + /// + /// When enabled, only logs associated with sampled traces are exported. + /// Logs without trace context are always exported. + /// This reduces log volume while maintaining trace-log correlation. + /// + public bool EnableTraceBasedLogsSampler { get; set; } = true; + /// /// Gets or sets the ratio of telemetry items to be sampled. The value must be between 0.0F and 1.0F, inclusive. /// For example, specifying 0.4 means that 40% of traces are sampled and 60% are dropped. @@ -86,6 +96,7 @@ internal void SetValueToExporterOptions(AzureMonitorExporterOptions exporterOpti exporterOptions.TracesPerSecond = TracesPerSecond; exporterOptions.StorageDirectory = StorageDirectory; exporterOptions.EnableLiveMetrics = EnableLiveMetrics; + exporterOptions.EnableTraceBasedLogsSampler = EnableTraceBasedLogsSampler; if (Transport != null) { exporterOptions.Transport = Transport; @@ -93,20 +104,5 @@ internal void SetValueToExporterOptions(AzureMonitorExporterOptions exporterOpti exporterOptions.Diagnostics.IsDistributedTracingEnabled = Diagnostics.IsDistributedTracingEnabled; exporterOptions.Diagnostics.IsLoggingEnabled = Diagnostics.IsLoggingEnabled; } - - //internal void SetValueToLiveMetricsOptions(AzureMonitorLiveMetricsOptions liveMetricsOptions) - //{ - // liveMetricsOptions.ConnectionString = ConnectionString; - // liveMetricsOptions.Credential = Credential; - // liveMetricsOptions.EnableLiveMetrics = EnableLiveMetrics; - - // if (Transport != null) - // { - // liveMetricsOptions.Transport = Transport; - // } - - // liveMetricsOptions.Diagnostics.IsDistributedTracingEnabled = Diagnostics.IsDistributedTracingEnabled; - // liveMetricsOptions.Diagnostics.IsLoggingEnabled = Diagnostics.IsLoggingEnabled; - //} } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureMonitorOptionsTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureMonitorOptionsTests.cs new file mode 100644 index 000000000000..c0d9a6c13cef --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureMonitorOptionsTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Azure.Monitor.OpenTelemetry.Exporter; +using Xunit; + +namespace Azure.Monitor.OpenTelemetry.AspNetCore.Tests +{ + public class AzureMonitorOptionsTests + { + private const string TestConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000"; + + [Fact] + public void AzureMonitorOptions_EnableTraceBasedLogsSampler_DefaultValue_IsTrue() + { + // Arrange & Act + var options = new AzureMonitorOptions(); + + // Assert + Assert.True(options.EnableTraceBasedLogsSampler); + } + + [Fact] + public void AzureMonitorOptions_EnableTraceBasedLogsSampler_CanBeDisabled() + { + // Arrange & Act + var options = new AzureMonitorOptions + { + EnableTraceBasedLogsSampler = false + }; + + // Assert + Assert.False(options.EnableTraceBasedLogsSampler); + } + + [Fact] + public void AzureMonitorOptions_SetValueToExporterOptions_CopiesEnableTraceBasedLogsSampler() + { + // Arrange + var azureMonitorOptions = new AzureMonitorOptions + { + ConnectionString = TestConnectionString, + EnableTraceBasedLogsSampler = false + }; + + var exporterOptions = new AzureMonitorExporterOptions(); + + // Act + azureMonitorOptions.SetValueToExporterOptions(exporterOptions); + + // Assert + Assert.False(exporterOptions.EnableTraceBasedLogsSampler); + Assert.Equal(TestConnectionString, exporterOptions.ConnectionString); + } + + [Fact] + public void UseAzureMonitor_DefaultEnableTraceBasedLogsSampler() + { + // Arrange + var serviceCollection = new ServiceCollection(); + + // Act + serviceCollection.AddOpenTelemetry() + .UseAzureMonitor(options => + { + options.ConnectionString = TestConnectionString; + options.DisableOfflineStorage = true; + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + // Assert + var azureMonitorOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + Assert.True(azureMonitorOptions.EnableTraceBasedLogsSampler); + + var exporterOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + Assert.True(exporterOptions.EnableTraceBasedLogsSampler); + } + + [Fact] + public void UseAzureMonitor_CanDisableTraceBasedLogsSampler() + { + // Arrange + var serviceCollection = new ServiceCollection(); + + // Act + serviceCollection.AddOpenTelemetry() + .UseAzureMonitor(options => + { + options.ConnectionString = TestConnectionString; + options.EnableTraceBasedLogsSampler = false; + options.DisableOfflineStorage = true; + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + // Assert + var azureMonitorOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + Assert.False(azureMonitorOptions.EnableTraceBasedLogsSampler); + + var exporterOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + Assert.False(exporterOptions.EnableTraceBasedLogsSampler); + } + + [Fact] + public void UseAzureMonitor_EnableTraceBasedLogsSampler_PropagatesCorrectly() + { + // Arrange + var serviceCollection = new ServiceCollection(); + + // Act - Test with true + serviceCollection.AddOpenTelemetry() + .UseAzureMonitor(options => + { + options.ConnectionString = TestConnectionString; + options.EnableTraceBasedLogsSampler = true; + options.DisableOfflineStorage = true; + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + // Assert + var azureMonitorOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + var exporterOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + Assert.Equal(azureMonitorOptions.EnableTraceBasedLogsSampler, exporterOptions.EnableTraceBasedLogsSampler); + Assert.True(exporterOptions.EnableTraceBasedLogsSampler); + } + + [Fact] + public void UseAzureMonitor_WithoutConfiguration_UsesDefaultValue() + { + // Arrange + var serviceCollection = new ServiceCollection(); + + // Act + serviceCollection.AddOpenTelemetry() + .UseAzureMonitor(options => + { + options.ConnectionString = TestConnectionString; + options.DisableOfflineStorage = true; + + // Not setting EnableTraceBasedLogsSampler, should use default + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + // Assert + var azureMonitorOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + var exporterOptions = serviceProvider.GetRequiredService>() + .Get(Options.DefaultName); + + // Both should be true (default value) + Assert.True(azureMonitorOptions.EnableTraceBasedLogsSampler); + Assert.True(exporterOptions.EnableTraceBasedLogsSampler); + } + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsTests.cs index 5059124efeda..5240d5b9d5e2 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/DefaultAzureMonitorOptionsTests.cs @@ -29,6 +29,7 @@ public void VerifyConfigure_Default() Assert.False(azureMonitorOptions.DisableOfflineStorage); Assert.Equal(1.0F, azureMonitorOptions.SamplingRatio); Assert.Null(azureMonitorOptions.StorageDirectory); + Assert.True(azureMonitorOptions.EnableTraceBasedLogsSampler); } #if NET @@ -39,7 +40,8 @@ public void VerifyConfigure_ViaJson() ""ConnectionString"" : ""testJsonValue"", ""DisableOfflineStorage"" : ""true"", ""SamplingRatio"" : 0.5, - ""StorageDirectory"" : ""testJsonValue"" + ""StorageDirectory"" : ""testJsonValue"", + ""EnableTraceBasedLogsSampler"" : ""true"" }}"; var configuration = new ConfigurationBuilder() diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/InitializationTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/InitializationTests.cs index a10e5bac996e..f8e969532cbd 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/InitializationTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/InitializationTests.cs @@ -401,7 +401,6 @@ public static void EvaluateLoggerProvider(IServiceProvider serviceProvider, bool var secondProcessor = valueField.GetValue(secondNode); Assert.NotNull(secondProcessor); - Assert.Contains("BatchLogRecordExportProcessor", secondProcessor.GetType().Name); var exporterProperty = secondProcessor.GetType().GetProperty("Exporter", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(exporterProperty); @@ -412,9 +411,6 @@ public static void EvaluateLoggerProvider(IServiceProvider serviceProvider, bool } else { - // When LiveMetrics is disabled, processor should be a BatchLogRecordExportProcessor - Assert.Contains("BatchLogRecordExportProcessor", processor.GetType().Name); - var exporterProperty = processor.GetType().GetProperty("Exporter", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(exporterProperty); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md index 1d470b7249f5..3d6ba6fae4d4 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md @@ -7,6 +7,11 @@ * Enabled resource metrics export by default. ([#53432](https://github.com/Azure/azure-sdk-for-net/pull/53432)) +* Added `EnableTraceBasedLogSampler` property to `AzureMonitorExporterOptions` + to enable filtering logs based on trace sampling decisions, reducing log + volume while maintaining trace-log correlation. + ([#53441](https://github.com/Azure/azure-sdk-for-net/pull/53441)) + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.net8.0.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.net8.0.cs index ee3faa78c67a..c900950d7ce9 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.net8.0.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.net8.0.cs @@ -15,6 +15,7 @@ public AzureMonitorExporterOptions(Azure.Monitor.OpenTelemetry.Exporter.AzureMon public Azure.Core.TokenCredential Credential { get { throw null; } set { } } public bool DisableOfflineStorage { get { throw null; } set { } } public bool EnableLiveMetrics { get { throw null; } set { } } + public bool EnableTraceBasedLogsSampler { get { throw null; } set { } } public float SamplingRatio { get { throw null; } set { } } public string StorageDirectory { get { throw null; } set { } } public double? TracesPerSecond { get { throw null; } set { } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.netstandard2.0.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.netstandard2.0.cs index ee3faa78c67a..c900950d7ce9 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.netstandard2.0.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/api/Azure.Monitor.OpenTelemetry.Exporter.netstandard2.0.cs @@ -15,6 +15,7 @@ public AzureMonitorExporterOptions(Azure.Monitor.OpenTelemetry.Exporter.AzureMon public Azure.Core.TokenCredential Credential { get { throw null; } set { } } public bool DisableOfflineStorage { get { throw null; } set { } } public bool EnableLiveMetrics { get { throw null; } set { } } + public bool EnableTraceBasedLogsSampler { get { throw null; } set { } } public float SamplingRatio { get { throw null; } set { } } public string StorageDirectory { get { throw null; } set { } } public double? TracesPerSecond { get { throw null; } set { } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs index b2cdebe7ba02..3a54291a8c94 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterExtensions.cs @@ -203,7 +203,12 @@ public static OpenTelemetryLoggerOptions AddAzureMonitorLogExporter( options.Credential ??= credential; } - return loggerOptions.AddProcessor(new BatchLogRecordExportProcessor(new AzureMonitorLogExporter(options))); + var exporter = new AzureMonitorLogExporter(options); + BaseProcessor processor = options.EnableTraceBasedLogsSampler + ? new LogFilteringProcessor(exporter) + : new BatchLogRecordExportProcessor(exporter); + + return loggerOptions.AddProcessor(processor); } /// @@ -271,7 +276,10 @@ public static LoggerProviderBuilder AddAzureMonitorLogExporter( sp.EnsureNoUseAzureMonitorExporterRegistrations(); // TODO: Do we need provide an option to alter BatchExportLogRecordProcessorOptions? - return new BatchLogRecordExportProcessor(new AzureMonitorLogExporter(exporterOptions)); + var exporter = new AzureMonitorLogExporter(exporterOptions); + return exporterOptions.EnableTraceBasedLogsSampler + ? new LogFilteringProcessor(exporter) + : new BatchLogRecordExportProcessor(exporter); }); } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterOptions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterOptions.cs index 6630b6e2fd14..a7441eed8138 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterOptions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/AzureMonitorExporterOptions.cs @@ -106,6 +106,16 @@ public enum ServiceVersion /// public bool EnableLiveMetrics { get; set; } = true; + /// + /// Enables or disables filtering logs based on trace sampling decisions. + /// + /// + /// When enabled, only logs associated with sampled traces are exported. + /// Logs without trace context are always exported. + /// This reduces log volume while maintaining trace-log correlation. + /// + public bool EnableTraceBasedLogsSampler { get; set; } = true; + /// /// Internal flag to control if Statsbeat is enabled. /// diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/ExporterRegistrationHostedService.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/ExporterRegistrationHostedService.cs index 98aa268da732..420565664aa2 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/ExporterRegistrationHostedService.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/ExporterRegistrationHostedService.cs @@ -83,19 +83,23 @@ private static void Initialize(IServiceProvider serviceProvider) var exporterOptions = serviceProvider!.GetRequiredService>().Get(Options.DefaultName); var exporter = new AzureMonitorLogExporter(exporterOptions); + BaseProcessor baseProcessor = exporterOptions.EnableTraceBasedLogsSampler + ? new LogFilteringProcessor(exporter) + : new BatchLogRecordExportProcessor(exporter); + if (exporterOptions.EnableLiveMetrics) { var manager = serviceProvider!.GetRequiredService(); - loggerProvider.AddProcessor(new CompositeProcessor(new BaseProcessor[] - { - new LiveMetricsLogProcessor(manager), - new BatchLogRecordExportProcessor(exporter) - })); + loggerProvider.AddProcessor(new CompositeProcessor( + [ + new LiveMetricsLogProcessor(manager), + baseProcessor + ])); } else { - loggerProvider.AddProcessor(new BatchLogRecordExportProcessor(exporter)); + loggerProvider.AddProcessor(baseProcessor); } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/LogFilteringProcessor.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/LogFilteringProcessor.cs new file mode 100644 index 000000000000..ee4744b129e6 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/LogFilteringProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Azure.Monitor.OpenTelemetry.Exporter.Internals +{ + internal class LogFilteringProcessor : BatchLogRecordExportProcessor + { + public LogFilteringProcessor(BaseExporter exporter) + : base(exporter) + { + } + + public override void OnEnd(LogRecord logRecord) + { + if (logRecord.SpanId == default || logRecord.TraceFlags == ActivityTraceFlags.Recorded) + { + base.OnEnd(logRecord); + } + } + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogFilteringProcessorTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogFilteringProcessorTests.cs new file mode 100644 index 000000000000..9ffaa7ce06d3 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogFilteringProcessorTests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using Azure.Monitor.OpenTelemetry.Exporter.Internals; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Logs; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Monitor.OpenTelemetry.Exporter.Tests +{ + public class LogFilteringProcessorTests + { + private readonly ITestOutputHelper output; + + public LogFilteringProcessorTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void LogFilteringProcessor_FiltersOutUnSampledLogsWithSpanId() + { + // Arrange + var exportedItems = new List(); + var testExporter = new TestExporter(exportedItems); + var processor = new LogFilteringProcessor(testExporter); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(processor); + }); + }); + + var logger = loggerFactory.CreateLogger(); + + // Create an activity that is NOT sampled + using var activity = new Activity("TestActivity"); + activity.ActivityTraceFlags = ActivityTraceFlags.None; // Not sampled + activity.Start(); + + // Act + logger.LogInformation("This log should be filtered out"); + + loggerFactory.Dispose(); + + // Assert + Assert.Empty(exportedItems); + } + + [Fact] + public void LogFilteringProcessor_AllowsSampledLogsWithSpanId() + { + // Arrange + var exportedItems = new List(); + var testExporter = new TestExporter(exportedItems); + var processor = new LogFilteringProcessor(testExporter); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(processor); + }); + }); + + var logger = loggerFactory.CreateLogger(); + + // Create an activity that IS sampled + using var activity = new Activity("TestActivity"); + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; // Sampled + activity.Start(); + + // Act + logger.LogInformation("This log should be exported"); + + loggerFactory.Dispose(); + + // Assert + Assert.Single(exportedItems); + } + + [Fact] + public void LogFilteringProcessor_AllowsLogsWithoutSpanId() + { + // Arrange + var exportedItems = new List(); + var testExporter = new TestExporter(exportedItems); + var processor = new LogFilteringProcessor(testExporter); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(processor); + }); + }); + + var logger = loggerFactory.CreateLogger(); + + // Act - no activity context + logger.LogInformation("This log should be exported"); + + loggerFactory.Dispose(); + + // Assert + Assert.Single(exportedItems); + } + + [Fact] + public void LogFilteringProcessor_MixedScenario() + { + // Arrange + var exportedItems = new List(); + var testExporter = new TestExporter(exportedItems); + var processor = new LogFilteringProcessor(testExporter); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(processor); + }); + }); + + var logger = loggerFactory.CreateLogger(); + + // Act + // Log without activity - should be exported + logger.LogInformation("Log 1: No activity"); + + // Log with sampled activity - should be exported + using (var sampledActivity = new Activity("SampledActivity")) + { + sampledActivity.ActivityTraceFlags = ActivityTraceFlags.Recorded; + sampledActivity.Start(); + logger.LogInformation("Log 2: Sampled activity"); + } + + // Log with unsampled activity - should NOT be exported + using (var unsampledActivity = new Activity("UnsampledActivity")) + { + unsampledActivity.ActivityTraceFlags = ActivityTraceFlags.None; + unsampledActivity.Start(); + logger.LogInformation("Log 3: Unsampled activity"); + } + + // Log without activity again - should be exported + logger.LogInformation("Log 4: No activity"); + + loggerFactory.Dispose(); + + // Assert + Assert.Equal(3, exportedItems.Count); + } + + private class TestExporter : BaseExporter + { + private readonly List exportedItems; + + public TestExporter(List exportedItems) + { + this.exportedItems = exportedItems; + } + + public override ExportResult Export(in Batch batch) + { + foreach (var item in batch) + { + this.exportedItems.Add(item); + } + + return ExportResult.Success; + } + } + } +}