diff --git a/src/OpenTelemetry.Extensions/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions/.publicApi/PublicAPI.Unshipped.txt index 1b59a80012..064129dc23 100644 --- a/src/OpenTelemetry.Extensions/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Extensions/.publicApi/PublicAPI.Unshipped.txt @@ -14,6 +14,10 @@ OpenTelemetry.Trace.BaggageActivityProcessor OpenTelemetry.Trace.TracerProviderBuilderExtensions override OpenTelemetry.RateLimitingSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult override OpenTelemetry.Trace.BaggageActivityProcessor.OnStart(System.Diagnostics.Activity! data) -> void +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddBaggageProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! builder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddBaggageProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, System.Predicate! baggageKeyPredicate) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddBaggageProcessor(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddBaggageProcessor(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Predicate! baggageKeyPredicate) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AttachLogsToActivityEvent(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action? configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! static OpenTelemetry.Trace.BaggageActivityProcessor.AllowAllBaggageKeys.get -> System.Predicate! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAutoFlushActivityProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Func! predicate, int timeoutMilliseconds = 10000) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Extensions/CHANGELOG.md b/src/OpenTelemetry.Extensions/CHANGELOG.md index a0848e428f..23a55b79e5 100644 --- a/src/OpenTelemetry.Extensions/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions/CHANGELOG.md @@ -19,6 +19,9 @@ rate per second. For details see * Updated OpenTelemetry core component version(s) to `1.10.0`. ([#2317](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2317)) +* Adds Baggage LogRecord Processor. + ([#2354](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2354)) + ## 1.0.0-beta.5 Released 2024-May-08 diff --git a/src/OpenTelemetry.Extensions/Internal/BaggageLogRecordProcessor.cs b/src/OpenTelemetry.Extensions/Internal/BaggageLogRecordProcessor.cs new file mode 100644 index 0000000000..9fd4811ebd --- /dev/null +++ b/src/OpenTelemetry.Extensions/Internal/BaggageLogRecordProcessor.cs @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Logs; + +namespace OpenTelemetry.Extensions.Internal; + +internal sealed class BaggageLogRecordProcessor : BaseProcessor +{ + private readonly Predicate baggageKeyPredicate; + + public BaggageLogRecordProcessor(Predicate baggageKeyPredicate) + { + this.baggageKeyPredicate = baggageKeyPredicate ?? throw new ArgumentNullException(nameof(baggageKeyPredicate)); + } + + public static Predicate AllowAllBaggageKeys => (_) => true; + + public override void OnEnd(LogRecord data) + { + var baggage = Baggage.Current; + + if (data != null && baggage.Count > 0) + { + var capacity = (data.Attributes?.Count ?? 0) + baggage.Count; + var attributes = new List>(capacity); + + foreach (var entry in baggage) + { + if (this.baggageKeyPredicate(entry.Key)) + { + attributes.Add(new(entry.Key, entry.Value)); + } + } + + if (data.Attributes != null) + { + attributes.AddRange(data.Attributes); + } + + data.Attributes = attributes; + } + + base.OnEnd(data!); + } +} diff --git a/src/OpenTelemetry.Extensions/Internal/OpenTelemetryExtensionsEventSource.cs b/src/OpenTelemetry.Extensions/Internal/OpenTelemetryExtensionsEventSource.cs index 7cef228fb4..9963e43ee0 100644 --- a/src/OpenTelemetry.Extensions/Internal/OpenTelemetryExtensionsEventSource.cs +++ b/src/OpenTelemetry.Extensions/Internal/OpenTelemetryExtensionsEventSource.cs @@ -45,8 +45,14 @@ public void LogRecordFilterException(string? categoryName, string exception) } [Event(3, Message = "Baggage key predicate threw exeption when trying to add baggage entry with key '{0}'. Baggage entry will not be added to the activity. Exception: '{1}'", Level = EventLevel.Warning)] - public void BaggageKeyPredicateException(string baggageKey, string exception) + public void BaggageKeyActivityPredicateException(string baggageKey, string exception) { this.WriteEvent(3, baggageKey, exception); } + + [Event(4, Message = "Baggage key predicate threw exeption when trying to add baggage entry with key '{0}'. Baggage entry will not be added to the log record. Exception: '{1}'", Level = EventLevel.Warning)] + public void BaggageKeyLogRecordPredicateException(string baggageKey, string exception) + { + this.WriteEvent(4, baggageKey, exception); + } } diff --git a/src/OpenTelemetry.Extensions/Logs/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry.Extensions/Logs/OpenTelemetryLoggingExtensions.cs index ce4152036b..e349bb3742 100644 --- a/src/OpenTelemetry.Extensions/Logs/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry.Extensions/Logs/OpenTelemetryLoggingExtensions.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Extensions.Internal; using OpenTelemetry.Internal; using OpenTelemetry.Logs; @@ -34,4 +36,102 @@ public static OpenTelemetryLoggerOptions AttachLogsToActivityEvent( return loggerOptions.AddProcessor(new ActivityEventAttachingLogProcessor(options)); #pragma warning restore CA2000 // Dispose objects before losing scope } + + /// + /// Adds a processor to the OpenTelemetry which will copy all + /// baggage entries as log record attributes. + /// + /// to add the processor to. + /// The instance of to chain the calls. + /// is null. + /// + /// Copies all current baggage entries to log record attributes. + /// + public static OpenTelemetryLoggerOptions AddBaggageProcessor( + this OpenTelemetryLoggerOptions loggerOptions) + { + return loggerOptions.AddBaggageProcessor(BaggageLogRecordProcessor.AllowAllBaggageKeys); + } + + /// + /// Adds a processor to the OpenTelemetry which will conditionally copy + /// baggage entries as log record attributes. + /// + /// to add the processor to. + /// Predicate to determine which baggage keys should be added to the log record. + /// The instance of to chain the calls. + /// is null. + /// is null. + /// + /// Conditionally copies current baggage entries to log record attributes. + /// In case of an exception the predicate is treated as false, and the baggage entry will not be copied. + /// + public static OpenTelemetryLoggerOptions AddBaggageProcessor( + this OpenTelemetryLoggerOptions loggerOptions, + Predicate baggageKeyPredicate) + { + Guard.ThrowIfNull(loggerOptions); + Guard.ThrowIfNull(baggageKeyPredicate); + + return loggerOptions.AddProcessor(_ => SetupBaggageLogRecordProcessor(baggageKeyPredicate)); + } + + /// + /// Adds a processor to the OpenTelemetry which will copy all + /// baggage entries as log record attributes. + /// + /// to add the processor to. + /// The instance of to chain the calls. + /// is null. + /// + /// Copies all current baggage entries to log record attributes. + /// + public static LoggerProviderBuilder AddBaggageProcessor( + this LoggerProviderBuilder builder) + { + return builder.AddBaggageProcessor(BaggageLogRecordProcessor.AllowAllBaggageKeys); + } + + /// + /// Adds a processor to the OpenTelemetry which will copy all + /// baggage entries as log record attributes. + /// + /// to add the processor to. + /// Predicate to determine which baggage keys should be added to the log record. + /// The instance of to chain the calls. + /// is null. + /// is null. + /// + /// Conditionally copies current baggage entries to log record attributes. + /// In case of an exception the predicate is treated as false, and the baggage entry will not be copied. + /// + public static LoggerProviderBuilder AddBaggageProcessor( + this LoggerProviderBuilder builder, + Predicate baggageKeyPredicate) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(baggageKeyPredicate); + + return builder.AddProcessor(_ => SetupBaggageLogRecordProcessor(baggageKeyPredicate)); + } + + private static BaggageLogRecordProcessor SetupBaggageLogRecordProcessor(Predicate baggageKeyPredicate) + { + return new BaggageLogRecordProcessor(baggageKey => + { + try + { + return baggageKeyPredicate(baggageKey); + } + catch (Exception exception) + { + OpenTelemetryExtensionsEventSource.Log.BaggageKeyLogRecordPredicateException(baggageKey, exception.Message); + return false; + } + }); + } } diff --git a/src/OpenTelemetry.Extensions/README.md b/src/OpenTelemetry.Extensions/README.md index 6ad53f5476..0fafdc3d4e 100644 --- a/src/OpenTelemetry.Extensions/README.md +++ b/src/OpenTelemetry.Extensions/README.md @@ -20,6 +20,28 @@ future. Adds a log processor which will convert log messages into events and attach them to the currently running `Activity`. +### AddBaggageProcessor + +Adds a log processor which will copy baggage entries to log records. +The method takes an optional predicate to filter the copied baggage entries +based on the entry key. If no predicate is provided, all entries are copied. + +Example of AddBaggageProcessor usage with a predicate: + +```csharp +var regex = new Regex("^allow", RegexOptions.Compiled); +using var loggerFactory = LoggerFactory.Create(builder => builder +.AddOpenTelemetry(options => +{ + options.AddBaggageProcessor(regex.IsMatch); + // other set up (exporters, processors) +}) +``` + +Warning: The baggage key predicate is executed for every baggage entry for each +log record. +Do not use slow or intensive operations. + ## Traces ### AutoFlushActivityProcessor diff --git a/src/OpenTelemetry.Extensions/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Extensions/TracerProviderBuilderExtensions.cs index 8ab9da3ac7..4cfddb9213 100644 --- a/src/OpenTelemetry.Extensions/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Extensions/TracerProviderBuilderExtensions.cs @@ -68,7 +68,7 @@ public static TracerProviderBuilder AddBaggageActivityProcessor( } catch (Exception exception) { - OpenTelemetryExtensionsEventSource.Log.BaggageKeyPredicateException(baggageKey, exception.Message); + OpenTelemetryExtensionsEventSource.Log.BaggageKeyActivityPredicateException(baggageKey, exception.Message); return false; } })); diff --git a/test/OpenTelemetry.Extensions.Tests/Logs/LoggerFactoryBaggageLogRecordProcessorTests.cs b/test/OpenTelemetry.Extensions.Tests/Logs/LoggerFactoryBaggageLogRecordProcessorTests.cs new file mode 100644 index 0000000000..4c3d5e77b2 --- /dev/null +++ b/test/OpenTelemetry.Extensions.Tests/Logs/LoggerFactoryBaggageLogRecordProcessorTests.cs @@ -0,0 +1,135 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Extensions.Tests.Logs; + +public class LoggerFactoryBaggageLogRecordProcessorTests +{ + [Fact] + public void BaggageLogRecordProcessor_CanAddAllowAllBaggageKeysPredicate() + { + var logRecordList = new List(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddBaggageProcessor(); + options.AddInMemoryExporter(logRecordList); + }) + .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels + Baggage.SetBaggage("allow", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + } + + [Fact] + public void BaggageLogRecordProcessor_CanUseCustomPredicate() + { + var logRecordList = new List(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddBaggageProcessor((baggageKey) => baggageKey.StartsWith("allow", StringComparison.Ordinal)); + options.AddInMemoryExporter(logRecordList); + }) + .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "other_value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord.Attributes, kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_CanUseRegex() + { + var regex = new Regex("^allow", RegexOptions.Compiled); + var logRecordList = new List(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddBaggageProcessor(regex.IsMatch); + options.AddInMemoryExporter(logRecordList); + }) + .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "other_value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord.Attributes, kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_PredicateThrows_DoesNothing() + { + var logRecordList = new List(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddBaggageProcessor(_ => throw new Exception("Predicate throws an exception.")); + options.AddInMemoryExporter(logRecordList); + }) + .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels + Baggage.SetBaggage("deny", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.DoesNotContain(logRecord?.Attributes ?? [], kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_PredicateThrows_OnlyDropsEntriesThatThrow() + { + var logRecordList = new List(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddBaggageProcessor(key => + { + return key != "allow" ? throw new Exception("Predicate throws an exception.") : true; + }); + options.AddInMemoryExporter(logRecordList); + }) + .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "value"); + Baggage.SetBaggage("deny_2", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord?.Attributes ?? [], kv => kv.Key == "deny"); + } + + private static string GetTestMethodName([CallerMemberName] string callingMethodName = "") + { + return callingMethodName; + } +} diff --git a/test/OpenTelemetry.Extensions.Tests/Logs/LoggerProviderBuilderBaggageLogRecordProcessorTests.cs b/test/OpenTelemetry.Extensions.Tests/Logs/LoggerProviderBuilderBaggageLogRecordProcessorTests.cs new file mode 100644 index 0000000000..5a8c865c1e --- /dev/null +++ b/test/OpenTelemetry.Extensions.Tests/Logs/LoggerProviderBuilderBaggageLogRecordProcessorTests.cs @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Extensions.Tests.Logs; + +public class LoggerProviderBuilderBaggageLogRecordProcessorTests +{ + [Fact] + public void BaggageLogRecordProcessor_CanAddAllowAllBaggageKeysPredicate() + { + var logRecordList = new List(); + var sp = new ServiceCollection(); + sp.AddOpenTelemetry().WithLogging(builder => + { + builder.AddBaggageProcessor(); + builder.AddInMemoryExporter(logRecordList); + }); + var s = sp.BuildServiceProvider(); + var loggerFactory = s.GetRequiredService(); + Baggage.SetBaggage("allow", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + } + + [Fact] + public void BaggageLogRecordProcessor_CanUseCustomPredicate() + { + var logRecordList = new List(); + var sp = new ServiceCollection(); + sp.AddOpenTelemetry().WithLogging(builder => + { + builder.AddBaggageProcessor((baggageKey) => baggageKey.StartsWith("allow", StringComparison.Ordinal)); + builder.AddInMemoryExporter(logRecordList); + }); + var s = sp.BuildServiceProvider(); + var loggerFactory = s.GetRequiredService(); + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "other_value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord.Attributes, kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_CanUseRegex() + { + var regex = new Regex("^allow", RegexOptions.Compiled); + var logRecordList = new List(); + var sp = new ServiceCollection(); + sp.AddOpenTelemetry().WithLogging(builder => + { + builder.AddBaggageProcessor(regex.IsMatch); + builder.AddInMemoryExporter(logRecordList); + }); + var s = sp.BuildServiceProvider(); + var loggerFactory = s.GetRequiredService(); + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "other_value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord.Attributes, kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_PredicateThrows_DoesNothing() + { + var logRecordList = new List(); + var sp = new ServiceCollection(); + sp.AddOpenTelemetry().WithLogging(builder => + { + builder.AddBaggageProcessor(_ => throw new Exception("Predicate throws an exception.")); + builder.AddInMemoryExporter(logRecordList); + }); + var s = sp.BuildServiceProvider(); + var loggerFactory = s.GetRequiredService(); + Baggage.SetBaggage("deny", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.DoesNotContain(logRecord?.Attributes ?? [], kv => kv.Key == "deny"); + } + + [Fact] + public void BaggageLogRecordProcessor_PredicateThrows_OnlyDropsEntriesThatThrow() + { + var logRecordList = new List(); + var sp = new ServiceCollection(); + sp.AddOpenTelemetry().WithLogging(builder => + { + builder.AddBaggageProcessor(key => + { + return key != "allow" ? throw new Exception("Predicate throws an exception.") : true; + }); + builder.AddInMemoryExporter(logRecordList); + }); + var s = sp.BuildServiceProvider(); + var loggerFactory = s.GetRequiredService(); + Baggage.SetBaggage("allow", "value"); + Baggage.SetBaggage("deny", "value"); + Baggage.SetBaggage("deny_2", "value"); + + var logger = loggerFactory.CreateLogger(GetTestMethodName()); + logger.LogError("this does not matter"); + var logRecord = Assert.Single(logRecordList); + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Attributes); + Assert.Contains(logRecord.Attributes, kv => kv.Key == "allow"); + Assert.DoesNotContain(logRecord?.Attributes ?? [], kv => kv.Key == "deny"); + } + + private static string GetTestMethodName([CallerMemberName] string callingMethodName = "") + { + return callingMethodName; + } +} diff --git a/test/OpenTelemetry.Extensions.Tests/OpenTelemetry.Extensions.Tests.csproj b/test/OpenTelemetry.Extensions.Tests/OpenTelemetry.Extensions.Tests.csproj index 1aa7195de6..4372bf3b0e 100644 --- a/test/OpenTelemetry.Extensions.Tests/OpenTelemetry.Extensions.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Tests/OpenTelemetry.Extensions.Tests.csproj @@ -15,4 +15,9 @@ + + + + +